Skip to main content
A phone call reaches your agent over a route: a target, a transport, and a carrier. Unmute picks the route from what your target declares and generates the code for it. On this page:

Declare a phone channel

agent.yaml
inbound and outbound say which directions this agent supports. They are separate, because most routes support them differently.

Every key a channel takes

One key is required on any channel. A telephony channel adds two more, and takes two optional ones.
string
required
Accepts realtime_audio or telephony. No kind is inferred.
boolean
Accepts true or false for telephony only. Omitted does not enable inbound calls. At least one of inbound and outbound must be true.
boolean
Accepts true or false for telephony only. Omitted does not enable outbound calls. Required as true for warm transfer or voicemail handling.
list of strings
Telephony only. Accepts cold_transfer, warm_transfer, dtmf_send, dtmf_receive, hold, hangup, voicemail_detection, and ivr_navigation; the route must support each requested control. Omit for no extra explicit requirements.
string
Accepts hangup or leave_message where supported by the route. Requires kind: telephony and outbound: true. Omit for no package-defined voicemail action.
Two rules arrive with that block, and validation enforces both:
  • A warm transfer needs outbound: true on the channel. A warm transfer dials the destination itself, so the agent places a call even on a line people only ring in on. Without it: channel "phone" needs outbound: true; a warm transfer places a call to its destination. Cold transfer does not need it, because it hands over the caller’s existing leg rather than making a second one.
  • capacity.peak_starts_per_second becomes required, and must be positive. It is optional on a browser-only package and required the moment any channel is telephony, because calls arrive in bursts and each one starts a session. Without it: capacity.peak_starts_per_second must be positive for telephony.
agent.yaml

Every key capacity takes

capacity: is your traffic estimate. It is required for every code target and for every package with a telephony channel, and the compiler sizes workers and quotas from it.
positive integer
required
Concurrent sessions you expect at peak.
positive integer
required
The ceiling you want to support. It cannot be lower than peak_sessions.
positive number
How fast calls arrive at peak. Required the moment any channel is telephony, optional otherwise. One is a fine answer for a first line.
duration, e.g. 5m
required
How long an average call lasts, as a positive Go duration.

Declare the route

The route lives in a connection file: the mechanism, the carrier, and the account settings as environment variable names, never values.
connections/twilio_voice.yaml
The target names it and says nothing else about telephony:
targets.yaml
So one file is the whole route, and that is the file you open when you want to know how a call reaches this agent. A connection has a full route like the one above or a receive-only cloud-websocket route with no credentials. connections/<name>.yaml explains both shapes. One target selects exactly one route and one connection. To use two carriers, or two mechanisms, declare two targets with a connection file each. Each compiles to its own build/<target>/ directory.

Every key a connection takes

Three keys, and the third holds names rather than values.
sip | connector | cloud-websocket | daily-sip
required
The mechanism that carries the call. Your target’s provider decides which of the four it accepts, and a pairing it does not have is refused with the ones it does.
twilio | telnyx | plivo
required
The carrier account behind the route. Telnyx and Plivo reach an agent through the LiveKit sip route only.
route key to environment variable name
Which variable holds each of the route’s account values. The keys on the left are fixed by the route: account_sid, auth_token, from_number, sip_address, sip_username, sip_password. Which environment keys a route accepts has the set per route, and a key from another route is refused. The names on the right are yours, and every one is UPPER_SNAKE.
A connection writes no kind:. Every transport in the catalog is telephony, so transport: has already said it.

Every key a target takes

A target says where the agent runs. These are the keys a phone agent uses, and targets.yaml has the rest.
livekit | pipecat | slng
required
Which orchestrator this target compiles to. Only livekit and pipecat carry a phone call.
exact x.y.z
The framework version pinned into the emitted project. Required for a code target, written with all three numbers, and installed exactly as written.
connection file stem
Which connection carries this target’s calls, named without the folder and without the .yaml. Required for a LiveKit or Pipecat phone agent, and refused on a package with no phone use.
platform region name, or a list of them
Where the platform deploys the agent. Pipecat takes exactly one. LiveKit takes several, and emits one create command per region.
integer
How many instances the platform holds ready, so a call is not waiting on a cold container. Pipecat only, and LiveKit refuses it.

The routes

Exotel is not listed for LiveKit SIP: no adapter, so this route is refused at validation.

Where each route deploys

Every route above deploys to a managed platform, so a phone call reaches this agent only once it is deployed. There is no route with nothing to deploy. The Pipecat Daily helper exposes one public /call webhook. It requires the exact HTTPS base URL in UNMUTE_PUBLIC_URL (an optional path is allowed) and verifies Twilio’s signature over the complete form before it uses the Pipecat Cloud key to start an agent. A missing or invalid signature returns HTTP 403 and starts nothing.

What a working phone route takes

Compiling is one of three parts, and the other two are things you do in someone else’s console or CLI. Neither is optional, and skipping either gives you a number that rings and never connects. Step 3 is the one people miss, partly because two different things get called a . The Elastic SIP trunk is Twilio’s, and it carries the call. The inbound trunk is LiveKit’s, and it says the number belongs to your project. LiveKit rejects a call whose number no inbound trunk claims, however correct the Twilio side is. Twilio setup covers part 1, and inbound calls walks all three in order.

What the transport decides

The transport is not a detail. It decides what the agent can do on a call.
  • SIP hands over a call leg with its own signalling, so the leg can be moved. That is why cold transfer, warm transfer, and voicemail detection live on the LiveKit sip route.
  • A media stream over a websocket hands over audio frames. Call control happens over the carrier’s REST API instead, so a transfer is either a different mechanism or not possible at all.
Transfers covers which route can reach a human, and how.

Point the carrier at the deployment

Your project is deployed by the time you reach this page, so what is left is carrier-side: finish the setup for your route, then call the number. There is no local phone loop. unmute dev runs the agent in your browser, and that loop covers the prompt, the tools, and the models, but it stops exactly where the phone leg starts.

Where to go next

Outbound calls

Make the agent dial out, and give the call its values.

Inbound calls

Take a real call, once the route is deployed.

Get your Twilio details

Which console value fills which name.