Skip to content

Repository files navigation

AI Resume Screening & Candidate Ranking System

An n8n-based recruitment automation system that takes an incoming job application (via webhook, with a PDF resume attachment), matches it against the correct job description, scores the candidate with Gemini against real requirements, and routes the outcome through a tiered decision path — CRM-style record keeping, candidate communication, and recruiter notification — with no data-corrupting shortcuts and no silent failures.

Why this exists

A resume screener that just runs a PDF through an LLM and returns a score isn't screening against anything — it's just summarizing a document. This system's core idea is that a match score is only meaningful relative to a specific job's actual requirements, so job-description retrieval and candidate scoring are treated as two connected steps, not one. It's also built to survive the realities of a real hiring pipeline: duplicate submissions, unsupported file types, missing attachments, and the need to keep a human in the loop for anything candidate-facing that could carry legal or reputational weight (like a rejection).

Architecture

Webhook (application submitted)
   │
   ▼
Data Extracter → Edit Fields (binary normalization)
   │
   ▼
If (guards: is a file actually attached?) → NO CV ATTACHED (stop)
   │
   ├─→ Search for Job Description and Required Skills (by Position)
   │        → Search for JD Error (stop) if no matching job found
   │
   ▼
Merge (candidate data + job requirements)
   │
   ▼
Switch1 (file-type gate: PDF only) → File is not supported (stop)
   │
   ▼
Extract from File → Basic LLM Chain (Gemini, primary + fallback model)
   → match_score, recommendation, strengths, weaknesses, skills, missing_skills, summary
   → Basic LLM Chain Error (stop) on failure
   │
   ▼
Create or update a record (Airtable upsert, matched on Email)
   → Record ID → CV upload chain (binary → base64 → Airtable content API)
   │
   ▼
Switch (tier router: >79 / >49 / <50 — explicit, no implicit fallback)
   │
   ├─ Strong Hire  → Update record1 (Recommendation locked to "Strong Hire")
   │                 → Send a message to recruiter + notify candidate (auto-send)
   ├─ Consider     → Update record  (Recommendation locked to "Consider",
   │                 Need Manual Review = true) → notify candidate (auto-send)
   └─ Reject       → Update record2 (Recommendation locked to "Reject", Status = "Rejected")
                     → notify candidate (auto-send, neutral "received" message)
                     → Create a draft of rejection (Gmail draft — NOT sent — for human review)

Key engineering decisions

The recommendation tier is enforced by the workflow, not trusted from the LLM. Rather than writing the model's recommendation output directly to Airtable, each tier's Update node explicitly overwrites Recommendation to match the score band that actually routed it there. This closes a real gap: an LLM given ambiguous scoring instructions can produce a recommendation that doesn't match its own match_score — the system doesn't let that inconsistency reach stored data.

Duplicate applications are handled via Airtable's native upsert, matched on Email — deliberately, not incidentally. One candidate maps to one active record. A new submission from the same email updates the existing record rather than creating a second one. This is an intentional design choice (a candidate has one current application state, not a running history of every submission) rather than an oversight — worth stating plainly here so it reads as a decision, not a limitation someone stumbles onto later.

Rejection emails are drafted, never auto-sent. Every tier gets an immediate, neutral "your application was received" notification — but the actual rejection content is created as a Gmail draft for a human to review and send. Auto-sending AI-generated rejection language carries real reputational and legal risk (specific, AI-phrased reasons for rejection can be disputed); keeping a human in the loop for that one message is a deliberate boundary, not a missing feature.

File-type and attachment validation happen before any processing, not after a crash. An If node confirms binary data is actually present before continuing, and a dedicated Switch1 gate confirms the file is a PDF before extraction — both routes to explicit, named error nodes (NO CV ATTACHED, File is not supported) rather than a generic n8n failure.

CV storage uses the Airtable content API directly, not the native node's default upload path. The native Airtable node can't attach binary files directly from workflow data, so the resume is converted to base64 and pushed via a direct HTTP call to Airtable's attachment endpoint — a more advanced solution than relying on an external file host.

Tech stack

  • Orchestration: n8n (native nodes preferred; Code nodes only where genuinely needed — dynamic field extraction, base64 conversion)
  • Scoring: Google Gemini (primary + fallback model pair)
  • Storage/CRM: Airtable (Search, Store tables), including native upsert for duplicate handling
  • Email: Gmail (send for notifications, draft for rejections)

Known limitations (honest, as of this version)

  • The recruiter notification address is a placeholder value, not a real address, since environment variables aren't currently configured for this n8n instance. Replace with your actual recruiting team's email (or wire up an env var / Airtable settings lookup) before using this in production.
  • One candidate = one active application, by design. A second submission from the same email overwrites the first record entirely (position, score, all fields) rather than preserving both as separate applications. This is intentional, not a bug — see above.
  • Position matching depends on the intake form enforcing a single-select field matching the Airtable Search table's exact values. The workflow itself does a case-sensitive exact match with no normalization — this is safe today because the form constrains input, but if the form and Airtable's option list ever drift out of sync (e.g., a new role added to one but not the other), lookups will silently fail closed (Search for JD Error) rather than fuzzy-match.
  • Attachment support is PDF only — enforced explicitly via Switch1, not a silent gap.

Setup requirements

  • n8n instance with @n8n/n8n-nodes-langchain package installed
  • Airtable base with Search (job descriptions) and Store (candidates) tables per the schema used throughout this build
  • Gmail OAuth credential with send + draft-creation scopes
  • Google Gemini API credential (stored in n8n credential manager)
  • Intake form constraining "Position" to the exact same option set as the Airtable Search table

Screenshot

Workflow Screenshot

About

AI-powered candidate screening pipeline built in n8n — Gemini-based scoring against real job requirements, enforced score-to-recommendation consistency, and human-reviewed rejection drafts.

Topics

Resources

Stars

Watchers

Forks

Contributors