Automate Resume Screening with n8n + Ollama (AI Recruitment Pipeline)
Recruiters spend 23 hours screening resumes for a single hire. For popular positions, that means reading 200–500 applications, most of which are obvious non-matches. AI resume screening tools like HireVue or Pymetrics charge $5,000–25,000/year and require uploading candidate data to cloud servers.
With n8n and Ollama, you can build a resume screening pipeline that runs on your own hardware — zero API costs, candidate data stays private (critical for GDPR), and you control exactly how scoring works.
In this tutorial, you'll build a workflow that:
- Receives resumes via email attachment or webhook upload
- Extracts text from PDF/DOCX documents
- Scores candidates against your job requirements using local AI
- Extracts structured data (skills, experience, education)
- Ranks applicants and sends a shortlist to the hiring manager
Why Local AI for Resume Screening?
Resume screening is a perfect use case for self-hosted AI:
| Factor | Cloud AI Screening | Local AI (Ollama) |
|---|---|---|
| Data privacy (GDPR) | Candidate PII sent to US servers | Data never leaves your infrastructure |
| Cost per resume | $0.05–0.50 per API call | $0 (runs on your hardware) |
| Bias auditability | Black box — can't inspect model | Full control over prompts and scoring |
| Customization | Generic scoring models | Tailored to your exact job requirements |
| Volume limits | Rate-limited | Unlimited (hardware-bound only) |
GDPR note: Under GDPR Article 22, candidates have the right to not be subject to purely automated decision-making. This workflow is designed as a screening assistant — it ranks and scores to help humans make faster decisions, not to auto-reject candidates. Always have a human make the final hiring decision.
The Architecture
Resume Received (Email/Webhook)
↓
[Extract Text] → [Parse Sections] → [Score Against Requirements]
↓
[Extract Skills]
↓
[Rank Candidates]
↓
[Email Shortlist Report]
Step 1: Receive and Extract Resume Text
Set up a webhook that accepts resume uploads (PDF or DOCX):
// Webhook Node
HTTP Method: POST
Path: /resume-screen
Binary Property: file
// For email-based intake, use IMAP Trigger instead
// and extract attachments from the email
Then extract text from the document. For PDFs, use n8n's built-in PDF extraction or an HTTP Request to a local tool:
// Function Node: Extract Text
// For PDF: use pdf-parse or similar
// For DOCX: extract from XML structure
const text = $input.first().json.text ||
$input.first().json.body ||
Buffer.from($input.first().binary.file.data, 'base64').toString('utf-8');
// Clean up extracted text
const cleaned = text
.replace(/\s+/g, ' ')
.replace(/[^\x20-\x7E\n]/g, '')
.trim()
.substring(0, 4000); // Stay within context window
return [{ json: { resume_text: cleaned } }];
Step 2: Define Job Requirements
Create a structured job requirements template that Ollama will score against:
// Set Node: Job Requirements
{
"job_title": "Senior Backend Engineer",
"required_skills": ["Python", "PostgreSQL", "REST APIs", "Docker"],
"preferred_skills": ["Kubernetes", "AWS", "Redis", "GraphQL"],
"min_experience_years": 5,
"education": "BS in Computer Science or equivalent",
"key_responsibilities": [
"Design and build scalable microservices",
"Lead technical design reviews",
"Mentor junior engineers"
]
}
Step 3: AI Scoring with Ollama
The core scoring prompt asks Ollama to evaluate the resume against your requirements:
// HTTP Request to Ollama
{
"model": "llama3:8b",
"prompt": "You are an expert technical recruiter. Score this resume against the job requirements.\n\nJob: Senior Backend Engineer\nRequired Skills: Python, PostgreSQL, REST APIs, Docker\nPreferred Skills: Kubernetes, AWS, Redis, GraphQL\nMin Experience: 5 years\n\nResume:\n${resume_text}\n\nRespond in this exact JSON format:\n{\n \"overall_score\": 0-100,\n \"skills_match\": {\n \"required_met\": [\"list of required skills found\"],\n \"required_missing\": [\"list of required skills NOT found\"],\n \"preferred_met\": [\"list of preferred skills found\"]\n },\n \"experience_years\": estimated_number,\n \"experience_relevance\": \"high/medium/low\",\n \"education_match\": true/false,\n \"strengths\": [\"top 3 strengths for this role\"],\n \"concerns\": [\"top 3 concerns or gaps\"],\n \"recommendation\": \"strong_yes/yes/maybe/no\",\n \"summary\": \"2-3 sentence assessment\"\n}",
"stream": false,
"options": {
"temperature": 0.1,
"num_predict": 1000
}
}
Why temperature: 0.1? Resume scoring must be consistent. Two identical resumes should get the same score. Low temperature makes the model deterministic. The structured JSON output format also helps — Ollama follows formatting instructions more reliably at low temperatures.
Step 4: Extract and Structure Results
Parse the AI response and add metadata:
// Function Node: Parse Scoring Results
const response = JSON.parse($json.data).response;
let scoring;
try {
// Extract JSON from the response
const jsonMatch = response.match(/\{[\s\S]*\}/);
scoring = JSON.parse(jsonMatch[0]);
} catch (e) {
scoring = {
overall_score: 0,
recommendation: 'error',
summary: 'Failed to parse AI response'
};
}
return [{
json: {
candidate_name: $('Extract Text').item.json.candidate_name || 'Unknown',
email: $('Extract Text').item.json.email || '',
...scoring,
reviewed_at: new Date().toISOString(),
reviewer: 'AI (Ollama llama3:8b)'
}
}];
Step 5: Rank and Report
After processing all resumes, sort by score and email the shortlist:
// Function Node: Generate Shortlist Report
const candidates = $input.all()
.map(item => item.json)
.sort((a, b) => b.overall_score - a.overall_score);
const shortlist = candidates.filter(c => c.overall_score >= 60);
let report = `# Resume Screening Report\n\n`;
report += `**Position:** Senior Backend Engineer\n`;
report += `**Total Resumes:** ${candidates.length}\n`;
report += `**Shortlisted:** ${shortlist.length}\n\n`;
report += `## Top Candidates\n\n`;
for (const c of shortlist) {
report += `### ${c.candidate_name} — Score: ${c.overall_score}/100\n`;
report += `**Recommendation:** ${c.recommendation}\n`;
report += `**Skills Match:** ${c.skills_match.required_met.join(', ')}\n`;
report += `**Missing:** ${c.skills_match.required_missing.join(', ') || 'None'}\n`;
report += `**Summary:** ${c.summary}\n\n`;
}
return [{ json: { report, shortlist_count: shortlist.length, total: candidates.length } }];
Reducing AI Bias in Resume Screening
AI models can inherit biases from training data. Here's how to mitigate this:
- Remove identifying information before scoring. Strip names, photos, addresses, and university names from the text before sending to Ollama. Score purely on skills and experience.
- Use skills-based scoring only. The prompt above focuses on skills match, experience relevance, and technical fit — not proxies like company prestige or university ranking.
- Audit regularly. Run the same diverse set of test resumes monthly to check for scoring drift. Log all scores for compliance.
- Human review is mandatory. This is a screening tool, not a hiring tool. Every shortlisted candidate should be reviewed by a human before any outreach.
Scaling for High Volume
For companies receiving 500+ applications per posting:
- Batch processing: Use n8n's "Split in Batches" node to process 10 resumes at a time, preventing Ollama from queuing too many requests
- Pre-filtering: Add a keyword filter before AI scoring — if the resume doesn't contain any required skills as plain text, skip the AI step
- Database storage: Save all scores to PostgreSQL for historical analysis and compliance auditing
- Scheduled runs: Instead of real-time processing, batch resumes hourly and process them in bulk
Complete Workflow JSON
Click to expand full workflow JSON
{
"name": "AI Resume Screening Pipeline (Ollama)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "resume-screen",
"responseMode": "lastNode"
},
"id": "webhook",
"name": "Resume Upload Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "resume",
"name": "resume_text",
"value": "={{ $json.body.resume_text || $json.body.text || 'No resume text provided' }}",
"type": "string"
},
{
"id": "name",
"name": "candidate_name",
"value": "={{ $json.body.candidate_name || 'Unknown' }}",
"type": "string"
},
{
"id": "email",
"name": "candidate_email",
"value": "={{ $json.body.email || '' }}",
"type": "string"
},
{
"id": "job",
"name": "job_title",
"value": "={{ $json.body.job_title || 'Senior Backend Engineer' }}",
"type": "string"
}
]
}
},
"id": "prepare",
"name": "Prepare Resume Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [460, 300]
},
{
"parameters": {
"url": "http://localhost:11434/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'llama3:8b', prompt: 'You are an expert technical recruiter. Score this resume against the job requirements.\\n\\nJob: ' + $json.job_title + '\\nRequired Skills: Python, PostgreSQL, REST APIs, Docker\\nPreferred: Kubernetes, AWS, Redis, GraphQL\\nMin Experience: 5 years\\n\\nResume:\\n' + ($json.resume_text || '').substring(0, 4000) + '\\n\\nRespond in JSON: {\"overall_score\": 0-100, \"skills_match\": {\"required_met\": [], \"required_missing\": [], \"preferred_met\": []}, \"experience_years\": N, \"recommendation\": \"strong_yes/yes/maybe/no\", \"summary\": \"2-3 sentences\"}', stream: false, options: { temperature: 0.1, num_predict: 800 } }) }}",
"options": { "timeout": 120000 }
},
"id": "score",
"name": "Score Resume (Ollama)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [680, 300]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "result",
"name": "scoring",
"value": "={{ JSON.parse($json.data).response }}",
"type": "string"
},
{
"id": "candidate",
"name": "candidate_name",
"value": "={{ $('Prepare Resume Data').item.json.candidate_name }}",
"type": "string"
},
{
"id": "email",
"name": "candidate_email",
"value": "={{ $('Prepare Resume Data').item.json.candidate_email }}",
"type": "string"
}
]
}
},
"id": "output",
"name": "Format Results",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [900, 300]
}
],
"connections": {
"Resume Upload Webhook": {
"main": [[{ "node": "Prepare Resume Data", "type": "main", "index": 0 }]]
},
"Prepare Resume Data": {
"main": [[{ "node": "Score Resume (Ollama)", "type": "main", "index": 0 }]]
},
"Score Resume (Ollama)": {
"main": [[{ "node": "Format Results", "type": "main", "index": 0 }]]
}
},
"settings": { "executionOrder": "v1" },
"tags": [{ "name": "AI" }, { "name": "Ollama" }, { "name": "HR" }, { "name": "Recruitment" }]
}
Wrapping Up
AI resume screening with n8n + Ollama gives your recruitment team a fast, private, and customizable first-pass filter. It handles the tedious work of matching skills against requirements and ranking candidates, so recruiters can spend their time on interviews and relationship-building instead of reading 500 PDFs.
The key advantages over cloud-based solutions: your candidate data stays on your infrastructure (critical for GDPR), you pay zero per-resume processing costs, and you have full control over how scoring works — no black-box AI making decisions you can't audit.
Want 11 Production-Ready AI Workflows?
The Self-Hosted AI Workflow Pack includes resume screening, lead scoring, email automation, document processing, and 7 more n8n + Ollama templates. One payment, unlimited runs, zero API costs.
Get the Full Pack — $39