Talking to candidates
One email thread per application, deterministic lifecycle messages, and an agent that classifies replies and only ever sends the narrow reply it is allowed to send.
The problem it solves
Candidate email is where hiring processes quietly fail. A reply lands in one recruiter's personal inbox and nobody else can see it. A question about the interview time sits for three days. A candidate asks about compensation and gets an off-the-cuff answer that becomes a problem later.
The design here is a choke point plus a containment boundary: one place sends mail, and the model classifies but never acts.
One thread per application
A thread belongs to an application, not to a candidate — the same person applying to two roles has two conversations, because "can we talk about the offer" means something different on each.
Every message on the thread is visible to whoever picks it up, and a recruiter replying by hand goes out through the same path the automation uses. That is what keeps the reply-to address, the threading headers and the delivery tracking identical whoever is typing; a recruiter replying outside that path would break the thread the whole inbound flow depends on.
The lifecycle emails
These are deterministic templates. No model writes them.
- Application received — consent was given on our own form.
- Consent request — the applicant applied somewhere we do not control. It carries the disclosure and the link.
- Consent reminder — before the consent link expires.
- Résumé unreadable — we could not read the file, with the specific reason.
- Screening invitation — the résumé cleared the invite bar. It carries the link, the accommodation route and the human-review route.
- Interview reminder — before a screening link expires.
- Interview confirmed — a time was booked.
- Screening rejection — the résumé score fell below the reject bar and the rejection was automatic. It says so and carries a human-review link.
- Pick a time — the candidate reached a round a person runs and somebody is assigned to interview it. Nobody assigned means no email until somebody is.
- Application not going forward — a person rejected the candidate and left Email the candidate ticked. No reason, no score, no review link, because the decision was a person's and not the system's.
A rejection is the most legally exposed sentence this product writes. Generating it with a model buys no variation and adds a hallucination surface to the one message a regulator asks about.
An email missing a detail it needs, such as the job title, is not sent half filled; it fails before it reaches anyone's inbox as "your application for ".
The sending guarantees
Every email, automatic or typed by a person, goes out through one place, and the guarantees live there:
- Do-not-contact is checked on every send. It is permanent and per person, and there is no other way to send that could skip it.
- Each automatic email is sent once. A retry after a hiccup does not send a second confirmation, on our side or our email provider's.
- An email that cannot be filled in is not sent.
Delivery outcomes are tracked, and the distinction between a permanent failure (an address that will never exist) and a transient one (a rate limit, a timeout) is the retry policy: permanent failures are recorded and stop, transient ones back off and eventually raise an alert.
The email agent
When a candidate replies, an agent reads the reply and decides what kind of message it is. It classifies; it never acts.
The model's entire output is one decision: an intent, a confidence, an action, and at most a choice from a list of times we supplied. Our own code turns that into one of a few fixed actions on this thread only. Nothing the model says can move a candidate, change a score or a threshold, or reach another candidate. An injected instruction telling it to advance someone to an offer is not one of the allowed intents, so the thread escalates to a person.
What it may answer by itself
Five intents, and only these: confirming a scheduled time, requesting a reschedule, asking for a fresh link, asking about status, and asking a logistics question.
Everything else escalates to a person: compensation, legal or policy, visa or immigration, complaints and appeals, offers and negotiation, withdrawal, and anything it cannot classify. Some escalations are sticky — once a thread is a compensation conversation, a follow-up inside it is still part of that conversation however logistical it looks on its own.
The gate is not the model's confidence
Before anything is sent automatically, every one of these conditions has to pass, and most are checked without the model. A model under injection reports high confidence about a message telling it to book an interview for someone else, so confidence is one condition among many:
- the message is not an out-of-office or mailing-list autoresponder,
- the agent is enabled for this organization, and autosend specifically is enabled,
- the application is still open (a reply after it closed goes to a person),
- the thread has not been escalated for a sticky reason,
- the thread is still in the agent's hands,
- the thread is under its reply cap,
- the sender address matches the candidate this thread is about (the sender line of an email is easy to fake, and a forwarded thread looks the same),
- the intent is one of the five,
- confidence clears the bar,
- "my interview" is unambiguous — there is exactly one live interview it could refer to,
- and the chosen action actually has what it needs to run.
The first condition that failed is recorded, so "why didn't it reply?" has an answer weeks later.
Autosend is separately switchable from the agent itself, deliberately: the point of a rollout period is that the agent classifies and drafts for a while before it is allowed to send, and a team cannot watch it propose if it never runs.
What actually reaches a candidate
Auto-sent bodies are our own template. The model chooses which of our times and which of our links; the surrounding sentences are written in code.
The one exception is a plain question answered in prose, where the model's own sentence is scrubbed first: a length cap, no URLs outside our own domain, no email addresses or phone numbers, and none of the compensation, visa or offer vocabulary that must always reach a person. Prose that fails the scrub is not trimmed into shape — it escalates. Editing a sentence until it passes is how you ship half of what the model meant to say.
Prompt-injection containment
- Only the candidate's new words are ever prompted. The quoted reply chain is stored — a recruiter may need to read it — and never shown to the model, because an injection planted once in a footer would otherwise replay on every subsequent turn of the conversation.
- What is prompted sits inside a fenced block labelled as data, and the fence markers are stripped out of the candidate's text first, so a candidate cannot close the block by typing its closing marker.
- A slot index outside the range we supplied discards the whole decision, not just the action. A model that invented an index has demonstrated it is not reading what we sent it.
- The never-ask topics are composed into the agent's prompt from the same single list the AI interviewer uses. The agent refuses to raise or answer any of them and escalates the thread instead.
Withdrawal
If a candidate says they want no further contact, the agent can set a permanent do-not-contact flag on the person — the conservative direction: automation stops, a recruiter is paged, and they can still write by hand or clear it. A withdrawal on its own does not set it, because a withdrawal is per application and the flag is per person.
Scheduling
Times offered to candidates come from a real availability engine that is timezone aware and already subtracts the panels an interviewer is booked on. If the organization has connected an external scheduling tool, its slots and bookings are used instead. An interviewer who connects their Google or Microsoft 365 calendar also has its busy times left out, and a booked round is put on it (see Integrations).
When a candidate advances into a round a person runs, they are emailed a link to pick a time. A round with several interviewers offers only the times when every one of them is free, and the chosen time is checked again when the candidate confirms it, so a time somebody else took in the meantime is refused rather than double-booked. The external scheduling tool is used only when one person interviews: a panel is booked on the built-in engine. A round with nobody assigned goes to a recruiter instead of offering times.
The fallback is the feature. An organization that has never connected anything still receives a working invitation with real times. And every failure to get a usable answer from the external tool — a revoked key, a deleted event type, a timeout, a server error — falls back to the built-in engine and logs it, rather than telling a candidate there are no times available. Only booking and cancelling raise, because silently not booking is a much worse lie.
The AI screening interview books nothing externally: there is no interviewer to hold an hour for and no room to reserve.
Unrouted mail
Mail that reaches us without a valid reply address cannot be tied to any company, and we never guess which one it belongs to. It is kept out of every company's inbox.
What the user does
- Read and reply from the shared inbox. Replies thread correctly automatically.
- Decide whether the agent runs at all, and separately whether it may send.
- Watch the escalation queue. Escalation is the default outcome, by design.
- Connect a scheduling tool if you want external calendar bookings; otherwise do nothing and the built-in engine is used.
Limits and edge cases
- A reply after the application closed reaches a person, not the assistant. The conversation reopens in the inbox's Needs you, marked as a reply after the application closed. The assistant never reads or answers it. A request for a human review is still best made through the link in the rejection email, which puts it in Needs you as a review request.
- The agent never moves anybody. It cannot advance, reject, score or re-threshold a candidacy, and it cannot reach another candidate's thread.
- Escalation is not a failure state. It is the intended outcome for most of the intents, and for anything ambiguous.
- An unroutable inbound message is acknowledged and dropped, not bounced — answering it with an error turns one stray email into a retry flood.
- A reply's contents appear a moment after it arrives, because its body and attachments are fetched separately.