I build HRSpeeder, an applicant tracking system (ATS) for small companies. This post
describes how our inbound e-mail routing works in production today, including what we deliberately left out.
The problem
Small companies still receive most applications by e-mail. A candidate writes "Application – Warehouse operator",
attaches a PDF, and sends it to a generic address. The recruiter then has to:
- save the CV somewhere,
- figure out which job it is for,
- create a record in a spreadsheet or an ATS.
At 2 minutes per e-mail, 200 applications for one job means more than 6 hours of pure data entry, before reading a
single CV. We wanted that number to be zero, without asking candidates to use a form.
The pipeline
Every organization gets a receiving address (apply+<tag>@inbox.hrspeeder.com). Our mail server hands each message to a
webhook, which:
- finds the organization from the
+tag, - routes the e-mail to one of the organization's active job offers (this post),
- stores the attachments and creates the candidate and the application,
- queues the CV for AI analysis (score against the job description, strengths, missing skills, summary).
Routing, step by step
We only consider the organization's active offers. If there are none, the application goes straight to the review
queue.
Step 1 — Title match on the subject
Most candidates put the job title in the subject. We normalize both strings (lowercase, accents removed, punctuation
stripped) and accept the offer if:
- the subject contains the normalized title (titles longer than 5 characters only, to avoid "dev" matching everything) → confidence 0.88, or
- the Levenshtein similarity is above 0.75 → confidence 0.85.
const distance = levenshtein(normalizedSubject, normalizedTitle); const similarity = 1 - distance / Math.max(normalizedSubject.length, normalizedTitle.length); if (normalizedSubject.includes(normalizedTitle) && normalizedTitle.length > 5) { return { offerId: offer.id, confidence: 0.88, method: 'TITRE_MATCH', requiresReview: false }; } Step 2 — Keyword score on subject + body
Each offer has a list of required skills. The score is the share of those skills found as words in the normalized
subject + body (3 of 5 skills → 0.6). The best offer wins if its score is above 0.4; its confidence is capped at 0.72,
which is below our 0.75 review threshold:
const confidence = Math.min(0.5 + score * 0.3, 0.72); return { offerId, confidence, method: 'KEYWORD_MATCH', requiresReview: confidence < 0.75 }; So a keyword match attaches the application but stores the inbound e-mail as NEEDS_REVIEW (instead of MATCHED),
with the method and confidence, so it can be checked later.
Step 3 — The review queue
Everything else is stored as UNCLASSIFIED, the recruiter gets an "unclassified e-mail" notification, and the
application waits in an "À classer" (to sort) list until someone picks the offer. No application is ever dropped or
auto-rejected.
What we left out (on purpose)
- A job reference regex (
ABC-1234). The extractor exists, but small companies rarely use references and candidates rarely copy them. It is not active. - An LLM fallback. The code path exists (a small model returning
{ offre_id, confidence }), but it is disabled in production. Title + keywords + a human queue were enough for our volumes, and sending every e-mail body to another processor is a GDPR question we did not need to open for routing.
The AI we do use is for the CV analysis itself, after routing, and its output is advisory: the score tells the
recruiter which CVs to read first. In the EU, AI that screens job applications is a high-risk use under the AI Act, so
the decision stays human by design.
Lessons
- Confidence is a product feature. Storing the method and confidence with each routed e-mail makes every decision explainable, and separates "matched" from "matched, needs review".
- Cheap heuristics first. Subject-contains-title catches the majority of real e-mails.
- Never lose an application. A visible queue beats a clever model that silently guesses wrong.
HRSpeeder is free to start (10 applications per month with AI analysis included). Plans, data hosting (EU) and FAQ:
https://hrspeeder.com/reponses

