{
  "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
          }
        ]
      ]
    }
  }
}