{
  "id": "yHmIg0ykCCKnbNLB",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Analyze resume PDF for ATS fit and score using OpenRouter and webhook",
  "tags": [],
  "nodes": [
    {
      "id": "f4927711-bcbc-457e-8497-f291580e4a70",
      "name": "Receive Resume PDF",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -32,
        48
      ],
      "parameters": {
        "path": "resume-analyzer",
        "options": {
          "binaryPropertyName": "resume_file"
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "b0703cb3-d3d5-4bd4-bc07-e8e954e14f7e",
      "name": "Extract Text from PDF",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        288,
        48
      ],
      "parameters": {
        "options": {},
        "operation": "pdf",
        "binaryPropertyName": "resume_file0"
      },
      "typeVersion": 1.1
    },
    {
      "id": "75af61fa-52a7-4110-87f6-2510da96f9dc",
      "name": "Analyze Resume with AI",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        544,
        48
      ],
      "parameters": {
        "text": "=Analyze this resume for the role:\n{{ $('Receive Resume PDF').item.json.body.job_role || 'General' }}\n\nResume Text:\n{{ $json.text }}",
        "options": {
          "systemMessage": "You are an expert ATS resume analyzer.\n\nAnalyze the resume carefully and return ONLY valid JSON.\n\nFormat:\n{\n  \"ats_score\": number,\n  \"best_suited_role\": \"string\",\n  \"missing_skills\": [\"skill1\", \"skill2\"],\n  \"improvement_suggestions\": [\"suggestion1\", \"suggestion2\"],\n  \"summary\": \"short summary\"\n}\n\nRules:\n- Return only JSON\n- No markdown\n- No explanations\n- ats_score must be integer"
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "ddba6329-e401-4dcc-a7e2-40fc83a68b9b",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        544,
        320
      ],
      "parameters": {
        "model": "openai/gpt-4o-mini",
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d5f341b3-d266-432d-9475-981d4c438f02",
      "name": "Parse & Validate AI Response",
      "type": "n8n-nodes-base.code",
      "position": [
        960,
        48
      ],
      "parameters": {
        "jsCode": "const rawInput = $input.item.json.output;\n\n// Strip markdown code fences (```json ... ```)\nconst cleaned = rawInput\n  .replace(/^```json\\s*/i, '')\n  .replace(/^```\\s*/i, '')\n  .replace(/```\\s*$/i, '')\n  .trim();\n\ntry {\n  const parsed = JSON.parse(cleaned);\n\n  return [{\n    json: {\n      ats_score: typeof parsed.ats_score === 'number' \n        ? parsed.ats_score \n        : parseInt(parsed.ats_score) || null,\n      best_suited_role: parsed.best_suited_role || \"\",\n      missing_skills: Array.isArray(parsed.missing_skills) \n        ? parsed.missing_skills \n        : [],\n      improvement_suggestions: Array.isArray(parsed.improvement_suggestions) \n        ? parsed.improvement_suggestions \n        : [],\n      summary: parsed.summary || \"\"\n    }\n  }];\n\n} catch (e) {\n  return [{\n    json: {\n      ats_score: null,\n      best_suited_role: \"\",\n      missing_skills: [],\n      improvement_suggestions: [],\n      summary: \"\",\n      error: \"Failed to parse AI response\",\n      raw: rawInput\n    }\n  }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "8346032f-1afe-4e22-b8eb-3addf21da773",
      "name": "Return ATS Report",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1296,
        48
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "99a36856-bd07-42b0-80ae-b2f84d286c47",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        -80
      ],
      "parameters": {
        "height": 112,
        "content": "Step 1 \u2013 Receive Resume\nWebhook accepts a multipart POST with a PDF file and optional job_role field."
      },
      "typeVersion": 1
    },
    {
      "id": "5707e820-7f7d-4ee0-a281-333a06e522db",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        224,
        -80
      ],
      "parameters": {
        "width": 208,
        "height": 112,
        "content": "Step 2 \u2013 Extract Text\nPulls plain text out of the uploaded PDF for AI processing."
      },
      "typeVersion": 1
    },
    {
      "id": "9d1b3028-6c7a-4aec-8d72-301ecc7cdfae",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        912,
        -96
      ],
      "parameters": {
        "height": 128,
        "content": "Step 4 \u2013 Parse & Validate\nJavaScript cleans any markdown formatting from the AI output and safely \nparses the JSON, with error handling fallback."
      },
      "typeVersion": 1
    },
    {
      "id": "379c86ed-01f3-4a62-b2c0-ac78d766416c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -96
      ],
      "parameters": {
        "width": 224,
        "height": 128,
        "content": "Step 3 \u2013 AI Analysis\nOpenRouter GPT analyzes the resume against the target role and returns \nstructured feedback as JSON."
      },
      "typeVersion": 1
    },
    {
      "id": "84f0da7e-1a78-47d9-a8d5-ed1dc0505b1c",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1248,
        -80
      ],
      "parameters": {
        "height": 96,
        "content": "Step 5 \u2013 Respond\nReturns the final structured ATS report to the caller."
      },
      "typeVersion": 1
    },
    {
      "id": "0b257e86-293f-4008-b37b-2c766f49cf6e",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        -400
      ],
      "parameters": {
        "width": 608,
        "height": 336,
        "content": "# \ud83d\udcc4 Resume ATS Analyzer\n\nSend a PDF resume to this workflow via webhook and get back an instant \nATS compatibility report powered by OpenRouter GPT.\n\nPOST to: /resume-analyzer\nFields: resume_file (PDF), job_role (optional text)\n\nReturns JSON:\n- ats_score (0\u2013100)\n- best_suited_role\n- missing_skills\n- improvement_suggestions\n- summary\n\nSetup: Add your OpenRouter API key to the OpenRouter Chat Model node credential."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "bc023a7c-bddb-4905-8a69-57a2048b295e",
  "connections": {
    "Receive Resume PDF": {
      "main": [
        [
          {
            "node": "Extract Text from PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Text from PDF": {
      "main": [
        [
          {
            "node": "Analyze Resume with AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze Resume with AI",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Resume with AI": {
      "main": [
        [
          {
            "node": "Parse & Validate AI Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Validate AI Response": {
      "main": [
        [
          {
            "node": "Return ATS Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}