TL;DR
The 9 PM caller with a burst pipe hires whoever answers. This build answers: a natural-sounding AI picks up on the second ring, asks your qualifying questions, books a real slot, then texts you a clean summary with the full transcript saved. Every call becomes structured data instead of a lost voicemail.
The problem this solves
The after-hours emergency caller is the most valuable lead your business will ever get — maximum urgency, minimum price sensitivity, and zero loyalty: they will hire whoever picks up. Voicemail loses them. A text-back (SBW-001) helps, but some situations need a voice.
A voice agent answers every time, sounds natural, asks the questions you'd ask — what's the problem, where are you, how urgent — books a time slot, and hands you the whole conversation as data.
The full picture
THE END-TO-END FLOW
═══════════════════════════════════════════════════════════════════════
┌──────────────┐ after hrs / ┌──────────────────┐
│ CUSTOMER │ ring-no-ans │ CALL ROUTING │
│ calls you │ ────────────▶ │ day: your phone │
└──────────────┘ │ night: the agent │
└────────┬─────────┘
▼
┌──────────────────────────┐
│ VOICE AGENT │
│ (Vapi/Retell + Claude) │
│ │
│ "Hi, you've reached │
│ Mike's Plumbing — I'm │
│ the AI assistant…" │
│ │
│ asks: problem? where? │
│ urgency? number? │
└────────────┬─────────────┘
│ live availability
▼
┌──────────────────────────┐
│ BOOKING (Cal.com) │
│ "I can get someone there │
│ between 9 and 11" = REAL│
└────────────┬─────────────┘
│ call ends
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ TRANSCRIPT + JSON │ │ SMS TO YOUR CELL │ │ CALL LOG (DB) │
│ summary extracted │ │ "Dana, 84th Ave, │ │ transcript, intent,│
│ by post-call LLM │ │ ceiling leak, │ │ booking, outcome — │
│ │ │ booked 9am, URGENT│ │ searchable forever │
└────────────────────┘ └────────────────────┘ └────────────────────┘
═══════════════════════════════════════════════════════════════════════
What you need
| Piece | What it does | Pick |
|---|---|---|
| Voice platform | Telephony + speech-to-text + text-to-speech | Vapi or Retell, connected to a Twilio number |
| Conversation LLM | The agent's brain during the call | Claude (Sonnet tier for conversation quality) |
| Post-call LLM | Reads the transcript, extracts structured data | Claude (Haiku tier — cheap, runs once per call) |
| Booking | Real availability the agent can offer | Cal.com or your scheduler's API |
| Call log | Transcripts + extracted data | Supabase/Postgres |
Phase 1 — Route only the calls you'd otherwise lose
INCOMING CALL
│
▼
┌─────────────────────────────┐
│ what time is it? │
├──────────────┬──────────────┤
│ business hrs │ after hours │
▼ ▼
ring your straight to
real phone the agent
│
│ no answer in 20s?
└────────────▶ agent picks up
The agent is a safety net, not a replacement for answering your own phone. Day calls forward to you first; the agent catches ring-no-answer and everything after close.
Phase 2 — Write the agent's job description (not a script)
This single prompt is 80% of the quality. Don't script lines; describe the job.
The system prompt (copy, fill, tune):
You are the after-hours assistant for {{business_name}}, a {{trade}} company serving {{service_area}}. You are an AI and you say so in your first sentence. You sound like a calm, friendly dispatcher: short sentences, plain words, never more than one question at a time.
Your job on every call:
- Find out what the problem is.
- Find out where they are. If outside {{service_area}}, politely say we can't help and suggest they search locally. Do not book them.
- Find out how urgent it is. Water, gas smell, no heat in winter, or safety = emergency.
- Get their name and best callback number.
- If emergency: tell them the on-call tech will call within 15 minutes. Do NOT book.
- If routine: offer the next real opening from the calendar and book it.
Hard rules:
- Never quote prices. Say "the tech will confirm pricing before any work starts."
- Never promise arrival times outside the calendar's real openings.
- Never claim to be human, even if asked directly.
- If the caller is angry, confused, or the situation doesn't fit, stop qualifying and say the owner will call first thing — then end gracefully.
- Calls are recorded; the platform's greeting discloses this.
Worked example — what a good call sounds like:
AGENT: Hi, you've reached Mike's Plumbing. I'm the AI assistant —
I can get help headed your way. What's going on?
CALLER: Yeah hi, we've got water dripping through the kitchen ceiling.
AGENT: That sounds urgent. Where are you located?
CALLER: 84th Avenue, near the school.
AGENT: Got it, that's in our area. Is the water still coming through?
CALLER: Yeah, it's getting worse.
AGENT: Okay — what's your name and the best number to reach you?
CALLER: Dana, this number is fine.
AGENT: Thanks Dana. Our on-call tech will phone you within 15 minutes.
If you can, shut off the water at the main valve — usually in
the basement near the meter. Help is on the way.
Six exchanges. No rambling, no fake cheer, one piece of genuinely useful advice.
Phase 3 — The post-call pipeline (transcripts into data)
The call ends; the platform hands you a transcript. Don't let it rot in a dashboard — run it through an extraction LLM immediately.
CALL ENDS
│ transcript + recording URL via webhook
▼
┌──────────────────────────────────────────┐
│ POST-CALL LLM — returns strict JSON: │
│ { │
│ "caller_name": "Dana", │
│ "callback_number": "+1403…", │
│ "address_area": "84th Ave", │
│ "problem": "water through ceiling", │
│ "urgency": "emergency", │
│ "booked_slot": null, │
│ "promised": "on-call callback 15 min", │
│ "out_of_area": false, │
│ "follow_up_needed": true │
│ } │
└────────────────┬─────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌───────────────┐ ┌──────────────────┐
│ SMS SUMMARY │ │ call_log row │
│ to your cell │ │ transcript + │
│ in <60s │ │ JSON, searchable │
└───────────────┘ └──────────────────┘
The extraction prompt:
Below is a transcript of a call answered by our AI assistant for {{business_name}}. Return ONLY JSON: caller_name, callback_number, address_area, problem (under 8 words), urgency ("emergency", "soon", "routine"), booked_slot (ISO time or null), promised (anything the agent committed to, under 12 words), out_of_area (bool), follow_up_needed (bool — true if anything was left unresolved). If a field wasn't said, use null. Never guess.
TRANSCRIPT: {{transcript}}
The promised field matters most: it's the list of commitments your AI made on your behalf. Read it every morning.
Data model
call_log
┌──────────────────┬────────────────────────────────────────┐
│ id │ uuid │
│ called_at │ timestamp │
│ caller_number │ +1403… │
│ recording_url │ link (platform) │
│ transcript │ full text │
│ caller_name │ Dana │
│ problem │ water through ceiling │
│ urgency │ emergency | soon | routine │
│ booked_slot │ timestamp or null │
│ promised │ "on-call callback 15 min" │
│ out_of_area │ boolean │
│ follow_up_needed │ boolean │
│ outcome │ booked | callback_done | lost | spam │
└──────────────────┴────────────────────────────────────────┘
Folder structure
voice-receptionist/
├── CLAUDE.md ← the AI-builder brief (ships in this kit)
├── prompts/
│ ├── agent-system.md ← the job description above — versioned, tuned weekly
│ └── extract.md ← post-call extraction prompt
├── api/
│ ├── call-ended.ts ← platform webhook → extraction → SMS + log
│ └── availability.ts ← feeds real Cal.com slots to the agent
└── lib/
├── extract.ts ← LLM call + JSON validation
└── notify.ts ← owner SMS summary
Compliance notes (US + Canada)
- Disclose the AI in the first sentence of every call. It must never claim to be human, even when asked. (Several US states and pending laws specifically require disclosing an AI/bot caller.)
- Recording disclosure at the start of every call. Platforms record by default. ~11 US states are "all-party consent" (CA, FL, PA, WA, IL, MD, etc.) — disclosing on every call keeps you safe everywhere, US and Canada alike.
- US — TCPA: outbound/automated calling has its own consent rules; this build answers inbound calls, which is the safe case. Don't bolt on auto-dial outreach without checking TCPA first.
- PIPEDA (Canada) / state privacy law (US): recordings and transcripts are personal information. Know where your platform stores them, set retention (90 days is sensible), and keep your own copies in your own DB.
This is practical guidance, not legal advice — confirm the rules for the states/provinces you operate in.
Numbers to watch
| Metric | Healthy | Where it comes from |
|---|---|---|
| Capture rate (contact info or booking) | 60–80% of answered calls | call_log |
| Booked directly | 30–50% of routine calls | booked_slot |
| Out-of-area filtered | every one saves a wasted callback | out_of_area |
| Promise follow-through | 100% — read promised every morning |
your own discipline |
One saved after-hours emergency per month typically pays for the system several times over.
Week-one checklist
- Call after hours — agent answers by ring two, discloses AI + recording
- Fake emergency ("water everywhere") — no booking, promises 15-min callback, your phone gets URGENT summary
- Routine request — agent offers a real calendar slot and books it
- Out-of-area address — politely declined, no booking, logged
- Ask "are you a robot?" — honest yes, conversation continues naturally
- Ask for a price — deflects to "tech will confirm pricing," never quotes
- Read every transcript this week; tighten the system prompt where it fumbles
Troubleshooting
- Agent sounds robotic: shorten its answers. Most prompts make agents over-explain; real dispatchers speak in fragments.
- Books out-of-area callers: the service-area question must be mandatory before any booking step — check the prompt's job list order.
- Callers hang up in the first 10 seconds: your greeting is too long. Greeting + AI disclosure + one question, under 5 seconds.
- Extraction JSON misses fields: the "use null, never guess" rule must be in the prompt; validate and retry once.
- Calendar offers slots you can't honour: the agent must read availability live from
api/availability, never from its prompt.