AutomationFlowsWeb Scraping › AI Tailoring Engine

AI Tailoring Engine

AI Tailoring Engine. Uses httpRequest. Webhook trigger; 5 nodes.

Webhook trigger★★★★☆ complexity5 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 5 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "AI Tailoring Engine",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ai-tailoring-engine",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "aabbccdd-eeff-0011-2233-445566778899",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Support both direct JSON and webhook body wrapper\nconst raw  = $input.first().json;\nconst body = raw.body || raw;\n\nlet resumeJson = body.resume_json;\nlet jdRaw = body.jd_raw || '';\n\n// If resume_json arrived as a string (double-encoded), parse it\nif (typeof resumeJson === 'string') {\n  try { resumeJson = JSON.parse(resumeJson); } catch(e) {}\n}\n\n// Normalise jd_raw to string regardless of how it arrived\nif (typeof jdRaw === 'object') {\n  jdRaw = jdRaw.jd_raw || JSON.stringify(jdRaw);\n}\njdRaw = String(jdRaw).trim();\n\nif (!resumeJson) throw new Error('resume_json is required. Got: ' + typeof body.resume_json + ' keys: ' + Object.keys(body).join(','));\nif (!jdRaw)      throw new Error('jd_raw is required');\n\nconst resumeStr = JSON.stringify(resumeJson, null, 2);\n\n// Trim JD to a reasonable size\nconst jdTrimmed = jdRaw.slice(0, 8000);\n\nconst outputSchema = {\n  tailored_resume: {\n    contact:    { name: 'string', email: 'string', phone: 'string', linkedin: 'string', github: 'string' },\n    summary:    'string \u2014 rewritten to mirror JD language and highlight candidate fit',\n    skills:     [{ category: 'string', items: ['string \u2014 JD-matching skills listed first'] }],\n    experience: [{ company: 'string', title: 'string', dates: 'string', bullets: ['string \u2014 strong verbs + JD keywords'] }],\n    projects:   [{ name: 'string', tech: ['string'], bullets: ['string \u2014 relevance to JD highlighted'] }],\n    education:  [{ institution: 'string', degree: 'string', dates: 'string', gpa: 'string' }]\n  },\n  matched_keywords: ['string \u2014 keywords from JD present or aligned in tailored resume'],\n  ats_score: 'integer 0\u2013100'\n};\n\nconst prompt = `You are a professional resume optimization expert specializing in ATS optimization.\n\nTailor the candidate's resume to the job description and return a structured JSON response.\n\n## CRITICAL RULES:\n1. Only use facts from the candidate's resume. NEVER add skills, companies, titles, or metrics not already present.\n2. You MAY rewrite bullets with stronger action verbs and JD keywords \u2014 only when the underlying fact supports it.\n3. You MAY re-rank skills so JD-matching items appear first within each category.\n4. You MAY rewrite the summary to emphasize the most relevant experience.\n5. Never fabricate GPA, dates, certifications, or quantitative metrics.\n6. Preserve ALL original experience, education, and project entries \u2014 do not drop any.\n7. Return ONLY valid JSON. No markdown fences, no explanatory text.\n\n## CANDIDATE RESUME (JSON):\n${resumeStr}\n\n## JOB DESCRIPTION:\n---\n${jdTrimmed}\n---\n\n## REQUIRED OUTPUT SCHEMA:\n${JSON.stringify(outputSchema, null, 2)}\n\n## ATS SCORE METHODOLOGY:\nCount distinct JD keywords/phrases present in the tailored resume, estimate coverage as 0\u2013100 integer.\n\nReturn the JSON object now:`;\n\nreturn [{ json: { prompt } }];\n"
      },
      "id": "bbccddee-ff00-1122-3344-556677889900",
      "name": "Build Tailoring Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $env.GEMINI_API_KEY }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { contents: [{ role: 'user', parts: [{ text: $json.prompt }] }], generationConfig: { response_mime_type: 'application/json', temperature: 0.2, maxOutputTokens: 8192 } } }}",
        "options": {
          "timeout": 90000
        }
      },
      "id": "ccddeeff-0011-2233-4455-667788990011",
      "name": "Gemini API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        700,
        300
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const response = $input.first().json;\n\nif (response.error) {\n  throw new Error('Gemini API error: ' + JSON.stringify(response.error));\n}\n\nlet rawText;\ntry {\n  rawText = response.candidates[0].content.parts[0].text;\n} catch (e) {\n  throw new Error('Unexpected Gemini response: ' + JSON.stringify(response).slice(0, 300));\n}\n\nconst cleaned = rawText\n  .replace(/^```json\\s*/i, '')\n  .replace(/^```\\s*/i, '')\n  .replace(/\\s*```$/i, '')\n  .trim();\n\nlet parsed;\ntry {\n  parsed = JSON.parse(cleaned);\n} catch (e) {\n  throw new Error('JSON parse failed: ' + e.message + ' | Raw: ' + rawText.slice(0, 200));\n}\n\nif (!parsed.tailored_resume || typeof parsed.tailored_resume !== 'object') {\n  throw new Error('Gemini response missing tailored_resume key');\n}\n\nconst tr = parsed.tailored_resume;\nconst normalised = {\n  contact:    tr.contact    || { name: '', email: '', phone: '', linkedin: '', github: '' },\n  summary:    tr.summary    || '',\n  skills:     Array.isArray(tr.skills)     ? tr.skills     : [],\n  experience: Array.isArray(tr.experience) ? tr.experience : [],\n  projects:   Array.isArray(tr.projects)   ? tr.projects   : [],\n  education:  Array.isArray(tr.education)  ? tr.education  : []\n};\n\nconst matched = Array.isArray(parsed.matched_keywords) ? parsed.matched_keywords : [];\nlet score = parseInt(parsed.ats_score, 10);\nif (isNaN(score)) score = 0;\nscore = Math.max(0, Math.min(100, score));\n\nreturn [{ json: { tailored_resume: normalised, matched_keywords: matched, ats_score: score } }];\n"
      },
      "id": "ddeeff00-1122-3344-5566-778899001122",
      "name": "Parse & Validate Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "allIncomingItems",
        "options": {}
      },
      "id": "eeff0011-2233-4455-6677-889900112233",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1180,
        300
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Build Tailoring Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Tailoring Prompt": {
      "main": [
        [
          {
            "node": "Gemini API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini API": {
      "main": [
        [
          {
            "node": "Parse & Validate Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Validate Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [],
  "id": 3,
  "active": true
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

AI Tailoring Engine. Uses httpRequest. Webhook trigger; 5 nodes.

Source: https://github.com/1Mangesh1/resume-gen/blob/main/workflows/ai-tailoring-engine.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.

HTTP Request
Web Scraping

This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia

HTTP Request
Web Scraping

This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c

HTTP Request