94 CVs Triaged in 11 Min for $0.31: n8n Beats Your ATS

Abstract tech illustration: 94 CVs Triaged in 11 Min for $0.31: n8n Beats Your ATS

Ninety-four applications for one junior role over four days. A founder's Saturday morning gone. Two strong candidates missed because they were buried under referral spam and a "Dear Hiring Manager at [Wrong Company]" cover letter. That's the actual pain point in early-stage hiring — not sourcing, not scheduling, triage. And every ATS on the market wants $50/seat/month before it reads a single resume, then hides its ranker behind a black box you can't audit. Here's the five-node n8n workflow I built instead, the exact classifier prompt, and the spots where it breaks.

The five-node workflow that replaces the ranker

The whole thing is a Gmail trigger, an attachment parser, a Claude Haiku classifier, an Airtable log, and a switch node. No queue, no cron, no dashboard. Applications land in a shared Gmail with the label inbound-applications (set by a filter on the jobs@yourdomain address), the workflow fires on the label, and eleven minutes later the founder gets a Telegram ping for the candidates worth a real look.

Here's the node-by-node shape:

  • Gmail Trigger — watches the inbound-applications label. Fires on every new match. No polling guesswork.
  • Function (parser) — checks for PDF attachments first, falls back to email body text.
  • Claude Haiku classifierstructured JSON output, four criteria rubric.
  • Airtable "Applications" base — audit log for every single application, scored or not.
  • Switch — three branches by score: >=7 → Telegram, 4–6 → review queue, <4 → auto-archive + rejection draft.

The n8n workflow itself runs in milliseconds. The eleven minutes is Haiku API latency across 94 sequential calls. If you batch or parallelize, you can push that under three minutes, but for a hiring workflow the wall-clock doesn't matter — the founder isn't sitting there watching it run.

The parser is where 80% of tutorials quietly fail

Attachment parsing is the boring part everyone skips, and it's the reason most "AI resume screener" demos fall apart in production. Roughly 90% of applications arrive as PDF attachments. The rest paste the CV into the email body, or send a Google Docs link, or attach a .docx from an old Word install.

The naive path is pdf-parse → text → prompt. That breaks on:

  • Two-column resumes that interleave columns line-by-line (Name Skills Email Python Phone JavaScript)
  • LinkedIn PDF exports with private-use Unicode block characters where bullets should be
  • Scanned PDFs — someone printed their CV, scanned it, attached the scan. Zero extractable text.
  • Pages exports with weird font embedding that comes out as \u0001\u0002 garbage

Here's the function node I actually use:

const attachments = $input.item.binary || {};
let text = '';
let source = 'body';

for (const key of Object.keys(attachments)) {
  const att = attachments[key];
  if (att.mimeType === 'application/pdf') {
    const buffer = Buffer.from(att.data, 'base64');
    const parsed = await pdfParse(buffer);
    text = parsed.text.replace(/\s+/g, ' ').trim();
    source = 'pdf';
    break;
  }
}

if (!text || text.length < 100) {
  text = $json.textPlain || $json.snippet || '';
  source = text.length < 100 ? 'unparseable' : 'body';
}

return {
  json: {
    candidate_email: $json.from,
    subject: $json.subject,
    cv_text: text,
    parse_source: source,
    parse_length: text.length,
    thread_id: $json.threadId,
  }
};

The parse_source: 'unparseable' flag is the important part. Anything under 100 characters skips the classifier and goes straight to a manual-review branch. Scoring garbage text as 2/10 and auto-rejecting a scanned CV is the worst possible failure mode — you can't get that candidate back.

The classifier prompt (Claude Haiku, structured JSON)

The classifier is Claude Haiku because it's cheap and fits a scoring rubric perfectly. You do not need Sonnet for this. The prompt does one thing: score the CV against four criteria and return JSON. No prose, no candidate summary, no "let me walk through this resume" filler.

You are a resume screener for a {role_title} position.

MUST-HAVES (score heavily):
- {must_haves}

NICE-TO-HAVES (bump score modestly):
- {nice_to_haves}

RED FLAGS (deduct heavily):
- Wrong country when role is on-site in {location}
- Cover letter addressed to a different company
- Copy-paste generic application with no role reference
- Obvious keyword stuffing without context

SENIORITY: The role is {seniority}. A significant mismatch (e.g. staff engineer
applying to junior role) should reduce the score.

Return ONLY valid JSON, no prose:
{
  "score": <integer 0-10>,
  "reason": "<one sentence, max 20 words>",
  "human_check": <true|false>,
  "flags": ["<optional short tags>"]
}

CV TEXT:
{{ $json.cv_text }}

Set the Claude node to response_format: json and max_tokens: 200. Haiku returns in ~2–4 seconds per CV. Average CV parses to ~800 tokens. On the 94-CV batch:

Metric Value
Total input tokens ~78,000
Total output tokens ~9,400
Wall clock 11 min
API cost $0.31
Cost per CV $0.0033

Compare that to $50/seat/month for the entry tier of a mainstream ATS. You'd need to hire for 200+ roles a year before the ATS math even starts to make sense on cost alone, and that's before you factor in the week you spend configuring it.

Routing: why score buckets beat a ranked list

