You will build a voice agent that answers a real phone number, answers policy questions out of your own help centre, looks a caller’s order up through your API, and hands the call to a person when it should. By the end the number is live, every call is transcribed and summarized, and you can list the calls that went wrong. The example company is Northwind Tools, an online shop selling power tools. Substitute your own domain, endpoint and numbers as you go — everything else pastes as written.

What you need

Everything below is against https://api.in.talqing.com. Agents, tools, knowledge bases, numbers and calls are all regional — the same token reaches every region, the resources do not cross.
1

Pick the models and the language

Language is the first decision, not the last: config.language is one language shared by every model in the pipeline, and a model that cannot speak it is a save error. Pick the language your callers actually use, then models that cover it.
Two speech-to-text entries detect nothing and transcribe every caller as English on Auto: deepgram/nova-3 and raya/bakbak-stt. Neither reports that it guessed. If your callers are not all English speakers, set config.language or pick an entry that detects. See language.
This guide uses English. For the language model take openai/gpt-5.4-mini and leave reasoning_effort unset: the catalog orders each model’s efforts fastest first, unset means the first one, and every second of thinking on a live call is silence a caller reads as a dropped line. See thinking and provider tools.Confirm the pairs exist before you write them — the catalog is the only source of valid ones:
In the dashboard: the agent editor’s Models card has a picker per slot, and each one shows the languages that model speaks beside it.
2

Build the knowledge base

Start this first. A build is a crawl, then a model call per page, then one pass to write the table of contents — minutes, not seconds — and the rest of the work does not depend on it.Create it, wait for review, choose the pages worth indexing, then wait for ready:
Keep polling GET /v1/knowledge/{kb_id} until status is ready. Sixty pages that answer real questions beat six hundred that include the careers section: retrieval here is the model reading titles and summaries and choosing, so every extra entry is another line competing for its attention. The whole lifecycle, and what to do when a page fails, is in building.In the dashboard: KnowledgeNew, then the review step is a pruning tree — URLs nested under the page that linked to them, with a preview on click — and you delete the branches you do not want.
3

Build the order lookup

The tool needs a credential, and a credential belongs in a workspace secret where the model never sees it:
Now the tool. A say goes in front of the request because a tool call is silence on the line and silence reads as a dropped call — the tree keeps running while the line plays, so it costs nothing. The request is silent, so the raw JSON never reaches the model; what the model gets is the three values publish_fields kept, phrased by a generate_reply.
Three details are load-bearing. order_status and order_eta go to userdata, so the agent can still repeat them ten turns later and they are on the call record afterwards; order_carrier goes to tooldata, because it exists to build one sentence. Because the only operation that could return a result is silent, the whole tool is silent — the generate_reply is what speaks, and the agent does not improvise a second sentence on top of it. And publish_fields fails the operation when a path is missing or null, so your endpoint has to answer all three keys on every 200.
An order number that does not exist means a 404, which fails the operation. The model is told the tool could not be completed and apologises in its own words — the prompt below tells it what to say and when to give up. If you would rather the agent said something specific, answer 200 with a found boolean and branch on it with an if.
Then validate and publish. An unpublished tool cannot be attached to an agent.
In the dashboard: ToolsNew, three cards — Configuration, Arguments, Operations — with Save, Validate, Test and Publish across the top.
4

Build the escalation

A transfer tool is two operations and no arguments. What makes it good or bad is its description, because for this one kind of tool the description is not a hint about timing — it is the escalation policy, and it decides whether a frustrated caller reaches a person or is talked at for another two minutes.“Transfer to a human” is not a description. It is an invitation to escalate at the first sign of difficulty. Write the conditions instead:
disable_interruptions is on because the dial runs for up to thirty seconds while the caller hears only ringback, which is exactly long enough that saying “hello?” into the gap is normal — and an interrupted line is the only channel left to tell them what happened. on_failure: "continue" means a busy line brings the caller back to the agent with a plain-English reason rather than dropping them. The destination is a literal number frozen into the published version and can never be templated; if you need a second destination, publish a second tool. See transfer.
5

Write the prompt and create the agent

The prompt is the product. This one is written for speech: short turns, no formatting, spoken forms for anything text-to-speech mangles, and an explicit line about what to say while a tool runs.
Two things about the clock lines. {{system_vars.time}} and {{system_vars.date}} need config.timezone set — using one without it is a save error — and both are frozen at the moment the agent starts, which on an inbound call is the moment it answers. That is right for one call and wrong for a call that runs past midnight. See variables.Now create the agent with everything attached. A write takes the whole config, so this is the complete definition. Save the prompt above as support-prompt.txt and let jq fold it in — a multi-line prompt is miserable to escape by hand, and --rawfile is what stops it from being escaped at all:
conversation.context: "summary" is what makes a returning caller recognised: each new call opens with the summaries of that person’s three most recent calls, as background the agent is told the caller has not heard. On a phone call the person’s identity is the pair of numbers — your number and theirs — so a caller who rings a different one of your numbers, or from a second handset, is a different person as far as this is concerned. summary reads the summaries post-call analysis writes, which is why analysis and its summary have to be on; publish refuses the combination otherwise. See conversation memory.The three analysis fields are chosen for what a weekly report needs and nothing else. Notice what is not there: whether the call was escalated. close_reason already says transferred, and asking a model to re-derive a fact the call record holds is a way of getting a worse copy of it.In the dashboard: the editor’s cards map one to one onto this config — Channel, Models, Tools, Knowledge, Past conversations, Call analysis.
6

Validate and publish

