After-hours voice agent
A phone agent that answers what the office cannot, books the routine job and pages the owner on the emergency
Ten calls you can play yourself
The demo runs the shipped build: the same rendered system prompt, sha256 abd2007c1814, which is the hash the deployed assistant reports for its own; the same tool schemas, the same four n8n workflows and the same PostgreSQL constraints. Only the phone leg stands in, because there is no telephony account on the demo host. Every card states what has to hold before you run it, then shows the transcript, every tool call and answer, the guardrail verdicts and the database rows behind them. Policy checks are decided by the workflow and the database. Behaviour checks are the model's and are labelled as such, including the phrasing slips the source suite counts rather than fails on.
The classification is the agent's, the decision is not
Every detail the caller gives goes to one tool with a call_type on it: emergency, routine, out_of_area, spam or unknown. That value starts the page. Whether a page goes out is decided in the workflow, on the caller's own words, the caller id and a database claim.
VERIFY: Run Emergency, owner paged, then Caller speaks Spanish and watch the same tool refuse to pageThe agent may only say what a tool handed it
Safety lines, the out of area refusal, the paged line and the booked time are strings the backend returns. The model has no price list and no schedule in its context, so a price or an arrival time is not something it can leak.
VERIFY: Run Price pressure, four times. The detector that grades it is the one the source suite usesA detail the caller never gave is not a detail
The office does not take the agent's word for a name, an address or a callback number. It reads the caller's own turns, from the transcript on the request or from its own record of what they said, and keeps the value only if their words carry it. A field the model filled in for itself reaches no record, no service area check, no page and no booking.
VERIFY: Run Caller cannot give an address and read the log_detail answers: on_file stays empty for anything the caller did not sayNothing is announced that the system does not do
A confirmation text is promised only when the booking tool answered that one is going out. The technician is reported as paged only after a tool said the page went out, in any tense.
VERIFY: Run Routine job, booked and compare the spoken line with the message the stand-in carrier is holdingThe office hours decide the schedule, not the model
Slots are computed from the opening hours on the client row in the caller's timezone. A caller asking for tonight at 2am is offered the next open window and never a time the office is shut.
VERIFY: Run After hours, asked for tonight and read the offered slots against the opening hoursA replayed webhook cannot page twice or double book
One escalation per call id and one booking per client and slot, both on unique indexes. The resend counter is claimed with a conditional update, so only one execution can send.
VERIFY: Run Replayed end-of-call report. It posts the same report twice on purposeA failure degrades the call instead of ending it
With the backend refusing connections the agent says once that a dispatcher will confirm, never asks the caller to hold and books nothing. The end-of-call report writes the pending record afterwards.
VERIFY: Run Backend unreachable mid-callEvery tool call is in an append-only audit log
voice.tool_calls keeps the arguments, the response and the duration of every call the agent made. Update, delete and truncate are rejected by trigger.
VERIFY: Open the PostgreSQL evidence panel under any resultThe next business is a config swap
Business name, persona, service area, opening hours, emergency rules, safety lines, escalation number and notification settings come from one JSON spec per client. The prompt and the tool set are rendered from it.
VERIFY: Download the client spec and the rendered prompt from the demo- A caller rings the business number. The telephony provider routes it to the voice platform, which runs the transcription, the model and the speech.
- The agent sends the caller's own first sentence to a tool with a call_type on it. The tool is an n8n webhook on the same host as the database.
- The workflow matches the caller's words against the safety instructions on the client row and hands back the line to say. The model never picks a safety line.
- Each further detail goes across as the caller gives it, so the office keeps the record even if the line drops. Every call is written to an append-only audit log.
- On an emergency the workflow decides the page: the call type has to have been on the call before the detail that completes it, the caller's own words must not contradict it, and the number paged is the caller id.
- The page is claimed with an insert on a unique index, then sent. Only after the tool answers that it went out may the agent tell the caller.
- On a routine job the address is judged against the service area in SQL, the schedule returns open slots from the opening hours on the client row, and the booking is an insert on a unique index over client and slot.
- When the line goes down the platform posts the end-of-call report, which writes the call row, repairs a booking the in-call tool never landed, checks the carrier and resends the page once if it did not go.
- Anything that throws inside those workflows lands in an alerts table through a separate error workflow rather than disappearing.
Fictional business, synthetic callers, and one request that leaves the host.
- Cedar Ridge Heating and Plumbing does not exist. The name was checked against Michigan HVAC and plumbing businesses before it was used.
- Every phone number on the demo stack is in the +1 555 range reserved for fiction.
- The carrier on the demo is a stand-in service on the internal Docker network. No message reaches a handset and no message leaves the host.
- n8n, PostgreSQL and the stand-in are not published. The console is the single public surface.
- The one request that leaves the demo host is the model call, which carries the system prompt and the scripted caller lines.
- The demo database resets to its seeded state every night at 04:00 CET.
An after-hours line fails in one of two ways. Either it rings out and the job goes to whoever picks up first, or an assistant answers confidently, quotes a number it invented and promises a van that is not coming. This build takes the second risk seriously: the model is allowed to listen, classify and speak lines the office wrote, and nothing else.
The agent greets the caller as the business, takes their own first sentence as the problem, and asks for the name, the service address and the callback number one at a time. Every answer goes to the backend as it arrives, so the office keeps the details even if the line drops. It separates an emergency from a routine job on rules that belong to the business, in the business's own words, not on the model's judgement about what sounds bad.
Everything after the classification is deterministic. The safety line is matched to the caller's own words in the workflow, because given a list in its prompt the model reached for the first entry and told a burst pipe caller to evacuate. The page is sent by the backend rather than the agent, because with the send left to the model it narrated a page it had never made on about one call in eight. The number the owner is given is the line the call came in on, not one the model typed. A name, an address and a callback number are checked against the caller's own turns before the office keeps them, because an agent asked to quote the caller will quote a detail nobody gave. The service area is judged in SQL. Slots come out of the opening hours on the client row. A price and an arrival time are not guarded by a prompt line, they are absent from the model's context.
This is a reference build on my own accounts against a public brief, with a fictional business and synthetic callers. What is proven live is written down next to what is not: fourteen real inbound calls with measured turn latency and interruption behaviour, an emergency page that reached the owner's handset while the caller was still on the line, and a text-mode suite that grades 32 conversations across 12 scenarios on mechanical rules and passed every one of them on the delivered commit. What has not been measured still says so on this page. The demo renders the same system prompt, byte for byte, as the assistant behind the live number, and runs the same tool schemas and the same workflows, so you can check the claims rather than take them.
What it does
- Voice agent build and configuration on Vapi, with endpointing and interruption handling tuned rather than defaulted
- Telephony routing through Twilio, with the live wiring verified by one command that exits non-zero on drift
- Deterministic guardrails in the workflow and the database instead of in the prompt: safety lines, service area, opening hours, escalation
- Caller details judged against the caller's own turns before they are stored, so a name or an address the model filled in for itself is refused, reaches no record and is written to the audit log as judged on no conversation
- Idempotency on unique indexes, a claimed resend counter and an append-only audit log of every tool call
- Calendar and CRM behind two adapters, so a client is a config swap: local PostgreSQL live, Google Calendar and Housecall Pro contract tested against their documented responses, Jobber and ServiceTitan as declared stubs
- A scripted scenario suite that grades transcripts mechanically, samples the emergency path five times per run and writes a machine readable record





Want something like this for your business?