60 Applicants, 4 Minutes: The Inbound-Only Hiring Filter

monday.com just spent millions on an ad claiming AI will source candidates for you. If you run a business under 10 people, that's the wrong problem. You already have 74 applicants sitting in Gmail from one LinkedIn post, and zero time to read them.
You don't need more candidates. You need a filter. Here's the exact n8n + Claude + Telegram pipeline I shipped for a 3-person agency hiring their first VA — no ATS, no monthly SaaS, no outbound spam.
The math that kills founder time
A 3-person agency posted one VA role on LinkedIn and two job boards. Within 72 hours, hiring@ had 87 threads. Some had CVs attached, some had Loom links, some were one-liners asking if the role was still open.
Manual review at 90 seconds per applicant is roughly 2 hours 10 minutes of founder time — and that's before the second read to actually pick who gets an interview. Multiply by three roles a year and you've burned a full work week on triage that produces almost no strategic value.
The pitch from every ATS ad is that the fix is AI sourcing. It isn't. Sourcing is what agencies do when they have zero inbound. If you're posting on LinkedIn and getting 60–120 replies, sourcing is not the bottleneck. Reading is. So we skip the entire outbound half of the recruiting stack and build only the inbound filter.
Here's what the filter costs at real volume:
| Stage | Manual | This pipeline |
|---|---|---|
| Intake (74 applicants) | 0 min | 0 min (Gmail auto-collects) |
| Extraction (PDFs → text) | 30 min | ~40 sec |
| Scoring against brief | 110 min | ~3 min |
| Ranking + shortlist | 20 min | instant |
| Founder decision surface | scattered | 1 Telegram message |
| Total founder time | ~2h 40m | ~4 min |
Compute cost for the full 74-applicant batch on Claude: $0.31.
Step 1: intake surface (do not overbuild)
You do not need Greenhouse. You do not need Workable. You do not need an ATS subscription.
You need one Gmail alias — hiring@yourdomain — and one Google Form linked in the job post. The form asks four things:
- Name
- Link to CV or portfolio
- One paragraph on why this role
- One paragraph proving they read the post
That last field is the single highest-signal filter you will ever add to a hiring funnel, and it costs $0. It filters mass appliers who spray the same cover letter across 40 postings a day. Every form submission triggers an email to hiring@. That's your entire intake layer.
Applicants who ignore the form and email directly still land in the same inbox — the workflow handles both paths.
Step 2: the n8n trigger and extraction path
Open n8n, drop in a Gmail Trigger node, point it at hiring@, filter by a label or subject-line match on the job title. Poll every 15 minutes. When a new thread arrives, the workflow grabs the sender, the body, and any PDF attachment.
Branching logic:
- PDF attached → pass through a PDF-to-text node so Claude reads it as plain markdown
- Google Form response → fields already structured, skip extraction
- Plain-text email → strip signatures, pass body through as-is
[Gmail Trigger] → [IF has_attachment?]
├─ true → [Extract from File (PDF)] → [Merge]
└─ false → [Set: body_only] → [Merge]
↓
[Function: normalize to markdown]
↓
[Claude scoring node]
Keep the normalized applicant object small. Name, email, one string of markdown, one field for "form_filled: true/false". That's it.
Step 3: the YAML scoring brief nobody writes
This is the piece most people skip, and it's the reason their AI screener produces garbage. Before you write a single prompt, you write a YAML file called role_brief.yaml and store it as a static input in the workflow.
role: Virtual Assistant (Operations)
must_haves:
- fluent written English (native or C2 level)
- timezone overlap of at least 4 hours with US Eastern (13:00–17:00 ET)
- prior experience with a shared inbox tool (Front, Missive, Help Scout, or similar)
- form_filled equals true
nice_to_haves:
- familiarity with Notion or ClickUp
- invoicing or bookkeeping experience
- portfolio link that loads and shows real client work
- written proof they read the job post (specific reference, not generic praise)
hard_reject_signals:
- one-line email with no CV
- CV attached but no cover paragraph
- obviously AI-generated cover letter with placeholder text
Every applicant gets scored against this exact file. That means ranking is consistent across the whole batch — the 60th applicant is evaluated the same way as the 1st. Change the role? Change the YAML. Don't touch the prompt.
Step 4: the Claude call (strict JSON, no invention)
The prompt is short and strict. That matters more than being clever.
SCREENER_PROMPT = """You are a hiring screener. Score one applicant against the role brief.
ROLE BRIEF (YAML):
{role_brief}
APPLICANT (markdown):
{applicant_markdown}
Return ONLY a JSON object with these fields:
{
"hard_fail": boolean,
"score": integer 1-10 across nice_to_haves,
"one_line_summary": string, max 20 words,
"missing_must_haves": [list of strings]
}
Rules:
- If any must_have is missing or unclear, hard_fail is true and score is 0.
- Do NOT invent qualifications the applicant did not explicitly state.
- Do NOT give credit for skills merely implied by a job title.
- If the applicant claims a skill without evidence, note it in the summary.
"""
That "do not invent qualifications" line is not decorative. Without it, Claude will assume a candidate who says "worked at a marketing agency" knows Notion, invoicing, and shared-inbox workflow. You'll interview people who cannot do any of it.
Call it with claude-sonnet-4 (or Haiku if cost matters more than nuance). Set temperature to 0. Force JSON mode.
Why hard_fail is a separate boolean
- Score is a spectrum — useful for ranking
- Hard_fail is binary — useful for routing
- Mixing them into "score 0-10 where 0 means fail" gives you inconsistent behavior at the boundary
Step 5: ranking + the top-8 rule
After Claude returns JSON for every applicant, a Function node splits them into two buckets.
const results = items.map(i => i.json);
const rejected = results.filter(r => r.hard_fail === true);
const shortlist = results
.filter(r => r.hard_fail === false)
.sort((a, b) => b.score - a.score)
.slice(0, 8);
return [
{ json: { bucket: 'shortlist', candidates: shortlist } },
{ json: { bucket: 'rejected', candidates: rejected } }
];
You keep the top 8. Eight because it fits in one Telegram message without scrolling, and because interviewing more than 8 people for one role is a sign your brief is too vague — not that you have too many good candidates. If your top 8 all cluster at score 6, tighten nice_to_haves. If they cluster at 9, loosen them.
Step 6: the Telegram digest with inline buttons
At 6pm every day, n8n fires a Telegram sendMessage call to the founder's chat. Eight lines, one per candidate, plus three inline buttons under each.
📥 Today's shortlist — 74 applicants, 8 shortlisted
1. Maria K. — 9/10 — 6 yrs VA, Front + Notion, US timezone
[Interview] [Reject] [Hold]
2. James O. — 8/10 — bookkeeping background, referenced 3 lines from post
[Interview] [Reject] [Hold]
...
- Interview → triggers a second workflow that drafts a scheduling email with the founder's Calendly link
- Reject → moves applicant to rejection queue
- Hold → logs decision, does nothing else
Total founder input on a batch of 74: 8 taps, roughly 4 minutes.
Step 7: the fail-safe (never auto-send rejections)
Rejection emails are drafted, never sent. The rejection workflow creates a Gmail draft in hiring@ using a short, human template. The founder opens Gmail once a week, skims the drafts, and hits send on the batch.
Subject: Update on your application — [Role]
Hi [Name],
Thanks for applying for the [Role] position. We received a strong pool
this round and won't be moving forward with your application.
Wishing you the best in your search.
— [Founder]
This one design choice — human-in-the-loop on all outbound — is what separates a system a client actually runs from one that gets turned off after the first embarrassing auto-reply. It costs 5 minutes a week. It prevents the day your screener rejects the founder's cousin.
What breaks if you skip the fail-safe
- Claude misreads a PDF where the CV is an image and rejects a qualified candidate silently
- A form validation edge case marks
form_filled: falseincorrectly - The role brief has a typo in a must-have and rejects everyone
- Your reject template renders a Jinja variable literally as
{{name}}
Every one of those has happened to me. Every one was caught by the founder scanning drafts on Friday morning.
The live run: 74 applicants, 3m 40s of compute
Job post went up Monday morning. By Thursday evening, 74 applicants had submitted. The workflow processed the full batch in 3 minutes 40 seconds of compute across all Claude calls. The Telegram digest arrived at 6:02pm. Founder tapped through 8 candidates in under 4 minutes. Three got interview drafts, four got reject drafts, one went to hold.
Total spend on Claude for the batch: $0.31. Total monthly SaaS cost of the stack: $0 (self-hosted n8n on a $6/mo VPS the client already runs). Compare to Workable at $149/mo minimum or Greenhouse starting north of $6,500/yr.
For a company hiring 3 roles a year, the annualized cost of this pipeline is roughly $1 in API calls. The ATS you were about to buy costs $1,788–$7,000+.
Why bizflowai.io helps with this
This is the exact class of workflow I ship for small teams weekly — the inbound triage layer that sits between a public-facing form or inbox and a human decision. Most of my client builds follow the same shape: a narrow YAML brief, a strict Claude prompt with JSON output, a ranking function, and a single approval surface (Telegram, Slack, or Gmail drafts). The value isn't the AI — it's the discipline of never letting the model send anything a human didn't approve.
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 the real bottleneck when a small agency posts a job on LinkedIn?
For a small agency getting 60 to 120 replies within 72 hours of posting, the bottleneck is not sourcing candidates but reading them. Manual review at 90 seconds per applicant takes about 2.5 hours of founder time per role, and multiplied across three roles a year that's a full work week spent on triage. The fix is an inbound filter, not AI sourcing tools.
How do I build an AI-powered applicant screening workflow in n8n?
Use a Gmail alias (hiring@yourdomain) plus a Google Form as intake. Add an n8n Gmail Trigger polling every 15 minutes, extract PDF attachments to text, then send each applicant plus a YAML role brief to Claude. Claude returns JSON with hard_fail, a 1-10 score, and a one-line summary. A Function node sorts survivors by score, keeps the top eight, and Telegram delivers a daily digest.
Why does a YAML role brief matter for AI candidate scoring?
A role_brief.yaml file with must_haves and nice_to_haves sections ensures every applicant is scored against identical criteria, making rankings consistent across the entire batch. Must-haves trigger a hard_fail if missing (like timezone overlap or language fluency), while nice-to-haves generate the 1-10 score. Without this static brief, scoring drifts between candidates and the ranking becomes unreliable.
Why should the Claude screening prompt forbid inventing qualifications?
The prompt must explicitly state: do not invent qualifications the applicant did not state. Without this instruction, Claude gives candidates credit for skills merely implied by their job titles, causing founders to interview people who cannot actually do the work. The model should only score against what the applicant explicitly wrote in their CV or form response.
When should I use a Google Form instead of an ATS like Greenhouse or Workable?
Use a Google Form when you're a small agency running inbound-only hiring with a few roles per year. An ATS is overbuilt for this volume. A form asking for name, CV link, why this role, and proof they read the post captures the highest-signal data at zero cost. Reserve ATS subscriptions for teams doing continuous, high-volume, or outbound-sourced hiring.