Build a voice agent that answers a real phone number, answers policy questions from its prompt, looks up a caller’s order through your API, and transfers to a person when it should. Every call is transcribed, summarized and judged. The example company is Northwind Tools, an online shop. Substitute your own endpoint, policies and numbers; everything else pastes as written.

What you need

1

Store the endpoint's credential

Credentials go in a workspace secret, which the model never sees:
2

Create the order lookup tool

The say covers the silence while the request runs. The http operation is silent, so the raw JSON never reaches the model; publish_fields keeps three values and generate_reply phrases them. Because no operation returns a result to the model, the whole tool is silent and only the generate_reply speaks.
  • userdata values stay readable for the rest of the call and land on the call record; tooldata exists only for this tool run. See templating and data.
  • A publish_fields path missing from the response fails the operation, so your endpoint must return all three keys on every 200.
  • An unknown order number should return 404. That fails the operation and the model is told the tool could not complete; the prompt below says what to do next. To branch on it yourself, return 200 with a found flag and use an if.
Validate and publish it — an unpublished tool cannot be attached to an agent:
3

Create the transfer tool

For a transfer, the description is the escalation policy: write the conditions, not “transfer to a human”.
disable_interruptions keeps a caller who talks over the say line from cancelling the tool before the dial; the dial itself cannot be interrupted either way. on_failure: "continue" returns the caller to the agent if nobody answers. destination is a literal and cannot be templated; a second destination is a second tool. Publish it the same way as check_order. See transfer.
4

Write the prompt

Save as support-prompt.txt. It is written for speech: short turns, no formatting, spoken forms for numbers and dates. See prompting.
{{system_vars.time}} and {{system_vars.date}} require config.timezone, and in a prompt they are fixed at the moment the call is answered. See variables.
5

Create the agent

Save the config as support-agent.json, with the two tool ids from the steps above:
Then fold the prompt in and create it:
  • conversation.context: "summary" opens each call with the summaries of that caller’s last three calls. It reads post-call summaries, so analysis.enabled and analysis.summary must be on. See conversation memory.
  • No field asks whether the call was transferred: close_reason already records transferred. See call analysis.
  • For languages other than English, change language, stt and tts together. See language and GET /v1/catalog for valid pairs.
6

Validate and publish

errors block the publish; warnings do not. A missing provider key shows up here. Publishing pins each attached tool to its current published version; republishing a tool changes nothing on calls until you publish the agent again. See versions.
7

Put it on a phone number

Create the carrier account, provision it, import the number and assign the agent:
Import reports per number: read ok on every item, not just the HTTP status. On Exotel, inbound calls do not arrive until you complete the step the account’s setup_steps names in Exotel’s console.
Only a published voice agent can be assigned. The number’s readiness must read live. Republishing the agent updates routing on its own. See phone numbers.

Test it

Run the tool against your endpoint. POST /v1/tools/{tool_id}/run executes the draft for real and returns each step, the resolved request and response, and the published values. Spoken operations are recorded, not performed.
Look for publish field path 'estimated_delivery' was not found in operation output — a field your API returns only sometimes. Talk to it in the browser. The agent editor’s test panel runs the latest published version. A web call cannot test the transfer: it fails with transfer is only available on phone calls, and the phone {{system_vars.*}} keys are empty. Ring the number. Try a policy question, a valid order number, a wrong one, and “can I speak to someone”. Then list the calls:
GET /v1/calls/{session_id} returns the transcript with every tool call and result, session.userdata, and a snapshot of latency and issues. See calls.

What to watch in the first week

  • Read transcripts, not summaries. Misread order numbers, policy answers from memory and premature transfers show up there; each fix is a line in the prompt. See debugging agents.
  • GET /v1/calls?outcome=failure lists the calls analysis judged as failures, each with outcome_rationale. Compute the success rate from successful_calls over judged_calls in GET /v1/calls/stats; calls too short to judge are excluded.
  • unanswered_question: true marks a policy your prompt is missing.

Limits of this setup

  • No opening hours. An assigned number answers whenever it rings. The prompt tells the agent the hours; a transfer out of hours rings an empty desk until ringing_timeout.
  • Policy changes need a publish. The agent knows only the prompt of the published version.
  • The transfer target sees your number, not the caller’s, because the outbound leg is dialled from your trunk.
  • Memory is keyed on the pair of numbers, so the same customer calling a second number of yours is a new contact.

Next

Multi-agent triage

Split one overloaded prompt into a front desk and specialists.

Appointment booking

The same shape with a Google Calendar behind it.

Telephony troubleshooting

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

Webhooks

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