AutomationFlowsAI & RAG › Score and Triage Indeed Cvs with Mistral Ocr, Groq, and Google Sheets

Score and Triage Indeed Cvs with Mistral Ocr, Groq, and Google Sheets

ByFederico @federik500 on n8n.io

This template is for HR teams, recruitment agencies, and startups that receive job applications via Indeed and want to eliminate manual CV screening. If you're spending hours reading CVs before deciding who to call, this workflow automates that first-pass evaluation entirely.

Manual trigger★★★★★ complexityAI-powered30 nodesEmail Read ImapChain LlmOutput Parser StructuredEmail SendGoogle DriveHTTP RequestGroq ChatGoogle Sheets
AI & RAG Trigger: Manual Nodes: 30 Complexity: ★★★★★ AI nodes: yes Added:
Score and Triage Indeed Cvs with Mistral Ocr, Groq, and Google Sheets — n8n workflow card showing Email Read Imap, Chain Llm, Output Parser Structured integration

This workflow corresponds to n8n.io template #15854 — we link there as the canonical source.

This workflow follows the Chainllm → Emailsend recipe pattern — see all workflows that pair these two integrations.

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
{
  "id": "x1D6kcIROn2c236i",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "CV Rating Automation - Indeed",
  "tags": [],
  "nodes": [
    {
      "id": "ff78627a-9d2e-4439-8f6a-00b488f7cff5",
      "name": "\ud83d\udccb Overview Workflow",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        640
      ],
      "parameters": {
        "color": 3,
        "width": 380,
        "height": 756,
        "content": "## \ud83c\udfaf CV Rating Automation - Production\n\n**Full automated candidate screening pipeline for Indeed applications.**\n\n### Pipeline\n1. IMAP receives Indeed application email\n2. Metadata extraction (job position, candidate name)\n3. CV upload to Google Drive (folder per position)\n4. OCR with Mistral AI (base64 inline)\n5. Text cleaning & normalization\n6. LLM analysis with Groq + Llama 3.3 70B + Structured Output Parser\n7. Score \u2194 recommendation consistency validation\n8. Routing by score:\n   - \u226575 \u2192 detailed email to recruiter\n   - always \u2192 append to Google Sheets\n9. Error branch \u2192 ops notification\n\n### Required Stack\n- n8n + PostgreSQL + Redis (Docker)\n- Queue mode with workers for parallelism\n\n### Required Credentials\n- IMAP (Indeed mailbox)\n- Google Drive OAuth2\n- Google Sheets OAuth2\n- Mistral AI API\n- Groq API\n- SMTP (for notifications)\n\n### Estimated Cost\n~\u20ac0.005 per processed CV (OCR + LLM)"
      },
      "typeVersion": 1
    },
    {
      "id": "6a569f42-5944-4096-88b8-8a2cb1011bf7",
      "name": "\u2699\ufe0f Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        976,
        1008
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "config-drive-folder",
              "name": "DRIVE_ROOT_FOLDER_ID",
              "type": "string",
              "value": "YOUR_GOOGLE_DRIVE_FOLDER_ID"
            },
            {
              "id": "config-sheet-id",
              "name": "SHEET_ID",
              "type": "string",
              "value": "YOUR_GOOGLE_SHEET_ID"
            },
            {
              "id": "config-recruiter-email",
              "name": "RECRUITER_EMAIL",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "config-ops-email",
              "name": "OPS_EMAIL",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "config-sender-email",
              "name": "SENDER_EMAIL",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "config-score-threshold",
              "name": "SCORE_THRESHOLD",
              "type": "number",
              "value": 75
            },
            {
              "id": "config-imap-subject-filter",
              "name": "IMAP_SUBJECT_FILTER",
              "type": "string",
              "value": "New application for"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "4864c888-d142-4baf-8454-74192720eb6b",
      "name": "\ud83d\udce5 Note IMAP",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        576
      ],
      "parameters": {
        "color": 5,
        "width": 336,
        "height": 616,
        "content": "## \ud83d\udce5 IMAP Trigger\n\n**Why IMAP instead of a webhook?**\nIndeed does not offer public APIs to receive applications in real time. A dedicated mailbox is the most reliable solution.\n\n### Recommended Setup\n- Dedicated mailbox (e.g. `applications@company.com`)\n- Filter `subjectContains: application`\n- Polling every 1 minute\n- `downloadAttachments: true`\n\n### Alternatives\n- Indeed Apply API (enterprise employers)\n- ATS bridge (Greenhouse, Workable)\n- Dedicated email parser (Mailparser)"
      },
      "typeVersion": 1
    },
    {
      "id": "6a6877b9-593b-4792-86e4-15cfb92b8590",
      "name": "\ud83d\udd0d Note Extract",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        640
      ],
      "parameters": {
        "color": 5,
        "width": 336,
        "height": 712,
        "content": "## \ud83d\udd0d Metadata Extraction\n\nThe Code node extracts:\n- **Job position** (regex on Indeed subject line)\n- **Candidate name** (trailing part of the subject)\n- **Valid attachments** (PDF/DOCX only)\n\n\u26a0\ufe0f **Adapt the regex** to match the email format you receive: the Indeed template varies by language. Test with real emails.\n\n\ud83d\udca1 Emails without valid attachments are **silently dropped**."
      },
      "typeVersion": 1
    },
    {
      "id": "a14f0f3d-3631-4907-bacf-6d967bf60a8a",
      "name": "\ud83d\udcc1 Note Drive",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1504,
        512
      ],
      "parameters": {
        "color": 5,
        "width": 336,
        "height": 752,
        "content": "## \ud83d\udcc1 Google Drive Organization\n\nCVs are saved in **per-position folders**:\n```\nCV_Senior_Backend_Developer/\nCV_Frontend_React/\n```\n\n\u26a0\ufe0f **Replace `YOUR_FOLDER_ID`** with the ID of your root folder on Drive (found in the folder URL).\n\n### Benefits\n- Organized historical archive\n- Easy sharing with HR team\n- Direct link in Google Sheets\n- GDPR compliance (easy bulk deletion)"
      },
      "typeVersion": 1
    },
    {
      "id": "1b2f75bb-21e3-4921-958b-39822f34ef56",
      "name": "\ud83e\udde0 Note OCR",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1872,
        432
      ],
      "parameters": {
        "color": 4,
        "width": 368,
        "height": 820,
        "content": "## \ud83e\udde0 OCR with Mistral\n\nUses **`mistral-ocr-latest`**, optimized for document processing.\n\n### Strategy: inline base64\nThe PDF is sent to Mistral **as base64 in the request body** instead of via a Drive URL. Benefits:\n- \u2705 No dependency on public Drive permissions\n- \u2705 More secure (no public files)\n- \u2705 Faster (zero round-trip)\n- \u2705 50MB limit is more than enough for CVs\n\n### Cost\n~\u20ac0.001 per page, ~\u20ac0.003 per average CV.\n\n### Optional Optimization\nFor simple text-only CVs (no graphics), add a branch with `Extract from File` (free) before Mistral."
      },
      "typeVersion": 1
    },
    {
      "id": "8fba51de-d1e8-472e-8426-e7900d5fb1b0",
      "name": "\u2728 Note AI Chain",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        544
      ],
      "parameters": {
        "color": 4,
        "width": 452,
        "height": 860,
        "content": "## \u2728 Core AI Flow\n\nCluster of 3 LangChain nodes:\n```\nCV Analyzer Chain (root)\n    \u251c\u2500 Groq Chat Model       \u2190 ai_languageModel\n    \u2514\u2500 Output Parser         \u2190 ai_outputParser\n```\n\n### Benefits\n- **Groq**: ultra-fast inference (10x faster than OpenAI)\n- **Structured Output Parser**: injects JSON Schema into the prompt and auto-retries on non-compliant output\n- **Always valid JSON output** according to schema\n\n\ud83d\udca1 To switch models (`gpt-oss-120b`, `mixtral-8x7b`), simply change the dropdown in the Groq node."
      },
      "typeVersion": 1
    },
    {
      "id": "b95b4eaa-0794-4cad-9fe1-73536adcd90e",
      "name": "\ud83d\udcd0 Note Schema",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2752,
        496
      ],
      "parameters": {
        "color": 4,
        "width": 320,
        "height": 668,
        "content": "## \ud83d\udcd0 Output Schema\n\nThe JSON Schema defines 16 typed fields:\n\n**Personal Info**: first_name, last_name, email, phone\n\n**Profile**: years_experience, education, languages, key_skills\n\n**Evaluation**: short_summary (max 200 chars), detailed_summary (300-500 words), strengths, weaknesses\n\n**Decision**:\n- **score** (0-100)\n- **recommendation** (REJECT / REVIEW / INTERVIEW / STRONG_HIRE)\n\n### Scoring Grid\n| Score | Decision |\n|-------|-----------|\n| 0-30 | REJECT |\n| 31-60 | REVIEW |\n| 61-80 | INTERVIEW |\n| 81-100 | STRONG_HIRE |"
      },
      "typeVersion": 1
    },
    {
      "id": "21753e52-a600-4cc8-9aa8-e442d1ea1d86",
      "name": "\u2705 Note Validate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3104,
        528
      ],
      "parameters": {
        "color": 4,
        "width": 320,
        "height": 664,
        "content": "## \u2705 Validation & Consistency Check\n\nEven though the parser guarantees valid JSON, this node adds a **safety net**:\n\n1. Double-parse if output arrives as a string\n2. Required fields verification\n3. Score clamped to [0, 100]\n4. **Forces consistency** between score \u2194 recommendation (auto-corrects if mismatched, saves original for audit)\n5. Enrichment with Drive metadata, timestamp, fileName\n6. Builds formatted HTML email for recruiter\n\n\ud83d\udca1 Fault-tolerant: errors return `error: true` instead of breaking the flow."
      },
      "typeVersion": 1
    },
    {
      "id": "fcab60e1-b45a-4020-8b30-3fe80e62e393",
      "name": "\ud83d\udcca Note Sheets",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3392,
        1280
      ],
      "parameters": {
        "color": 2,
        "width": 320,
        "height": 440,
        "content": "## \ud83d\udcca Google Sheets Dashboard\n\nEvery candidate \u2192 one row in the HR sheet.\n\n### Columns\nDate \u00b7 First Name \u00b7 Last Name \u00b7 Email \u00b7 Phone \u00b7 Position \u00b7 Years Exp. \u00b7 Skills \u00b7 **Score** \u00b7 **Recommendation** \u00b7 Summary \u00b7 CV Link\n\n### Tips\n- Conditional formatting on Score column (red/yellow/green)\n- Filter by position\n- Pivot table on Recommendation\n- **Replace `YOUR_SHEET_ID`** with your real Sheet ID\n\n### Alternatives\nNotion DB, Airtable, Baserow, direct PostgreSQL."
      },
      "typeVersion": 1
    },
    {
      "id": "1c69aa3a-2daa-4dcc-bb32-a7253312a190",
      "name": "\ud83c\udfaf Note Notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3952,
        448
      ],
      "parameters": {
        "color": 2,
        "width": 320,
        "height": 708,
        "content": "## \ud83c\udfaf Top Candidate Notification\n\nConfigurable threshold (default: **score \u2265 75**).\n\n### Formatted HTML Email\n- Color-coded header by score (green/blue/yellow/red)\n- KPI cards (score, recommendation, years of experience)\n- Short summary highlighted\n- Candidate profile in table format\n- Skills as badges\n- Strengths/weaknesses in two columns\n- Detailed analysis section\n- Footer with CV link and processing time\n\n### Suggested Extensions\n- Slack/Telegram with action buttons\n- Automatic task creation in Asana/Trello\n- Branch score < 30 \u2192 polite rejection email (with 24-48h delay)"
      },
      "typeVersion": 1
    },
    {
      "id": "ebb452bb-16a6-457a-80d4-7e8624914c23",
      "name": "\u26a0\ufe0f Note Errori",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3488,
        432
      ],
      "parameters": {
        "color": 6,
        "width": 320,
        "height": 616,
        "content": "## \u26a0\ufe0f Error Handling\n\nEvery failed execution \u2192 automatic email to the Ops team.\n\n### Most Common Causes\n1. CV too long \u2192 Mistral OCR timeout\n2. CV in unsupported language\n3. Corrupted or password-protected PDF\n4. Groq rate limit (free tier: 30 RPM)\n5. JSON schema not respected (rare)\n\n### Production Best Practices\n- Set a global **Error Workflow** in Settings\n- **Retry with backoff** on critical HTTP nodes\n- Log errors to Postgres for analysis\n- Redis dead letter queue for unprocessable CVs"
      },
      "typeVersion": 1
    },
    {
      "id": "dcf6f24d-0187-4ae4-a050-10710173ef88",
      "name": "\ud83d\udd12 Note GDPR",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        1232
      ],
      "parameters": {
        "color": 6,
        "width": 340,
        "height": 468,
        "content": "## \ud83d\udd12 GDPR & Security\n\nCVs contain **sensitive personal data**.\n\n### Minimum Compliance\n- \u2705 Legal basis documented (Art. 6 GDPR)\n- \u2705 Privacy policy linked in the job posting\n- \u2705 Retention policy: delete rejected CVs after 6-12 months\n- \u2705 Encryption at rest (Docker volumes on encrypted filesystem)\n- \u2705 Access audit log\n- \u2705 Right to erasure (separate workflow)\n\n### Recommended\nA separate scheduled workflow (monthly):\n1. Identify CVs older than X months\n2. Delete files from Drive\n3. Anonymize Sheets row (keep aggregate stats)\n4. Notify DPO"
      },
      "typeVersion": 1
    },
    {
      "id": "2430890c-ac15-4f7c-b8cc-f5bf75848c1a",
      "name": "IMAP Email Trigger (Indeed)",
      "type": "n8n-nodes-base.emailReadImap",
      "position": [
        816,
        1008
      ],
      "parameters": {
        "options": {
          "customEmailConfig": "[\"UNSEEN\"]"
        },
        "postProcessAction": "nothing",
        "downloadAttachments": true
      },
      "notesInFlow": false,
      "retryOnFail": true,
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "4a59236b-8d34-490c-8253-356db1adcd4f",
      "name": "Extract Email Metadata",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        1008
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// EXTRACT EMAIL METADATA FROM INDEED\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst items = $input.all();\nconst output = [];\n\nfor (const item of items) {\n  const subject = item.json.subject || '';\n  const from = item.json.from || '';\n  const date = item.json.date || new Date().toISOString();\n  \n  // Regex on Indeed subject: \"New application for X - Y\"\n  // \u26a0\ufe0f Adapt this regex to match your Indeed email language/format\n  const positionMatch = subject.match(/per\\s+(.+?)\\s+-/i);\n  const candidateMatch = subject.match(/-\\s*(.+)$/);\n  \n  const position = positionMatch ? positionMatch[1].trim() : 'Unknown position';\n  const candidateName = candidateMatch ? candidateMatch[1].trim() : 'Unknown candidate';\n  \n  // Filter valid attachments (PDF/DOCX only)\n  const attachments = item.binary || {};\n  const attachmentKeys = Object.keys(attachments).filter(k => {\n    const mime = attachments[k].mimeType || '';\n    return mime.includes('pdf') || mime.includes('word') || mime.includes('document');\n  });\n  \n  if (attachmentKeys.length === 0) {\n    continue; // Skip emails without a CV attachment\n  }\n  \n  for (const key of attachmentKeys) {\n    const executionStart = Date.now();\n    output.push({\n      json: {\n        _meta: { executionStart },\n        subject,\n        from,\n        date,\n        position,\n        candidateName,\n        attachmentKey: key,\n        fileName: attachments[key].fileName || `cv_${Date.now()}.pdf`,\n        mimeType: attachments[key].mimeType\n      },\n      binary: {\n        cv: attachments[key]\n      }\n    });\n  }\n}\n\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "34acf446-4393-46e4-9784-f0ed59e3c725",
      "name": "\u2728 CV Analyzer Chain",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2368,
        1008
      ],
      "parameters": {
        "text": "=JOB POSITION APPLIED FOR: {{ $json.position }}\nAPPLICATION DATE: {{ $json.applicationDate }}\nCANDIDATE NAME: {{ $json.candidateName }}\n\nCV TEXT:\n{{ $json.cvText }}\n\nAnalyze the CV and return a structured JSON according to the provided schema.",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "You are a senior recruiter specializing in CV evaluation. Your task is to analyze the provided CV and produce a structured, objective, and detailed assessment.\n\nScoring rules:\n- 0-30: profile not aligned, missing key experience/skills\n- 31-60: partially aligned profile, needs review\n- 61-80: good profile, candidate for interview\n- 81-100: excellent profile, strong hire\n\nThe recommendation must be consistent with the score:\n- REJECT (0-30), REVIEW (31-60), INTERVIEW (61-80), STRONG_HIRE (81-100)\n\nBe rigorous but fair. Always explain the reasoning behind the score."
            }
          ]
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "dca5260c-583a-4f24-ac9c-8e0115ce5a7e",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        2576,
        1248
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"nome\": { \"type\": \"string\", \"description\": \"Nome del candidato\" },\n    \"cognome\": { \"type\": \"string\", \"description\": \"Cognome del candidato\" },\n    \"email\": { \"type\": \"string\", \"description\": \"Email del candidato\" },\n    \"telefono\": { \"type\": \"string\", \"description\": \"Numero di telefono\" },\n    \"data_candidatura\": { \"type\": \"string\", \"description\": \"Data candidatura YYYY-MM-DD\" },\n    \"posizione\": { \"type\": \"string\", \"description\": \"Posizione per cui si candida\" },\n    \"anni_esperienza\": { \"type\": \"number\", \"description\": \"Anni di esperienza totale\" },\n    \"competenze_chiave\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"Lista delle competenze tecniche principali\"\n    },\n    \"lingue\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"Lingue parlate con livello\"\n    },\n    \"titolo_studio\": { \"type\": \"string\", \"description\": \"Titolo di studio piu alto\" },\n    \"spiegazione_breve\": {\n      \"type\": \"string\",\n      \"description\": \"Sintesi del profilo in massimo 200 caratteri\"\n    },\n    \"spiegazione_dettagliata\": {\n      \"type\": \"string\",\n      \"description\": \"Analisi dettagliata 300-500 parole con punti di forza, gap, fit con la posizione\"\n    },\n    \"punti_forza\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"Lista dei principali punti di forza\"\n    },\n    \"punti_debolezza\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"description\": \"Lista delle aree di miglioramento o gap rispetto alla posizione\"\n    },\n    \"punteggio\": {\n      \"type\": \"number\",\n      \"minimum\": 0,\n      \"maximum\": 100,\n      \"description\": \"Punteggio di fit con la posizione da 0 a 100\"\n    },\n    \"raccomandazione\": {\n      \"type\": \"string\",\n      \"enum\": [\"REJECT\", \"REVIEW\", \"INTERVIEW\", \"STRONG_HIRE\"],\n      \"description\": \"Raccomandazione finale\"\n    }\n  },\n  \"required\": [\n    \"nome\",\n    \"cognome\",\n    \"posizione\",\n    \"spiegazione_breve\",\n    \"spiegazione_dettagliata\",\n    \"punteggio\",\n    \"raccomandazione\"\n  ]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "bb290e42-ed39-4ff0-8bf4-5c4c97469b1f",
      "name": "\u2705 Validate & Build Email",
      "type": "n8n-nodes-base.code",
      "position": [
        2848,
        1008
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// VALIDATE & ENRICH + BUILD EMAIL HTML\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst items = $input.all();\nconst output = [];\n\nfor (const item of items) {\n  let parsed = item.json.output || item.json;\n  \n  // Safety net: handle string responses\n  if (typeof parsed === 'string') {\n    try {\n      parsed = JSON.parse(parsed.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim());\n    } catch (err) {\n      output.push({\n        json: {\n          error: true,\n          errorMessage: 'Parse error: ' + err.message,\n          rawResponse: item.json\n        }\n      });\n      continue;\n    }\n  }\n  \n  // Validate required fields\n  const requiredFields = ['nome', 'cognome', 'posizione', 'punteggio', 'raccomandazione'];\n  const missing = requiredFields.filter(f => !(f in parsed));\n  if (missing.length > 0) {\n    output.push({\n      json: {\n        error: true,\n        errorMessage: 'Missing fields: ' + missing.join(', '),\n        partialData: parsed\n      }\n    });\n    continue;\n  }\n  \n  // Retrieve upstream context\n  const ctx = $node['\ud83e\uddf9 Clean Text'].json;\n  const meta = ctx._meta || {};\n  \n  // Clamp score to [0, 100]\n  parsed.punteggio = Math.max(0, Math.min(100, Number(parsed.punteggio) || 0));\n  const score = parsed.punteggio;\n  \n  // Force score <-> recommendation consistency\n  const expectedRec = score <= 30 ? 'REJECT'\n    : score <= 60 ? 'REVIEW'\n    : score <= 80 ? 'INTERVIEW'\n    : 'STRONG_HIRE';\n  const recCorrected = parsed.raccomandazione !== expectedRec;\n  if (recCorrected) {\n    parsed.raccomandazione_originale = parsed.raccomandazione;\n    parsed.raccomandazione = expectedRec;\n  }\n  \n  // Execution time\n  const executionTime = meta.executionStart ? ((Date.now() - meta.executionStart) / 1000).toFixed(1) : 'N/A';\n  \n  // Score badge color\n  const scoreColor = score >= 81 ? '#22c55e'\n    : score >= 61 ? '#3b82f6'\n    : score >= 31 ? '#f59e0b'\n    : '#ef4444';\n  \n  // Build HTML email\n  const puntiForza = (parsed.punti_forza || []).map(p => `<li>${p}</li>`).join('');\n  const puntiDebolezza = (parsed.punti_debolezza || []).map(p => `<li>${p}</li>`).join('');\n  const competenze = (parsed.competenze_chiave || []).map(c => `<span style=\"display:inline-block;background:#e0e7ff;color:#3730a3;padding:3px 10px;border-radius:12px;margin:2px;font-size:12px\">${c}</span>`).join(' ');\n  const lingue = (parsed.lingue || []).join(' \u00b7 ');\n  \n  const emailHtml = `\n<div style=\"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;max-width:680px;margin:0 auto;color:#1f2937\">\n  <div style=\"background:linear-gradient(135deg,${scoreColor} 0%,${scoreColor}dd 100%);color:white;padding:24px;border-radius:12px 12px 0 0\">\n    <div style=\"font-size:11px;opacity:0.85;letter-spacing:1.5px;text-transform:uppercase\">\ud83c\udfaf New top candidate</div>\n    <div style=\"font-size:24px;font-weight:600;margin-top:8px\">${parsed.nome} ${parsed.cognome}</div>\n    <div style=\"opacity:0.9;margin-top:4px\">${parsed.posizione}</div>\n  </div>\n  \n  <div style=\"background:white;border:1px solid #e5e7eb;border-top:0;border-radius:0 0 12px 12px;padding:24px\">\n    \n    <div style=\"display:flex;gap:16px;margin-bottom:24px;flex-wrap:wrap\">\n      <div style=\"flex:1;min-width:140px;background:#f9fafb;padding:16px;border-radius:8px;text-align:center\">\n        <div style=\"font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:1px\">Score</div>\n        <div style=\"font-size:36px;font-weight:700;color:${scoreColor};margin-top:4px\">${score}<span style=\"font-size:18px;color:#9ca3af\">/100</span></div>\n      </div>\n      <div style=\"flex:1;min-width:140px;background:#f9fafb;padding:16px;border-radius:8px;text-align:center\">\n        <div style=\"font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:1px\">Recommendation</div>\n        <div style=\"font-size:18px;font-weight:600;color:${scoreColor};margin-top:8px\">${parsed.raccomandazione}</div>\n      </div>\n      <div style=\"flex:1;min-width:140px;background:#f9fafb;padding:16px;border-radius:8px;text-align:center\">\n        <div style=\"font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:1px\">Years of experience</div>\n        <div style=\"font-size:18px;font-weight:600;color:#374151;margin-top:8px\">${parsed.anni_esperienza || 'N/A'}</div>\n      </div>\n    </div>\n    \n    <div style=\"background:#f3f4f6;padding:14px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;color:#4b5563;border-left:3px solid ${scoreColor}\">\n      <strong style=\"color:#1f2937\">Summary:</strong> ${parsed.spiegazione_breve}\n    </div>\n    \n    <h3 style=\"font-size:14px;color:#374151;margin:20px 0 8px;text-transform:uppercase;letter-spacing:1px\">\ud83d\udccb Profile</h3>\n    <table style=\"width:100%;font-size:14px;color:#4b5563;border-collapse:collapse\">\n      <tr><td style=\"padding:6px 0;width:140px;color:#6b7280\">Email</td><td>${parsed.email || 'N/A'}</td></tr>\n      <tr><td style=\"padding:6px 0;color:#6b7280\">Phone</td><td>${parsed.telefono || 'N/A'}</td></tr>\n      <tr><td style=\"padding:6px 0;color:#6b7280\">Education</td><td>${parsed.titolo_studio || 'N/A'}</td></tr>\n      <tr><td style=\"padding:6px 0;color:#6b7280\">Languages</td><td>${lingue || 'N/A'}</td></tr>\n    </table>\n    \n    <h3 style=\"font-size:14px;color:#374151;margin:20px 0 8px;text-transform:uppercase;letter-spacing:1px\">\ud83d\udee0 Key Skills</h3>\n    <div>${competenze}</div>\n    \n    <div style=\"display:flex;gap:16px;margin-top:20px;flex-wrap:wrap\">\n      <div style=\"flex:1;min-width:240px\">\n        <h3 style=\"font-size:14px;color:#15803d;margin:0 0 8px;text-transform:uppercase;letter-spacing:1px\">\u2713 Strengths</h3>\n        <ul style=\"margin:0;padding-left:20px;color:#374151;font-size:14px;line-height:1.6\">${puntiForza}</ul>\n      </div>\n      <div style=\"flex:1;min-width:240px\">\n        <h3 style=\"font-size:14px;color:#b91c1c;margin:0 0 8px;text-transform:uppercase;letter-spacing:1px\">\u26a0 Weaknesses</h3>\n        <ul style=\"margin:0;padding-left:20px;color:#374151;font-size:14px;line-height:1.6\">${puntiDebolezza}</ul>\n      </div>\n    </div>\n    \n    <h3 style=\"font-size:14px;color:#374151;margin:24px 0 8px;text-transform:uppercase;letter-spacing:1px\">\ud83d\udcdd Detailed Analysis</h3>\n    <div style=\"font-size:14px;color:#4b5563;line-height:1.7;background:#fafafa;padding:16px;border-radius:8px\">${parsed.spiegazione_dettagliata}</div>\n    \n    ${recCorrected ? `<div style=\"margin-top:16px;background:#fef3c7;border-left:3px solid #f59e0b;padding:10px 14px;font-size:13px;color:#92400e;border-radius:4px\">\u26a0\ufe0f Recommendation auto-corrected from \"${parsed.raccomandazione_originale}\" a \"${parsed.raccomandazione}\" per coerenza con il punteggio.</div>` : ''}\n    \n    <div style=\"margin-top:24px;padding-top:20px;border-top:1px solid #e5e7eb;display:flex;gap:24px;flex-wrap:wrap;font-size:12px;color:#9ca3af\">\n      <div>\u23f1 ${executionTime}s</div>\n      <div>\ud83d\udcc4 <a href=\"${ctx.driveLink}\" style=\"color:#3b82f6;text-decoration:none\">Open CV on Drive</a></div>\n      <div>\ud83e\udd16 Llama 3.3 70B (Groq)</div>\n    </div>\n  </div>\n</div>\n  `;\n  \n  output.push({\n    json: {\n      ...parsed,\n      _emailHtml: emailHtml,\n      _emailSubject: `\ud83c\udfaf ${parsed.nome} ${parsed.cognome} - Score ${score}/100 (${parsed.raccomandazione})`,\n      cv_drive_link: ctx.driveLink,\n      cv_filename: ctx.fileName,\n      processed_at: new Date().toISOString(),\n      execution_time: executionTime,\n      error: false\n    }\n  });\n}\n\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "093a5c93-4f3a-4572-b6a6-e4a1918584fc",
      "name": "Has Error?",
      "type": "n8n-nodes-base.if",
      "position": [
        3152,
        1008
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "error-check",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.error }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8c1f4352-064f-420e-8b24-68c4a127fea1",
      "name": "Score >= 75?",
      "type": "n8n-nodes-base.if",
      "position": [
        3728,
        1104
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "score-threshold",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.punteggio }}",
              "rightValue": 75
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "1b31fddc-ba98-462c-b85e-e67440441926",
      "name": "\ud83d\udce7 Notify Recruiter",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        4048,
        992
      ],
      "parameters": {
        "html": "={{ $json._emailHtml }}",
        "options": {},
        "subject": "={{ $json._emailSubject }}",
        "toEmail": "user@example.com",
        "fromEmail": "user@example.com"
      },
      "typeVersion": 2.1
    },
    {
      "id": "364cbfe9-aedc-4bae-b67f-26f371aa86a0",
      "name": "\u26a0\ufe0f Notify Error",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        3600,
        864
      ],
      "parameters": {
        "options": {},
        "subject": "\u274c CV Rating Pipeline Error",
        "toEmail": "user@example.com",
        "fromEmail": "user@example.com"
      },
      "typeVersion": 2.1
    },
    {
      "id": "8a1e7307-e39c-4830-9b27-45a857d79598",
      "name": "Share file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        1696,
        1008
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "options": {},
        "operation": "share",
        "permissionsUi": {
          "permissionsValues": {
            "role": "reader",
            "type": "anyone"
          }
        }
      },
      "typeVersion": 3
    },
    {
      "id": "e74acfd0-5f82-4ffa-840d-113b9d6e3c52",
      "name": "\ud83d\udcc1 Upload Drive",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        1536,
        1008
      ],
      "parameters": {
        "name": "={{ $json.fileName }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        },
        "inputDataFieldName": "cv"
      },
      "typeVersion": 3
    },
    {
      "id": "0b9ce595-7d22-46c9-80b9-cec8f930a152",
      "name": "\ud83e\udde0 Mistral OCR",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1936,
        1008
      ],
      "parameters": {
        "url": "https://api.mistral.ai/v1/ocr",
        "method": "POST",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={\n  \"model\": \"mistral-ocr-latest\",\n  \"document\": {\n    \"type\": \"document_url\",\n    \"document_url\": \"https://drive.google.com/uc?export=download&id={{ $node['\ud83d\udcc1 Upload Drive'].json.id }}\"\n  },\n  \"include_image_base64\": false\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "mistralCloudApi"
      },
      "typeVersion": 4.2
    },
    {
      "id": "1e3d73b1-f241-49da-be52-8b44d3e54f1a",
      "name": "Groq Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        2320,
        1248
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {
          "temperature": 0.2,
          "maxTokensToSample": 2000
        }
      },
      "typeVersion": 1
    },
    {
      "id": "98f7dfd0-d690-4fdc-9ddd-ef3e15fc3bdb",
      "name": "Append row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3488,
        1120
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "36269d3c-8615-4cc9-b84e-146f89a92ddb",
      "name": "CV check",
      "type": "n8n-nodes-base.if",
      "position": [
        1168,
        1104
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "error-check",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.subject }}",
              "rightValue": "ADD_HEADER_INDEED_MAIL"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a5720f62-a4e6-4737-a4c8-e2ae8b914b4a",
      "name": "\ud83e\uddf9 Clean Text",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        1008
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// CLEAN & NORMALIZE OCR OUTPUT\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst items = $input.all();\nconst output = [];\n\nfor (const item of items) {\n  const ocrResponse = item.json;\n  let fullText = '';\n  \n  if (ocrResponse.pages && Array.isArray(ocrResponse.pages)) {\n    fullText = ocrResponse.pages\n      .map(p => p.markdown || p.text || '')\n      .join('\\n\\n');\n  }\n  \n  // Normalize whitespace and newlines\n  fullText = fullText\n    .replace(/\\s{3,}/g, ' ')\n    .replace(/\\n{3,}/g, '\\n\\n')\n    .trim();\n  \n  // Retrieve context from previous nodes (production workflow)\n  const meta = $('Extract Email Metadata').first().json;\n  const drive = $('\ud83d\udcc1 Upload Drive').first().json;\n  \n  output.push({\n    json: {\n      _meta: meta._meta,\n      cvText: fullText,\n      candidateName: meta.candidateName,\n      position: meta.position,\n      applicationDate: meta.date,\n      fileName: meta.fileName,\n      driveLink: drive.webViewLink,\n      driveId: drive.id,\n      textLength: fullText.length\n    }\n  });\n}\n\n// Drop binary data to keep the flow lightweight\nreturn output.map(item => ({ json: item.json }));"
      },
      "typeVersion": 2
    },
    {
      "id": "842769b0-4d36-4bda-a591-75955f6df68c",
      "name": "\u2699\ufe0f Note Configuration",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        112
      ],
      "parameters": {
        "color": 3,
        "width": 380,
        "height": 460,
        "content": "## \u2699\ufe0f Configuration\n\nAll user-configurable variables are grouped here.\n**Edit this node before activating the workflow.**\n\n| Variable | Description |\n|---|---|\n| `DRIVE_ROOT_FOLDER_ID` | Google Drive folder ID where CVs are saved |\n| `SHEET_ID` | Google Sheets ID for the HR dashboard |\n| `RECRUITER_EMAIL` | Email address to notify for top candidates |\n| `OPS_EMAIL` | Email address to notify on errors |\n| `SENDER_EMAIL` | Sender address (must match your SMTP account) |\n| `SCORE_THRESHOLD` | Minimum score to trigger recruiter notification (default: 75) |\n| `IMAP_SUBJECT_FILTER` | Subject keyword to filter Indeed emails |\n\n\ud83d\udca1 Folder/Sheet IDs are found in the URL of the respective Google resource."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "d47e3399-24aa-43c0-a5cc-7208a3e64444",
  "connections": {
    "CV check": {
      "main": [
        [
          {
            "node": "Extract Email Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Error?": {
      "main": [
        [
          {
            "node": "\u26a0\ufe0f Notify Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Share file": {
      "main": [
        [
          {
            "node": "\ud83e\udde0 Mistral OCR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score >= 75?": {
      "main": [
        [
          {
            "node": "\ud83d\udce7 Notify Recruiter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "\u2728 CV Analyzer Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\uddf9 Clean Text": {
      "main": [
        [
          {
            "node": "\u2728 CV Analyzer Chain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udde0 Mistral OCR": {
      "main": [
        [
          {
            "node": "\ud83e\uddf9 Clean Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcc1 Upload Drive": {
      "main": [
        [
          {
            "node": "Share file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append row in sheet": {
      "main": [
        [
          {
            "node": "Score >= 75?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2699\ufe0f Configuration": {
      "main": [
        [
          {
            "node": "CV check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2728 CV Analyzer Chain": {
      "main": [
        [
          {
            "node": "\u2705 Validate & Build Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Email Metadata": {
      "main": [
        [
          {
            "node": "\ud83d\udcc1 Upload Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "\u2728 CV Analyzer Chain",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "\u2705 Validate & Build Email": {
      "main": [
        [
          {
            "node": "Has Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IMAP Email Trigger (Indeed)": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This template is for HR teams, recruitment agencies, and startups that receive job applications via Indeed and want to eliminate manual CV screening. If you're spending hours reading CVs before deciding who to call, this workflow automates that first-pass evaluation entirely.

Source: https://n8n.io/workflows/15854/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This workflow automates the process of analyzing emails and their attachments (PDFs and images) using AI models (DeepSeek, Gemini, and OpenRouter). It extracts and summarizes the content of emails and

Email Read Imap, OpenAI Chat, Chain Summarization +7
AI & RAG

This comprehensive workflow automates the complete financial document processing pipeline using AI. Upload invoices via chat, drop expense receipts into a folder, or add bank statements - the system a

Chat Trigger, HTTP Request, Google Sheets +8
AI & RAG

Resume Screening & Behavioral Interviews with Gemini, Elevenlabs, & Notion ATS copy. Uses outputParserStructured, chainLlm, googleDrive, stickyNote. Webhook trigger; 67 nodes.

Output Parser Structured, Chain Llm, Google Drive +9
AI & RAG

Candidate Engagement | Resume Screening | AI Voice Interviews | Applicant Insights

Output Parser Structured, Chain Llm, Google Drive +9
AI & RAG

This end-to-end AI-powered recruitment automation workflow helps HR and talent acquisition teams automate the complete hiring pipeline—from resume intake and parsing to GPT-4-based evaluation, TA appr

Form Trigger, Output Parser Structured, Google Drive +10