The switch node has three branches, not a ranked list. Ranked lists sound smart in a demo and fall apart in practice, because they force a human to look at position 8 to decide if it's a maybe. Buckets give the founder a clear action per candidate.

  • Score >= 7 → Telegram ping. Candidate name, one-line reason, Google Drive link to the CV. Arrives in the same Telegram the team already uses. No dashboard, no unread counter, no login flow.
  • Score 4–6 → Airtable "review later" view. The maybes. Hiring manager batches through them once a week for ~15 minutes.
  • Score < 4 → Auto-archive + draft rejection. Gmail thread archived, polite rejection saved to Drafts. Never sent automatically. That's a human decision, always.

On the 94-CV run:

  • 7 candidates pinged to Telegram
  • 14 dropped into review-later
  • 73 auto-archived with rejection drafts ready to batch-send

The founder spent ~20 minutes total across the four days. Not four hours on a Saturday. The two strong candidates that would've been buried under referral spam got surfaced within an hour of applying.

Where this breaks — and the human-review gates

Every automation tutorial that skips this section is selling you something. Here's what actually goes wrong and the mitigations I ship with every deployment.

Unparseable PDFs. Older exports, Pages files, scans. Fix: the parse_length < 100 fallback routes them to a manual-review Airtable view with a flag. Never auto-reject an unparseable CV.

Referrals. Your CTO forwards a friend's CV with "hire this person." The classifier has no context and scores it 5. Fix: a separate referrals Gmail label that bypasses the classifier entirely and pings the hiring manager directly. Referrals get a human read, always.

Senior roles. The four-criteria rubric is fine for a junior role where must-haves are concrete (specific stack, years of experience). For a senior or staff hire, the nuance matters and the cost of a false negative is enormous. Fix: for senior roles, invert the routing — score >= 4 goes to human review, only clear spam (<4 with red flags) gets auto-archived.

Rejection emails. Never let an agent send rejections. Draft only. A wrong rejection to a strong candidate is a permanent reputation hit that costs you far more than the 30 seconds it takes to hit "send" on a batch of drafts.

Prompt drift. If your role description changes, re-run the classifier against a labeled set of 20 CVs you've hand-scored. Compare against the model's scores. If it's more than 1.5 points off on average, tune the rubric before you trust the next batch.

  • Add a labeled test set to Airtable and re-score on every prompt change
  • Log every classifier response with human_check: true for weekly review
  • Rotate the sample the hiring manager reads — pull 3 random rejections a week and verify the reason held up

Why bizflowai.io helps with this

The Gmail-to-triage pattern is one of the workflows I ship for small teams every month — hiring inboxes, sales inbounds, support routing, refund requests. Same shape: a labeled inbox, a parser that handles the messy edges, a small classifier prompt with structured output, an audit log, and a router that respects human review gates. The reason it works isn't the model — it's the boring parts most tutorials skip: the unparseable-file fallback, the referral bypass, the draft-not-send rule for anything that touches a candidate or customer. If you want this built for your inbox with your rubric, that's the day job.


Want more like this?

I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.

Subscribe to bizflowai.io on YouTube — never miss a new tutorial.

Planning an AI automation project or need a second opinion on your architecture?

Connect with me on LinkedIn — Lazar Milicevic, GenAI Engineer & bizflowai.io Founder.

Visit bizflowai.io for our services, case studies, and AI consulting.

Frequently asked questions

What is CV triage automation with n8n?

CV triage automation is a five-node n8n workflow that reads inbound job applications from Gmail, extracts CV text from PDFs or email bodies, scores candidates using Claude Haiku against a rubric, logs everything to Airtable, and routes results by score. It replaces manual resume skimming for small startups without needing a dedicated ATS, running on Gmail you already own for pennies per batch.

How do I score job applications automatically with Claude Haiku?

Send parsed CV text to Claude Haiku with a prompt covering four criteria: must-haves like tech stack or years of experience, nice-to-haves, red flags such as spam or wrong location, and seniority match. Instruct Haiku to return structured JSON containing a zero-to-ten score, a one-line reason, and a flag for items needing human review. Structured output only, so the next workflow node can parse it.

Why does application triage matter more than sourcing for small startups?

Small startups typically receive 90-plus applications per role within days, and the real bottleneck is reading each CV to decide whose time is worth pursuing, not finding candidates. Strong applicants get buried under referral spam and take other offers before founders finish skimming. Triage, not sourcing or scheduling, is the unglamorous work that causes small teams to miss good hires.

When should I auto-send rejection emails vs draft them?

Always draft rejection emails, never auto-send them. In this workflow, applications scoring below four get auto-archived and a polite rejection email is drafted in Gmail, but a human reviews and sends it. Sending rejections is a human decision because false negatives from parsing errors, like scanned PDFs or two-column resumes, can wrongly reject qualified candidates you would want to reconsider.

How much does automated CV triage cost per batch?

Running 94 applications for a junior full-stack role cost 31 cents total and took 11 minutes of wall clock time, mostly Claude Haiku API latency. Haiku input tokens are cheap and parsed CVs average around 800 tokens each. The workflow surfaced 7 strong candidates via Telegram, queued 14 for weekly review, and auto-archived 73 with rejection drafts ready for human approval.