{
  "name": "CV AI Screener",
  "nodes": [
    {
      "parameters": {
        "content": "## 📋 CV AI Screener - Overview\n\n**Purpose:** Upload multiple CVs and a job description. ChatGPT extracts candidate bio data, scores each CV against the role, and returns a downloadable CSV.\n\n> ⚠️ **Educational purpose only.** Built with minimal integrations to demonstrate the core pattern. Not production-ready - see *Next Steps*.\n\n\n**How to use:**\n1. Activate the workflow and open the form URL from the *On form submission* trigger\n2. Paste the full **Job Description** into the textarea\n3. Upload one or more **CV files** (PDF format)\n4. Submit, a few seconds per CV\n5. Download **cv_screening_results.csv** from the completion page\n\n**CSV columns:** fileName · fullName · email · phone · currentRole · yearsOfExperience · topSkills · score (0–100) · scoreReason",
        "height": 516,
        "width": 440,
        "color": 2
      },
      "id": "03871da6-7d99-484e-a3f1-3f44d74c07a3",
      "name": "Sticky Note ff35e604",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -688,
        272
      ]
    },
    {
      "parameters": {
        "content": "## ⚠️ Compliance Warning\n\n**DO NOT use in a real hiring process without legal and ethical compliance review.**\n\n**EU AI Act:** CV scoring is a **high-risk AI system** (Annex III - employment). EU deployment requires a conformity assessment, mandatory human oversight, candidate transparency, and registration in the EU high-risk AI database.\n\n**GDPR:** CVs are personal data - a lawful basis for processing is required. Apply data minimisation. Candidates have rights to explanation and objection (Article 22).\n\n**General:** AI scores are probabilistic, not objective truth. Models can reproduce historical biases. Bias testing across gender, age, nationality and disability is strongly recommended. Always involve HR professionals and legal counsel before live use.",
        "height": 340,
        "width": 520,
        "color": 5
      },
      "id": "9ab95a10-727f-4302-9303-c7a85db52da5",
      "name": "Sticky Note bc6393be",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2048,
        448
      ]
    },
    {
      "parameters": {
        "content": "## 🚀 Next Steps & Production Extensions\n\n**🔁 Trigger:** Replace the Form with a **Webhook** to receive CVs from your careers portal, or an **Email Trigger** to auto-process applications from a recruitment inbox.\n\n**📄 Better Extraction:** Swap **Extract from File** for **Mistral AI's OCR API** for much higher quality on scanned CVs and complex layouts.\n\n**🤖 Smarter AI:** Extend the schema to capture education, certifications, languages, and gaps. Add a second AI pass to generate **personalised interview questions** per candidate.\n\n**📊 Integrations:** Push results directly to your **ATS (Application Tracking System)** instead of a CSV. Write to **Google Sheets** or **Notion** for collaborative review. Send a **Slack/Teams** alert with top-ranked candidates. Store in a **vector DB** for talent pool search.\n\n**🔒 Security:** Add **SSO/magic link auth** to the form. Implement an **audit log** recording every run for regulatory traceability.",
        "height": 420,
        "width": 520,
        "color": 4
      },
      "id": "0ecf4db4-e0d4-4675-a67f-0395e95fa020",
      "name": "Sticky Note 187ab75b",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1504,
        368
      ]
    },
    {
      "parameters": {
        "content": "**1️⃣ Form Input**\nCollects a **Job Description** textarea and **multi-file CV upload**. `responseMode: lastNode` keeps the session open until the final node returns the CSV.",
        "height": 120,
        "width": 292,
        "color": 3
      },
      "id": "b626e85a-2461-490e-a75d-2eab2015e23d",
      "name": "Sticky Note 03aad570",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -176,
        672
      ]
    },
    {
      "parameters": {
        "content": "**2️⃣ Normalize Files**\nFlattens n8n's multi-file binary keys (`CVs_0`, `CVs_1`...) into **one item per file** and copies the job description onto each item.",
        "height": 120,
        "width": 260,
        "color": 3
      },
      "id": "2828b54d-b568-4e0d-9769-898c4f2cb1b1",
      "name": "Sticky Note 2ac84961",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        128,
        672
      ]
    },
    {
      "parameters": {
        "content": "**3️⃣ Loop + PDF Extraction**\nProcesses one CV at a time (batch size 1). **Extract PDF Text** reads all pages into a single string.\n💡 *Consider Mistral AI OCR for complex PDFs.*",
        "height": 120,
        "width": 360,
        "color": 3
      },
      "id": "c7582e1c-6f79-4239-a672-040f3c2d4c29",
      "name": "Sticky Note 362a6835",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        400,
        672
      ]
    },
    {
      "parameters": {
        "content": "**4️⃣ AI Analysis**\n**CV Analyser** sends CV text + job description to ChatGPT, **CV Schema** enforces a typed JSON schema; name, email, phone, role, experience, skills, score, reason.",
        "height": 120,
        "width": 396,
        "color": 3
      },
      "id": "94faaea3-b39c-4b3c-a557-e78be0df58b7",
      "name": "Sticky Note 3ceb5464",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        768,
        672
      ]
    },
    {
      "parameters": {
        "content": "**5️⃣ Flatten Output**\nUnwraps the `output.*` wrapper added by the structured parser and joins `topSkills[]` into a comma-separated string for clean CSV rows.",
        "height": 120,
        "width": 276,
        "color": 3
      },
      "id": "7211bfbe-28fd-41ff-8823-878012f4ad3b",
      "name": "Sticky Note bdb93d97",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1184,
        672
      ]
    },
    {
      "parameters": {
        "content": "**6️⃣ Export & Return**\n**Convert to CSV** turns all loop items into `cv_screening_results.csv`. **Return CSV Download** delivers it via the form completion page.\n💡 *In production, push to your ATS instead.*",
        "height": 120,
        "width": 520,
        "color": 3
      },
      "id": "6395fe56-5cca-4013-97f2-5c9b9eb032f4",
      "name": "Sticky Note deb325ac",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        656,
        160
      ]
    },
    {
      "parameters": {
        "formTitle": "CV Screening",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Job Description",
              "fieldType": "textarea",
              "requiredField": true
            },
            {
              "fieldLabel": "CVs",
              "fieldType": "file",
              "requiredField": true
            }
          ]
        },
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "8ffaee95-301c-4552-be91-3a3283a25092",
      "name": "On form submission",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.5,
      "position": [
        0,
        304
      ],
      "webhookId": "9a415ca8-ba71-4887-b7ba-3432a8e0ed7c"
    },
    {
      "parameters": {
        "jsCode": "const output = [];\nconst jobDescription = $input.first().json['Job Description'] ?? '';\nfor (const item of $input.all()) {\n  for (const [key, binaryFile] of Object.entries(item.binary ?? {})) {\n    output.push({\n      json: { fileName: binaryFile.fileName ?? key, mimeType: binaryFile.mimeType, jobDescription },\n      binary: { cv: binaryFile },\n    });\n  }\n}\nreturn output;"
      },
      "id": "610b7449-82c9-4def-8f39-db2edc300986",
      "name": "Normalize Files",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        224,
        304
      ]
    },
    {
      "parameters": {
        "options": {
          "fileName": "cv_screening_results.csv",
          "headerRow": true
        }
      },
      "id": "32d8fd1b-9d58-4918-9d9c-7c67274541e4",
      "name": "Convert to CSV",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        672,
        0
      ]
    },
    {
      "parameters": {
        "operation": "completion",
        "respondWith": "returnBinary",
        "completionTitle": "Screening complete!",
        "completionMessage": "Your CV screening results are ready. Download the CSV below.",
        "options": {}
      },
      "id": "41a2e372-cc85-4ae8-b12e-fde2097f3097",
      "name": "Return CSV Download",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        960,
        0
      ],
      "webhookId": "bf46f11a-2e94-4768-af30-b34005f34bf9"
    },
    {
      "parameters": {
        "operation": "pdf",
        "binaryPropertyName": "cv",
        "options": {
          "joinPages": true
        }
      },
      "id": "9bd32f0d-80fb-41f8-a55a-6c3c1cc86ffd",
      "name": "Extract PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        672,
        304
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Analyse the following CV against the job description.\n\nJob Description:\n{{ $(\"Loop Over CVs\").item.json.jobDescription }}\n\nCV File Name:\n{{ $(\"Loop Over CVs\").item.json.fileName }}\n\nCV Text:\n{{ $json.text }}\n\nExtract the candidate bio data and score the CV against the job description.",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are an expert HR recruiter and CV analyst. Extract structured bio data from CVs and score candidates against job descriptions. The fileName field must be copied exactly from the CV File Name provided in the prompt."
        }
      },
      "id": "29639b03-e1e1-4a8c-94bc-8f057dfdc4a9",
      "name": "CV Analyser",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        896,
        304
      ]
    },
    {
      "parameters": {
        "jsonSchemaExample": "{ \"fileName\": \"resume.pdf\", \"fullName\": \"John Doe\", \"email\": \"john@example.com\", \"phone\": \"+1234567890\", \"currentRole\": \"Software Engineer\", \"yearsOfExperience\": 5, \"topSkills\": [\"React\", \"TypeScript\", \"Node.js\"], \"score\": 82, \"scoreReason\": \"Strong React background. Missing leadership experience.\" }"
      },
      "id": "ff1b8c91-ef36-43b2-ad08-26411e1e3c7c",
      "name": "CV Schema",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1040,
        528
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "1",
              "name": "fileName",
              "value": "={{ $json.output.fileName }}",
              "type": "string"
            },
            {
              "id": "2",
              "name": "fullName",
              "value": "={{ $json.output.fullName }}",
              "type": "string"
            },
            {
              "id": "3",
              "name": "email",
              "value": "={{ $json.output.email }}",
              "type": "string"
            },
            {
              "id": "4",
              "name": "phone",
              "value": "={{ $json.output.phone }}",
              "type": "string"
            },
            {
              "id": "5",
              "name": "currentRole",
              "value": "={{ $json.output.currentRole }}",
              "type": "string"
            },
            {
              "id": "6",
              "name": "yearsOfExperience",
              "value": "={{ $json.output.yearsOfExperience }}",
              "type": "number"
            },
            {
              "id": "7",
              "name": "topSkills",
              "value": "={{ $json.output.topSkills.join(\", \") }}",
              "type": "string"
            },
            {
              "id": "8",
              "name": "score",
              "value": "={{ $json.output.score }}",
              "type": "number"
            },
            {
              "id": "9",
              "name": "scoreReason",
              "value": "={{ $json.output.scoreReason }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "dd942a4f-1906-465f-b10d-8c86dd71c93c",
      "name": "Flatten Output",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1248,
        416
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "6179b183-6d16-4878-8a5c-e79a28a0e915",
      "name": "Loop Over CVs",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        448,
        304
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini"
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        896,
        528
      ],
      "id": "81e47a3f-aa12-454c-8fd1-866186513a92",
      "name": "OpenAI Chat Model",
      "credentials": {
        "openAiApi": {
          "id": "eF2vCCF9GqRUBBR7",
          "name": "OpenAI account"
        }
      }
    }
  ],
  "pinData": {},
  "connections": {
    "On form submission": {
      "main": [
        [
          {
            "node": "Normalize Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Files": {
      "main": [
        [
          {
            "node": "Loop Over CVs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to CSV": {
      "main": [
        [
          {
            "node": "Return CSV Download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF Text": {
      "main": [
        [
          {
            "node": "CV Analyser",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CV Analyser": {
      "main": [
        [
          {
            "node": "Flatten Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CV Schema": {
      "ai_outputParser": [
        [
          {
            "node": "CV Analyser",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Flatten Output": {
      "main": [
        [
          {
            "node": "Loop Over CVs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over CVs": {
      "main": [
        [
          {
            "node": "Convert to CSV",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract PDF Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "CV Analyser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": true
  },
  "versionId": "3b5d1788-54bd-49a4-8e57-4019bea59360",
  "meta": {
    "aiBuilderAssisted": true,
    "builderVariant": "mcp",
    "templateCredsSetupCompleted": true,
    "instanceId": "61d1ceb8b60d551b2bd6cb318774870200f11403fbfe4e124bc9e07f08b20486"
  },
  "id": "PxQGnKF1ev8iqPRT",
  "tags": [
    {
      "updatedAt": "2026-05-03T08:51:16.018Z",
      "createdAt": "2026-05-03T08:51:16.018Z",
      "id": "FUFolilclluqXrmj",
      "name": "Indeed Hackathon"
    }
  ]
}