Build a Human-Approved Applicant Router

One shared hiring inbox can lose a qualified applicant before anyone opens their CV. If your five-person team is juggling applications, referrals, portfolios, and follow-up emails in Gmail, you don't have a recruiting system — you have an unread-email problem. Here's exactly how to turn that inbox into a daily applicant queue that extracts details, compares them to a role checklist, and sends one decision card to a manager.
The Real Problem: Unstructured Intake, Not Applicant Volume
The problem isn't that small teams have too many applicants. It's that applications arrive through too many unstructured paths. Someone applies by email. A friend forwards a referral. Another person sends a portfolio link with no CV. A manager flags an email, plans to review it later, and then that email disappears below invoices, client requests, and calendar notifications. A week later, nobody knows whether the candidate was rejected, waiting for a reply, or simply missed.
That creates a bad candidate experience, but it also creates internal risk. Manual copy-paste into a spreadsheet produces missing records. Decisions sit in private inboxes. When someone asks why a candidate was archived, there's no clean audit trail.
Most existing options fail small teams for two reasons:
- ATS migration is too heavy when you're hiring occasionally. Tools like Greenhouse, Lever, or Workable are built for companies running multiple open roles with dedicated recruiters. For a team hiring one or two people per quarter, the overhead of configuring job pipelines, custom stages, and user permissions exceeds the time saved.
- Generic AI tools can summarize a CV but don't connect anything. ChatGPT can read a resume and tell you the candidate has three years of support experience. It can't watch your Gmail, extract fields into a structured format, compare them against your role requirements, draft a follow-up, and post a decision card to your team channel.
The useful version is smaller and more honest: a router that prepares information while a human stays responsible for every hiring decision and every candidate communication.
Step 1: Create One Gmail Intake Label (Narrow Scope)
Create a single Gmail label called Applicant Intake. Every application, referral, portfolio email, and forwarded candidate goes there. Set up Gmail filters to auto-apply the label for:
- Job-board notification emails (LinkedIn, Indeed, etc.)
- Messages sent to
careers@yourcompany.com - Emails with common subject-line keywords:
application,CV,resume,portfolio,job application
For referrals that arrive from a manager's personal inbox, the rule is simple: apply the label manually before the email gets buried.
This label is the only inbox the automation watches. Do not point it at all of Gmail. Client emails, invoices, newsletters, and random attachments should never enter a hiring workflow by accident. Narrow intake is the first reliability feature — it prevents the system from ever touching communications it has no business reading.
Here's the Gmail filter configuration that works:
Matches: (subject:"application" OR subject:"CV" OR subject:"resume"
OR subject:"portfolio" OR to:careers@yourcompany.com)
Do this: Apply label "Applicant Intake", Skip Inbox
Skipping the inbox is intentional. The email lands in the label only, keeping the main inbox clean. The automation polls the label on a schedule, processes new threads, and removes the label (or applies a secondary Processed label) once extraction is complete.
Step 2: Write the Role Checklist Before You Touch AI
Write a short, structured checklist for one open role — not a vague prompt like "find the best candidate." Each requirement must be explicit. Here's the format I use, stored as a JSON file or a single Google Sheet row:
{
"role_title": "Customer Support Specialist",
"work_eligibility": "Eligible to work in the US OR available for remote contract",
"min_experience": "2 years customer-facing support",
"required_skills": ["written English"],
"portfolio_or_cv": "CV or LinkedIn profile required",
"availability": "within 30 days",
"salary_expectation": "if candidate provided it; otherwise 'not provided'"
}
The critical rule: if a field is not present in the application, the system records not_provided. It never invents a salary number, never guesses work eligibility from a phone number, never infers availability from a vague statement like "looking for new opportunities soon." not_provided is a valid state. It triggers a follow-up request, not a rejection.
This checklist becomes the extraction schema and the comparison logic in one document. When you change roles, you swap the checklist file. The router code doesn't change.
Step 3: Extract and Classify — Three Routes Only
The router reads each new labeled email and extracts exactly the fields defined in the checklist. It captures:
- Sender name and email
- Role applied for
- Location or work eligibility
- Relevant experience (with short evidence quote)
- CV or portfolio link
- Availability
- Salary expectation (when present)
- Original Gmail message ID (for audit trail)
It also stores brief evidence from the email — not a summary, but a direct quote. For example: "3 years handling B2B support tickets at [Company]" or "Portfolio linked in email signature." This is where most AI recruiting workflows become risky. They ask a model to decide whether a person is "good enough." Don't do that. The model extracts evidence into a fixed schema and compares it against the checklist. It can flag a requirement as one of four states:
| Status | Meaning | Example |
|---|---|---|
clearly_met |
Stated evidence directly satisfies the requirement | "I have 4 years of customer support experience" (req: 2 years min) |
unclear |
Partial evidence, needs human judgment | "Worked in client services" (req: customer-facing support) |
missing |
Requirement field is entirely absent from the email | No availability mentioned |
not_met |
Clearly outside a stated non-negotiable | Applied for a designer role, you're hiring support |
Unknown is not rejection. If the email doesn't mention work eligibility, the router returns missing for that field and routes accordingly.
The Three Routing Outcomes
The routing logic has exactly three outcomes — no more, no less:
Shortlist — Candidate has enough stated evidence for a manager to review. All core requirements are
clearly_metorunclear(human judgment needed). Missing fields are non-critical.Request Missing Information — Email lacks a required detail (availability, work eligibility, working portfolio link). The system creates a follow-up draft — it does not send it automatically. The draft uses a simple template:
FOLLOW_UP_TEMPLATE = """
Hi {candidate_name},
Thanks for your application for the {role_title} position.
We're missing one detail: {missing_field_label}
Could you confirm: {missing_field_question}
Thanks,
{manager_name}
"""
A manager can edit the wording, check the context, and send it only when it makes sense. This human stop prevents the system from auto-rejecting candidates whose emails were brief but otherwise promising.
- Archive — Candidate is clearly outside a non-negotiable requirement that the team wrote down in advance. Applied for a completely different role. Stated they're unavailable for a required location or time frame. This route still gets logged with the reason, so there's an audit trail.
Step 4: The Decision Card — Telegram or Slack
For every extracted applicant, the router sends one compact message into a private hiring channel. I use Telegram for this because the Bot API is lightweight, but Slack works identically. The card shows:
👤 Candidate: Sarah Chen
📋 Role: Customer Support Specialist
📧 s.chen@email.com
Evidence Summary:
• 4 years B2B support at TechCorp (clearly_met)
• Based in Austin, TX, US citizen (clearly_met)
• Portfolio: linkedin.com/in/sarahchen (clearly_met)
• Availability: "can start in 2 weeks" (clearly_met)
• Salary: not mentioned (missing)
Route: SHORTLIST
🔗 Original email | 📄 Extracted data
Under the card are three inline buttons: Shortlist, Request Details, Archive. The manager taps one. The system records the decision, updates the applicant record, and moves on. No opening Gmail. No reconstructing the story from five messages. No copy-pasting into a spreadsheet.
Here's the Telegram card builder in Python:
def build_decision_card(applicant: dict) -> dict:
"""Build a Telegram message with inline keyboard for hiring decisions."""
evidence_lines = []
for req, status in applicant["requirements"].items():
icon = {"clearly_met": "✅", "unclear": "❓",
"missing": "⚠️", "not_met": "❌"}[status]
evidence_lines.append(f"• {req}: {icon} {status}")
card_text = (
f"👤 *Candidate:* {applicant['name']}\n"
f"📋 *Role:* {applicant['role']}\n"
f"📧 `{applicant['email']}`\n\n"
f"*Requirements:*\n" + "\n".join(evidence_lines) + "\n\n"
f"*Route:* {applicant['recommended_route']}\n"
)
keyboard = {
"inline_keyboard": [[
{"text": "Shortlist", "callback_data": f"decide_{applicant['id']}_shortlist"},
{"text": "Request Details", "callback_data": f"decide_{applicant['id']}_request"},
{"text": "Archive", "callback_data": f"decide_{applicant['id']}_archive"},
]]
}
return {"text": card_text, "reply_markup": keyboard}
The callback handler on the bot side records the decision to a simple database (SQLite or Google Sheet) with: applicant ID, decision, decision maker, timestamp, and original Gmail thread ID. That's your audit trail. When someone asks six months later why a candidate was archived, the answer is one query away.
What This System Costs to Run
Real numbers from my setup, processing approximately 40-60 applicants per role:
| Component | Monthly Cost | Notes |
|---|---|---|
| Gmail API | $0 | Free tier covers polling |
| Extraction (Claude API, ~800 tokens/email) | ~$1.20 per 50 applicants | Sonnet class model |
| Telegram Bot API | $0 | Free |
| SQLite (local) or Google Sheets | $0 | Free tier |
| Hosting (home server / WSL) | $0 | Already running |
Total per role filled: roughly $2-4 in API costs. The biggest investment is the initial build — roughly 4-6 hours for a working version including the role checklist parser, Gmail poller, extraction prompt, routing logic, and Telegram card. After that, swapping roles is a 10-minute checklist file change.
What Breaks (and How to Handle It)
Three failure modes I've seen in production:
Emails with attachments only and no body text. The router needs to parse PDFs or DOCX files. Add a document-extraction step before the LLM call —
pdfplumberfor PDFs,python-docxfor Word files. If extraction fails, route toRequest Missing Informationwith the reason "couldn't read attachment, please paste CV inline."Forwards and reply chains. A manager forwards a referral email. The router sees the forwarded content buried in a thread with 15 previous messages. Solution: extract only the newest message body, or look for
---------- Forwarded message ----------markers and take the section after them.Candidates who apply for multiple roles. Same email, three different positions. The router checks the role title against the active checklist. If it doesn't match, it extracts the stated role and holds the email for manual routing rather than guessing.
How This Fits Into a Broader Workflow
The applicant router is one piece of a larger pattern: turning unstructured email inputs into structured, actionable outputs with human approval gates. The same architecture works for:
- Lead intake — parse inquiry emails, score fit, route to sales
- Support triage — extract issue type and urgency, assign to the right person
- Invoice processing — extract vendor, amount, and due date from emailed PDFs
The principles are identical: narrow intake label, structured extraction schema, fixed routing outcomes, human decision card, audit trail.
Where bizflowai.io Comes In
At bizflowai.io, I build these routers end-to-end for small teams that need them working in production, not as demos. The applicant router above is a stripped-down version of what I've deployed for clients — complete with the Gmail polling service, the extraction pipeline, the role-checklist management interface, and the Telegram decision-card bot. The full system handles multiple simultaneous roles, tracks applicant status changes over time, and logs every decision for compliance. If you want it running on your inbox without the 6-hour build, that's what I do.
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
How do I set up an AI hiring workflow for a small team?
Create a single Gmail label called Applicant Intake and route all applications, referrals, and portfolio emails there using filters for job-board notifications, careers@ messages, and common subject lines like CV or resume. Write a role checklist with explicit requirements such as title, location, experience, and availability. Point an AI router at that label only to extract candidate data into a fixed schema, compare it against your checklist, and produce one of three outcomes: shortlist, request missing information, or archive.
Why does narrow intake matter for AI-assisted recruiting?
Narrow intake prevents client emails, invoices, newsletters, and random attachments from entering your hiring workflow by accident. By limiting the automation to a single Gmail label rather than all of Gmail, you ensure only candidate-related emails are processed. This is the first reliability feature of the system. Without it, unrelated messages would be parsed as applications, producing incorrect data, false rejections, and a broken audit trail that undermines trust in the entire workflow.
When should I use a lightweight AI router vs a full ATS?
Use a lightweight AI router when your team hires occasionally and a full ATS migration is too heavy. An ATS makes sense for organizations with continuous, high-volume hiring that need structured pipelines, multi-stage interviews, and reporting dashboards. A small team that gets applications through scattered email paths needs a router that extracts candidate information, compares it to a role checklist, and prepares decisions while a human stays responsible for hiring. The router integrates with Gmail without requiring a platform migration.
What is the safest way to use AI for candidate screening?
Use AI to extract evidence from applications into a fixed data schema and compare it against a pre-written role checklist. The model should flag each requirement as clearly met, unclear, missing, or not met based only on stated evidence. Do not let the AI decide whether a candidate is good enough. If the email omits work eligibility or availability, the system returns unknown, not a rejection. A human reviews every shortlist and makes all final hiring decisions.
Why do small teams lose track of job applicants?
Applications arrive through unstructured paths including direct email, forwarded referrals, portfolio links without CVs, and manager-flagged messages. Without a central intake point, emails get buried under invoices, client requests, and calendar notifications. Manual copy-paste into spreadsheets produces missing records, decisions sit in private inboxes, and there is no clean audit trail. Within a week, nobody knows whether a candidate was rejected, waiting for a reply, or simply missed.