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

How you write it

A transfer is a block in the transfers: list of the agent that offers it. Cold is the default. Writing brief: makes it warm, so a warm only setting cannot be written on a cold transfer:
agent.yaml
A transfer is defined once, by the agent that writes its block. Another agent offers it by listing the bare name, - send_to_billing. A bare name nobody defines is refused, and so are two blocks with one name.

Transfer fields

string
required
A lower snake case name, unique across the package.
string
required
The UPPER_SNAKE name of an environment variable holding an E.164 number or a sip: URI, read at call time. Declare it in secrets:. No destination is inferred.
string
The situation the model reads to decide whether to transfer. Omission supplies no trigger guidance, so write one.
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. Makes the transfer warm. Supported only on LiveKit SIP.

The number is an environment variable

The model never sees a phone number. to: names the variable that holds it, and the variable goes in secrets::
agent.yaml
A number written in to: is refused. The refusal does not repeat the number:
The variable sits in agent.yaml rather than on the target because who this agent transfers 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 transfer has to be reachable

A transfer block sits in one agent’s transfers: list. Another agent offers it by listing the bare name. A bare name that no agent defines is refused at build, with the file, the line, and the fix:
A transfer no agent the call can reach offers is refused too, because its number would reach .env.example and the startup check for something nothing could dial.

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.