This workflow corresponds to n8n.io template #15155 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets 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 →
{
"id": "aCL6iKaxHJPvZrasrR_Ih",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "CV Screening Agent",
"tags": [],
"nodes": [
{
"id": "dfd6dfd0-28ec-4bca-a130-e47a03a0c7dd",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1728,
-240
],
"parameters": {
"width": 480,
"height": 800,
"content": "## CV Screening Agent\n\n### How it works\n\n1. The CV Intake Webhook captures incoming CV submissions.\n2. Input Validator checks the format and routes it through Format Router.\n3. Extract from PDF/DOCX and Fetch Job Requirements prepare text data.\n4. LLM Screening Agent screens CVs, followed by JSON Parsing.\n5. Duplicate Guard and Score Threshold Gate manage candidate scores and duplicates.\n6. Top candidates are sent email invites; others get polite declines.\n\n### Setup steps\n\n- [ ] Configure CV Intake Webhook URL in your application.\n- [ ] Set up Google Sheets API credentials for fetching job requirements and logging.\n- [ ] Integrate OpenAI API for the LLM Screening Agent.\n- [ ] Configure Gmail API credentials for email notifications.\n- [ ] Set up Telegram API credentials for HR alerts.\n\n### Customization\n\nAdjust the score threshold in the Score Threshold Gate according to desired candidate quality."
},
"typeVersion": 1
},
{
"id": "89413c8c-25aa-4bcc-984d-aac26cad1417",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
-128
],
"parameters": {
"color": 7,
"width": 368,
"height": 304,
"content": "## CV intake and validation\n\nHandle incoming CV data and validate input formats."
},
"typeVersion": 1
},
{
"id": "789995eb-e9d4-40d7-ba51-bac116cacaf0",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-768,
-208
],
"parameters": {
"color": 7,
"width": 368,
"height": 512,
"content": "## Format identification and extraction\n\nDetermine file format and extract content."
},
"typeVersion": 1
},
{
"id": "d38cc77d-6903-4eb2-874d-f38faa592148",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-320,
-144
],
"parameters": {
"color": 7,
"width": 400,
"height": 304,
"content": "## Job requirements fetching and preparation\n\nFetch job requirements and prepare CV text for screening."
},
"typeVersion": 1
},
{
"id": "f28f2e6c-c001-4285-970c-c3aee3c65f45",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
112,
-128
],
"parameters": {
"color": 7,
"width": 480,
"height": 272,
"content": "## Screening and JSON parsing\n\nScreen the CV using LLM and parse results to JSON."
},
"typeVersion": 1
},
{
"id": "422de1ef-f201-475b-8350-52a12c98600f",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
624,
-144
],
"parameters": {
"color": 7,
"width": 368,
"height": 304,
"content": "## Duplicate checking and scoring\n\nGuard against duplicate entries and assess candidate scores."
},
"typeVersion": 1
},
{
"id": "99a36e34-e2e2-4c5a-b1c5-d3c66bbf647a",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1024,
-240
],
"parameters": {
"color": 7,
"width": 528,
"height": 512,
"content": "## Final logging and communication\n\nLog candidate data and notify HR and candidates via Email and Telegram."
},
"typeVersion": 1
},
{
"id": "37f4027a-6ce1-409a-8d9a-d49f5609cefc",
"name": "Parse JSON Content",
"type": "n8n-nodes-base.code",
"position": [
448,
-16
],
"parameters": {
"jsCode": "const raw = $input.first().json.output[0].content[0].text;\nconst parsed = JSON.parse(raw);\nreturn { json: parsed };"
},
"typeVersion": 2
},
{
"id": "8f14ae73-63ac-400d-9962-16cc9340146e",
"name": "Send Interview Invite Email",
"type": "n8n-nodes-base.gmail",
"position": [
1408,
-112
],
"parameters": {
"sendTo": "={{ $('Receive CV Webhook').item.json.body.candidate_email }}",
"message": "=Dear {{ $('Parse JSON Content').item.json.candidate_name }}, Thank you for your application for the {{ $('Read Job Requirements').item.json.job_title }} position. We have reviewed your CV and are impressed with your profile. We would like to invite you for an interview to discuss the role further. Could you share your availability for a 30-minute call this week or next? Best regards, HR Team",
"options": {},
"subject": "=Your application for {{ $('Read Job Requirements').item.json.job_title }} \u2014 Next steps"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "5f29c1b6-a4cd-41a2-8a2d-598b35fa109a",
"name": "Send Rejection Email",
"type": "n8n-nodes-base.gmail",
"position": [
1248,
96
],
"parameters": {
"sendTo": "={{ $('Receive CV Webhook').item.json.body.candidate_email }}",
"message": "=Dear {{ $('Parse JSON Content').item.json.candidate_name }}, Thank you for taking the time to apply for the {{ $('Read Job Requirements').item.json.job_title }} position. After careful review, we have decided to move forward with other candidates whose experience more closely matches our current needs. We appreciate your interest and encourage you to apply for future openings. Best regards, HR Team",
"options": {},
"subject": "=Your application for {{ $('Read Job Requirements').item.json.job_title }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "37c83770-2b7a-42e0-b492-1df9540c3dc0",
"name": "Parse DOCX CV",
"type": "n8n-nodes-base.code",
"position": [
-544,
128
],
"parameters": {
"jsCode": "const mammoth = require('mammoth');\nconst binaryData = await this.helpers.getBinaryDataBuffer(0, 'file0');\n\nconst result = await mammoth.extractRawText({ buffer: binaryData });\n\nreturn { json: { text: result.value } };"
},
"typeVersion": 2
},
{
"id": "6eb92caa-d53a-4742-ba9a-68d0fd6431cc",
"name": "Generate CV Text",
"type": "n8n-nodes-base.code",
"position": [
-64,
-16
],
"parameters": {
"jsCode": "let cvText = '';\n\ntry {\n cvText = $('Parse PDF CV').first().json.text;\n} catch (e) {\n // PDF node didn't run\n}\n\nif (!cvText) {\n try {\n cvText = $('Parse DOCX CV').first().json.text;\n } catch (e) {\n // DOCX node didn't run either\n }\n}\n\nconst sheetData = $('Read Job Requirements').first().json;\n\nreturn {\n json: {\n ...sheetData,\n cv_text: cvText\n }\n};"
},
"typeVersion": 2
},
{
"id": "b1b55610-2946-47ca-84ba-8e3851df5652",
"name": "Receive CV Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-1120,
0
],
"parameters": {
"path": "cv-screening",
"options": {
"allowedOrigins": "*",
"binaryPropertyName": "file"
},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "4d590cec-9735-4b42-ae4e-a9207301c49c",
"name": "Route by File Format",
"type": "n8n-nodes-base.switch",
"position": [
-720,
0
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "6675c489-01b7-4ea3-8a10-43468396cfce",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $binary.file0.fileName.split('.').pop() }}",
"rightValue": "pdf"
}
]
}
},
{
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "809be46e-1ffb-43e0-a6eb-67f00d2bfd31",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $binary.file0.fileName.split('.').pop() }}",
"rightValue": "docx"
}
]
}
}
]
},
"options": {}
},
"typeVersion": 3.4
},
{
"id": "fa40a5d7-60bd-434d-9735-d86b2f18333d",
"name": "Parse PDF CV",
"type": "n8n-nodes-base.extractFromFile",
"position": [
-544,
-80
],
"parameters": {
"options": {},
"operation": "pdf",
"binaryPropertyName": "file0"
},
"typeVersion": 1.1
},
{
"id": "4272eb11-dcd5-4b6c-997b-bee8b9cdfedb",
"name": "Read Job Requirements",
"type": "n8n-nodes-base.googleSheets",
"position": [
-272,
-16
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $('Receive CV Webhook').item.json.body.job_id }}",
"lookupColumn": "job_id"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1kLEBclajlxrkJATa2tNuLwPIlQGpVmx7VfdrpbwcKcM/edit#gid=0",
"cachedResultName": "\u041b\u0438\u0441\u04421"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1kLEBclajlxrkJATa2tNuLwPIlQGpVmx7VfdrpbwcKcM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1kLEBclajlxrkJATa2tNuLwPIlQGpVmx7VfdrpbwcKcM/edit?usp=drivesdk",
"cachedResultName": "CV Screening \u2014 Job Requirements"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "ed7934a6-039b-4bb6-9cb2-e546d0050ea4",
"name": "AI Screening Model",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
160,
-16
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {},
"simplify": false,
"responses": {
"values": [
{
"role": "system",
"content": "You are an expert HR screening assistant. You analyze CVs against job requirements and provide structured assessments.\n\nYou MUST respond ONLY with valid JSON, no markdown, no extra text."
},
{
"content": "=Analyze this CV against the job requirements below.\n\n### JOB REQUIREMENTS:\n- Position: {{ $('Read Job Requirements').item.json.job_title }}\n- Required skills: {{ $('Read Job Requirements').item.json.required_skills }}\n- Nice to have: {{ $('Read Job Requirements').item.json.nice_to_have }}\n- Minimum experience: {{ $('Read Job Requirements').item.json.min_experience_years }} years\n- Description: {{ $('Read Job Requirements').item.json.description }}\n\n### CANDIDATE CV:\n{{ $json.cv_text }}\n\n### YOUR TASK:\n1. Score the candidate from 0 to 100 based on how well they match the requirements\n2. List GREEN FLAGS (strong matches with the job)\n3. List RED FLAGS (concerns, gaps, mismatches)\n4. Write a brief recommendation (2-3 sentences)\n\n### OUTPUT FORMAT (JSON only, no markdown):\n{\n \"candidate_name\": \"\",\n \"job_id\": \"{{ $('Receive CV Webhook').item.json.body.job_id }}\",\n \"score\": 0,\n \"green_flags\": [\"flag1\", \"flag2\"],\n \"red_flags\": [\"flag1\", \"flag2\"],\n \"skills_matched\": [\"skill1\", \"skill2\"],\n \"skills_missing\": [\"skill1\", \"skill2\"],\n \"experience_years\": 0,\n \"recommendation\": \"\",\n \"tier\": \"\"\n}\n\nSCORING GUIDE:\n- 90-100: Perfect match, immediate interview\n- 70-89: Strong candidate, recommend interview\n- 50-69: Partial match, review manually\n- 0-49: Poor match, archive\n\nSet \"tier\" based on the score: \"EXCELLENT\", \"STRONG\", \"REVIEW\", or \"ARCHIVE\"."
}
]
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "126ab010-eaf8-4e2e-b666-fdd8f654c9a6",
"name": "Check Score Threshold",
"type": "n8n-nodes-base.if",
"position": [
848,
-16
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b1b45aa6-dd69-45e0-b8e2-d4f311f044a3",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.score }}",
"rightValue": "={{ $('Read Job Requirements').item.json.threshold_score }}"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "c71c22c8-68ce-461b-940f-e149bfe85c17",
"name": "Append Top Candidates to Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1072,
-112
],
"parameters": {
"columns": {
"value": {
"Date": "={{ $now.format('dd.MM.yyyy HH:mm') }}",
"Tier": "={{ $('Parse JSON Content').item.json.tier }}",
"Score": "={{ $('Parse JSON Content').item.json.score }}",
"Job ID": "={{ $('Parse JSON Content').item.json.job_id }}",
"Status": "Interview Scheduled",
"Candidate": "={{ $('Parse JSON Content').item.json.candidate_name }}",
"Job Title": "={{ $('Read Job Requirements').item.json.job_title }}",
"Red Flags": "={{ $('Parse JSON Content').item.json.red_flags.join(', ') }}",
"Experience": "={{ $('Parse JSON Content').item.json.experience_years }}",
"Green Flags": "={{ $('Parse JSON Content').item.json.green_flags.join(', ') }}",
"Recommendation": "={{ $('Parse JSON Content').item.json.recommendation }}",
"Skills Matched": "={{ $('Parse JSON Content').item.json.skills_matched.join(', ') }}",
"Skills Missing": "={{ $('Parse JSON Content').item.json.skills_missing.join(', ') }}"
},
"schema": [
{
"id": "Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Candidate",
"type": "string",
"display": true,
"required": false,
"displayName": "Candidate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Job ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Job ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Job Title",
"type": "string",
"display": true,
"required": false,
"displayName": "Job Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Score",
"type": "string",
"display": true,
"required": false,
"displayName": "Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tier",
"type": "string",
"display": true,
"required": false,
"displayName": "Tier",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Green Flags",
"type": "string",
"display": true,
"required": false,
"displayName": "Green Flags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Red Flags",
"type": "string",
"display": true,
"required": false,
"displayName": "Red Flags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Skills Matched",
"type": "string",
"display": true,
"required": false,
"displayName": "Skills Matched",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Skills Missing",
"type": "string",
"display": true,
"required": false,
"displayName": "Skills Missing",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Experience",
"type": "string",
"display": true,
"required": false,
"displayName": "Experience",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Recommendation",
"type": "string",
"display": true,
"required": false,
"displayName": "Recommendation",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1PsaktBjJnJ8L06HXmQjqORbgItdpROaXvOBu_svUVAU/edit#gid=0",
"cachedResultName": "\u041b\u0438\u0441\u04421"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1PsaktBjJnJ8L06HXmQjqORbgItdpROaXvOBu_svUVAU",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1PsaktBjJnJ8L06HXmQjqORbgItdpROaXvOBu_svUVAU/edit?usp=drivesdk",
"cachedResultName": "CV Screening \u2014 Results"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "98317c0d-f7f7-491b-95a2-943c953c96d4",
"name": "Send HR Alert on Telegram",
"type": "n8n-nodes-base.telegram",
"position": [
1232,
-112
],
"parameters": {
"text": "=\ud83c\udfaf NEW TOP CANDIDATE! \ud83d\udc64 {{ $('Parse JSON Content').item.json.candidate_name }} \ud83d\udcbc Position: {{ $('Read Job Requirements').item.json.job_title }} \ud83d\udcca Score: {{ $('Parse JSON Content').item.json.score }}/100 \ud83c\udfc6 Tier: {{ $('Parse JSON Content').item.json.tier }} \u2705 Green flags: {{ $('Parse JSON Content').item.json.green_flags.join('\\n') }} \u26a0\ufe0f Red flags: {{ $('Parse JSON Content').item.json.red_flags.length > 0 ? $('Parse JSON Content').item.json.red_flags.join('\\n') : 'None' }} \ud83d\udcdd {{ $('Parse JSON Content').item.json.recommendation }}",
"chatId": "123456789",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "5a28dca2-b126-428c-b0db-23647ca0de79",
"name": "Archive Candidate Data",
"type": "n8n-nodes-base.googleSheets",
"position": [
1072,
96
],
"parameters": {
"columns": {
"value": {
"Date": "={{ $now.format('dd.MM.yyyy HH:mm') }}",
"Tier": "={{ $('Parse JSON Content').item.json.tier }}",
"Score": "={{ $('Parse JSON Content').item.json.score }}",
"Job ID": "={{ $('Parse JSON Content').item.json.job_id }}",
"Status": "Declined",
"Candidate": "={{ $('Parse JSON Content').item.json.candidate_name }}",
"Job Title": "={{ $('Read Job Requirements').item.json.job_title }}",
"Red Flags": "={{ $('Parse JSON Content').item.json.red_flags.join(', ') }}",
"Experience": "={{ $('Parse JSON Content').item.json.experience_years }}",
"Green Flags": "={{ $('Parse JSON Content').item.json.green_flags.join(', ') }}",
"Recommendation": "={{ $('Parse JSON Content').item.json.recommendation }}",
"Skills Matched": "={{ $('Parse JSON Content').item.json.skills_matched.join(', ') }}",
"Skills Missing": "={{ $('Parse JSON Content').item.json.skills_missing.join(', ') }}"
},
"schema": [
{
"id": "Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Candidate",
"type": "string",
"display": true,
"required": false,
"displayName": "Candidate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Job ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Job ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Job Title",
"type": "string",
"display": true,
"required": false,
"displayName": "Job Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Score",
"type": "string",
"display": true,
"required": false,
"displayName": "Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tier",
"type": "string",
"display": true,
"required": false,
"displayName": "Tier",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Green Flags",
"type": "string",
"display": true,
"required": false,
"displayName": "Green Flags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Red Flags",
"type": "string",
"display": true,
"required": false,
"displayName": "Red Flags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Skills Matched",
"type": "string",
"display": true,
"required": false,
"displayName": "Skills Matched",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Skills Missing",
"type": "string",
"display": true,
"required": false,
"displayName": "Skills Missing",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Experience",
"type": "string",
"display": true,
"required": false,
"displayName": "Experience",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Recommendation",
"type": "string",
"display": true,
"required": false,
"displayName": "Recommendation",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1PsaktBjJnJ8L06HXmQjqORbgItdpROaXvOBu_svUVAU/edit#gid=0",
"cachedResultName": "\u041b\u0438\u0441\u04421"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1PsaktBjJnJ8L06HXmQjqORbgItdpROaXvOBu_svUVAU",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1PsaktBjJnJ8L06HXmQjqORbgItdpROaXvOBu_svUVAU/edit?usp=drivesdk",
"cachedResultName": "CV Screening \u2014 Results"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "bbedf4c3-79c0-4734-b542-da6fc18ca77b",
"name": "Validate CV Input",
"type": "n8n-nodes-base.code",
"position": [
-944,
0
],
"parameters": {
"jsCode": "const item = $input.first();\n\n// 1. Check if file exists\nif (!item.binary || !item.binary.file0) {\n throw new Error('No file attached. Please send a CV file (PDF or DOCX).');\n}\n\n// 2. Check if job_id exists\nif (!item.json.body?.job_id) {\n throw new Error('Missing job_id. Please include job_id in your request.');\n}\n\n// 3. Check file format\nconst fileName = item.binary.file0.fileName || '';\nconst extension = fileName.split('.').pop().toLowerCase();\n\nif (!['pdf', 'docx'].includes(extension)) {\n throw new Error(`Unsupported file format: .${extension}. Only PDF and DOCX are accepted.`);\n}\n\n// 4. All checks passed \u2014 forward data\nreturn [item];"
},
"typeVersion": 2
},
{
"id": "05c7a26e-21c3-4a82-a36e-a2b432f62c86",
"name": "Check for Duplicates",
"type": "n8n-nodes-base.code",
"position": [
672,
-16
],
"parameters": {
"jsCode": "const candidate = $input.first().json.candidate_name;\nconst jobId = $input.first().json.job_id;\n\n// Create a unique key\nconst key = `${candidate}_${jobId}`.toLowerCase().replace(/\\s+/g, '_');\n\n// Check against static variable (persists across executions)\nconst seen = $getWorkflowStaticData('global');\n\nif (seen[key]) {\n throw new Error(`Duplicate: ${candidate} already screened for ${jobId}`);\n}\n\n// Mark as seen\nseen[key] = new Date().toISOString();\n\n// Pass data through\nreturn $input.all();"
},
"typeVersion": 2
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": "6679nTUbnBE1tTslH6wiu",
"timeSavedMode": "fixed",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "57a27fff-1107-4630-92c3-92c46255ce74",
"connections": {
"Parse PDF CV": {
"main": [
[
{
"node": "Read Job Requirements",
"type": "main",
"index": 0
}
]
]
},
"Parse DOCX CV": {
"main": [
[
{
"node": "Read Job Requirements",
"type": "main",
"index": 0
}
]
]
},
"Generate CV Text": {
"main": [
[
{
"node": "AI Screening Model",
"type": "main",
"index": 0
}
]
]
},
"Validate CV Input": {
"main": [
[
{
"node": "Route by File Format",
"type": "main",
"index": 0
}
]
]
},
"AI Screening Model": {
"main": [
[
{
"node": "Parse JSON Content",
"type": "main",
"index": 0
}
]
]
},
"Parse JSON Content": {
"main": [
[
{
"node": "Check for Duplicates",
"type": "main",
"index": 0
}
]
]
},
"Receive CV Webhook": {
"main": [
[
{
"node": "Validate CV Input",
"type": "main",
"index": 0
}
]
]
},
"Check for Duplicates": {
"main": [
[
{
"node": "Check Score Threshold",
"type": "main",
"index": 0
}
]
]
},
"Route by File Format": {
"main": [
[
{
"node": "Parse PDF CV",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse DOCX CV",
"type": "main",
"index": 0
}
]
]
},
"Check Score Threshold": {
"main": [
[
{
"node": "Append Top Candidates to Sheet",
"type": "main",
"index": 0
}
],
[
{
"node": "Archive Candidate Data",
"type": "main",
"index": 0
}
]
]
},
"Read Job Requirements": {
"main": [
[
{
"node": "Generate CV Text",
"type": "main",
"index": 0
}
]
]
},
"Archive Candidate Data": {
"main": [
[
{
"node": "Send Rejection Email",
"type": "main",
"index": 0
}
]
]
},
"Send HR Alert on Telegram": {
"main": [
[
{
"node": "Send Interview Invite Email",
"type": "main",
"index": 0
}
]
]
},
"Send Interview Invite Email": {
"main": [
[]
]
},
"Append Top Candidates to Sheet": {
"main": [
[
{
"node": "Send HR Alert on Telegram",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleSheetsOAuth2ApiopenAiApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Candidate submits CV via web form (PDF or DOCX) Workflow extracts and prepares CV text automatically GPT-4o scores the CV against configurable job requirements Ranked shortlist generated — HR notified via Telegram Full audit trail saved automatically in Google Sheets Import all…
Source: https://n8n.io/workflows/15155/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.
Universal Expense tracker. Uses telegram, httpRequest, openAi, googleSheets. Webhook trigger; 33 nodes.
This workflow automates document processing using LlamaParse to extract and analyze text from various file formats. It intelligently processes documents, extracts structured data, and delivers actiona
This workflow turns your WhatsApp Business number into a 24/7 AI-powered customer assistant — without any third-party chatbot platform. It receives incoming WhatsApp messages via Evolution API, unders
This workflow automates the process of generating, reviewing, and publishing blog posts across multiple platforms, now enhanced with support for RSS Feeds as a content source. It streamlines the manag