- Declare a phone channel -
channels:andcapacity:, key by key - Declare the route - the connection file, and the target that names it
- The routes - the four that exist, and where each one deploys
- What a working phone route takes - the two carrier-side steps compiling does not do
- What the transport decides - why a transfer lives on one route and not another
- Point the carrier at the deployment - the last step, and why there is no local phone loop
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.- A warm transfer needs
outbound: trueon 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_secondbecomes required, and must be positive. It is optional on a browser-only package and required the moment any channel istelephony, 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
targets.yaml
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.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, andtargets.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
siproute. - 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.
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.