Skip to main content
Sooner or later a caller needs a person. Unmute has one authoring shape for that, an entry under escalations:, and two forms. On this page:

How you write it

The shape you write is the shape you get. There is no mode: field, so a warm only setting cannot be written on a cold transfer:
agent.yaml
The escalation’s name goes in the agent’s own escalations: list. That half is enforced, not merely conventional. See the control has to be attached below.

Escalation fields

string
The situation the model reads to decide whether to transfer. Omission supplies no trigger guidance, so write one.
object
A cold transfer using the destination and timeout fields below. Exactly one of cold and warm is required; there is no default transfer form.
object
A warm transfer using the fields below, including optional briefing. Exactly one of cold and warm is required. Supported only on LiveKit SIP.

Transfer fields

string
required
A symbol declared in destinations, whose value names an environment variable holding the destination. No destination is inferred. Valid inside both cold and warm.
string
A positive Go duration, such as 25s. Omitted means 25 seconds on Pipecat; LiveKit leaves it unset for the platform default.
string
Accepts return_to_caller or hangup. Omitted means return_to_caller. Pipecat cloud-websocket requires explicit hangup, because the original media stream cannot be reconnected.
string
Instructions for briefing the person before connecting the caller. Legal only inside warm. Omit for the runtime’s standard briefing instructions.

Destinations are symbolic

The model never sees a phone number. destination names a symbol, and the symbol is resolved at the top level of agent.yaml:
agent.yaml

Every key a destination takes

One line per symbol, and both halves are names.
UPPER_SNAKE environment variable name
required
The left half is the symbol an escalation’s destination: names. The right half is the environment variable holding an E.164 number or a sip: URI, read at call time. A symbol no escalation reaches is refused.
A number written on the right is refused too:
Destinations sit in agent.yaml rather than on the target because who this agent escalates to is the same desk whichever carrier reaches it.

The route decides what is possible

Transfers ride the platform’s own primitive. There are three different mechanisms, which is why the answer differs per route: Warm transfer is not supported on any Pipecat target. It requires the LiveKit sip route. Pipecat daily-sip + Twilio is the Daily shape that transfers, and the carrier is why: your carrier owns the number and hands Daily the SIP leg, so there is a real phone leg to hand on. A cold transfer there needs a Twilio connection and an active channels.phone route; a browser session has no leg and gets a named failure before the agent announces anything. You hear a transfer on a deployed agent. Both shapes hand a real phone leg to a real number, so there is no local rehearsal for either: deploy, finish the carrier setup, and place the call. The browser loop that unmute dev gives you covers the prompt, the tools and the models, and stops exactly where the phone leg starts. Read the two words in that table carefully, because they are different answers:
  • yes means the compiler emits it, and a deployed call performs it.
  • refused means the shape does not compile. The error names the connection, the transport it declares, and a route where the shape does work.
The route comes from the connection the target names, so that is what the refusal points at. A transfer a route cannot do is refused at validation, naming the connection and the transport it declares, and it never compiles into something that quietly does nothing:
That last sentence is the fix: to get a warm transfer here you change the connection, not the control.

A target with no route at all

The table above is about which route does what. A target that names no connection is a separate case, and it is now refused too:
This used to compile. LiveKit emitted the transfer tool, and the generated code carried a branch explaining, in a comment, that the usual cause of failure is “a session that never arrived by phone: an Agent Console run, a browser session”. The compiler knew and shipped it anyway. A Pipecat daily-sip transfer needs a Twilio connection and an active channels.phone route. A browser session has no SIP leg and gets a named failure before the agent announces a transfer.

The escalation has to be attached

Declaring an escalation under escalations: is half the job. Until some agent lists its name in its own escalations: list, no agent can reach it, and that is refused at build with the file, the line, and the agents you could attach it to:
Before, it compiled at exit 0 and the control was simply absent from the generated project. Its destination’s environment name still reached .env.example and the generated startup check, so the agent refused to start over a secret nothing would ever read.

When the person does not pick up

On the LiveKit sip route, no answer, a decline, voicemail, and a failed dial all come back as one failure, and on_unavailable decides what happens next. On the Pipecat cloud-websocket route, when the destination leg ends, Twilio ends the original call. A decline or no answer also ends the call after the dial timeout. No fresh agent starts without the previous conversation context. Pipecat cloud-websocket requires explicit on_unavailable: hangup; it cannot reconnect the original media stream. The agent sees only the accepted REST update as transfer_started; it does not observe whether the person answers.

Where to go next

LiveKit route

Cold and warm over a SIP trunk.

Pipecat on Twilio

Cold, with nothing hosted by you.