Validation runs on the server against rules a draft cannot show you: a provider key for every model, the operation trees of every attached tool, the knowledge base’s status, the clock variables against timezone. Read it before you publish.
Errors block the publish; warnings do not. Publishing pins each attached tool to the version live at that moment — republishing check_order later changes nothing on real calls until you publish the agent again. See versions.
7

Put it on a phone number

Three things have to line up: a carrier account, a number on it, and a published voice agent assigned to that number.
Import is per number, not all-or-nothing: a 200 means the request was processed, so read ok on every item. And on Exotel there is one step you have to do in Exotel’s own console before inbound calls arrive at all — the account’s setup_steps names it, and until you confirm it every number imports with inbound disabled. See carrier accounts.
Only a published voice agent can answer a number, and readiness is the honest answer to “does this work”: live means active, inbound-capable, assigned and routed. Republishing the agent updates live routing on its own — you never reassign. See phone numbers.In the dashboard: Phone numbers groups rows by carrier and shows readiness per row, with the agent picker in the row itself.

Test it

Work outward: the tool alone, then the agent in a browser, then the real line. Run the tool against your real endpoint. POST /v1/tools/{tool_id}/run executes the draft for real — the HTTP operation calls your API with your secret — and reports every step, the resolved request and response, and everything published. Spoken operations are recorded with their resolved text rather than performed.
The failure to look for is publish field path 'estimated_delivery' was not found in operation output — a field your API only returns sometimes. Fix that before a caller finds it. Talk to the agent in the browser. The agent editor’s test panel runs the latest published version, so republish after any edit. Ask it a policy question and watch it fetch; give it an order number and watch the filler line cover the request.
A web test call cannot exercise the transfer. transfer is phone-only, and on a web call the operation fails with “transfer is only available on phone calls” — the agent apologises and carries on. The three {{system_vars}} phone keys are empty there too. Test the escalation by ringing the number.
Ring the number. Check readiness is live first, then call it from your own phone. Run the whole script: a policy question, an order number, a wrong order number, and “can I speak to someone”. Then read the call back:
snapshot.issues is a list of plain sentences naming every problem found on that call, and it is empty when there were none.

What to watch in the first week

Read the first twenty calls end to end. Not the summaries — the transcripts, with every tool call and what it returned beside the speech. That is where you find the agent reading an order number back wrong, fetching a knowledge entry that does not answer the question, or transferring on the first “hmm”. Each fix is a line added to the prompt, not a rewrite. See calls and debugging agents. Watch the latency breakdown, not the average. snapshot.response_latency on a call carries avg_ms, p95_ms and max_ms; the p95 is what tells you the agent was sometimes slow, which a mean hides. The threshold ships on the response — slow_response_threshold_ms is 3500 — and the stages beside it say where the time went: transcription_avg_ms, end_of_turn_avg_ms (subtract the first to get endpointing on its own), ttft_avg_ms for the model, ttfb_avg_ms for the voice. A high ttft is the language model; a high end_of_turn gap is turn handling; a high ttfb is the text-to-speech provider. snapshot.slow_tool_calls_in_silence is the one tool-latency number that is always a defect — a tool the caller waited on with nothing being said over it. Use the outcome to find the calls that went wrong. GET /v1/calls?outcome=failure is the list to read on a Monday morning, and outcome_rationale on each one says what in the transcript decided the verdict — which is also how you find out your outcome.prompt says something other than what you meant. Read your success rate over judged_calls from GET /v1/calls/stats, never over every call: analysis deliberately skips a call with fewer than two caller turns, so wrong numbers and hangups on the greeting do not drag it down. The knowledge_gap field is the other list worth reading — every true is a page your help centre is missing or a summary the model could not recognise. See call analysis and editing a knowledge base.

Where this falls short

There are no opening hours. Nothing schedules a number: once assigned, it answers whenever it rings. The prompt above tells the agent the hours and what to say, and that is the whole mechanism — a caller at midnight still reaches the agent, and if the model transfers anyway the dial rings an empty desk for thirty seconds and comes back with “nobody answered”. If out-of-hours calls must not be answered at all, unassign the number on a schedule of your own, and expect callers to hear a carrier failure rather than a message. For anything more precise than the frozen clock line, put the hours in a code operation over input.system_vars.now — the clock strings cannot be compared in an if. A question the knowledge base does not answer is simply not answered. Retrieval is the model reading the table of contents and choosing entries; there is no scoring and no fallback, so a page whose summary does not describe what the caller asked will never be fetched, however good the page is. The knowledge base is also a snapshot — nothing re-crawls on a schedule, so the day your returns policy changes on the website the agent is still reading the old one until you build again. The number answers the published version, and only that. An edit to the prompt or a republished tool changes nothing a caller hears until the agent is published again. The one exception cuts the other way and is worth knowing: a knowledge base is not versioned, so rebuilding one changes what live callers hear immediately, with nothing to publish. Three smaller things. The person answering a transfer sees your number, not the caller’s, because the outbound leg is dialled from your own carrier trunk. There is no voicemail detection anywhere on the platform, so a transfer that reaches a voicemail greeting on a cold transfer is a connected call as far as everything downstream is concerned. And conversation memory is keyed on the pair of phone numbers, so the same customer ringing a second number of yours arrives as a stranger.

Next

Multi-agent triage

When one prompt is carrying three jobs, split it into a front desk and specialists.

Appointment booking

The same shape, with a real calendar behind it.

Troubleshooting

Number not live, no inbound audio, calls failing at setup.

Webhooks

Push each finished call, with its outcome and fields, into your own systems.