> ## Documentation Index
> Fetch the complete documentation index at: https://unmute.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unmute compiles to exactly three targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Those three are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# Human transfers

> Handing the caller to a person: cold, warm, and why the route decides which you get.

Sooner or later a caller needs a person. Unmute has one authoring shape for
that, an entry under `escalations:`, and two forms.

| Form     | What the caller experiences                                                                                          |
| -------- | -------------------------------------------------------------------------------------------------------------------- |
| **cold** | the agent says it is putting them through, the call moves to the person, and the agent drops out                     |
| **warm** | the caller waits while the agent rings the person, tells them what the call is about, and only then connects the two |

On this page:

* [How you write it](#how-you-write-it) - the block, and every key it takes
* [Destinations are symbolic](#destinations-are-symbolic) - the model never sees a number
* [The route decides what is possible](#the-route-decides-what-is-possible) - which shape compiles where
* [The escalation has to be attached](#the-escalation-has-to-be-attached) - the half that is enforced
* [When the person does not pick up](#when-the-person-does-not-pick-up) - `ring_timeout` and `on_unavailable`
* [Where to go next](#where-to-go-next) - the two routes that carry a transfer

## 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:

```yaml agent.yaml theme={null}
escalations:
  send_to_billing:
    when: The caller asks about an invoice, a refund, or a charge they do not recognise.
    cold:
      destination: billing_line

  escalate_to_supervisor:
    when: The caller is unhappy with how something was handled and asks for a manager.
    warm:
      destination: supervisor_line
      briefing: |
        Lead with the caller's name and which stylist they saw.
        Say what they are unhappy about and what you already offered them.
        Ask whether they can take the call now.
      ring_timeout: 25s
      on_unavailable: return_to_caller
```

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](#the-escalation-has-to-be-attached) below.

## Escalation fields

<ParamField path="when" type="string">
  The situation the model reads to decide whether to transfer. Omission supplies no
  trigger guidance, so write one.
</ParamField>

<ParamField path="cold" type="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.
</ParamField>

<ParamField path="warm" type="object">
  A warm transfer using the fields below, including optional `briefing`. Exactly one of
  `cold` and `warm` is required. Supported only on LiveKit SIP.
</ParamField>

## Transfer fields

<ParamField path="destination" type="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`.
</ParamField>

<ParamField path="ring_timeout" type="string">
  A positive Go duration, such as `25s`. Omitted means 25 seconds on Pipecat; LiveKit
  leaves it unset for the platform default.
</ParamField>

<ParamField path="on_unavailable" type="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.
</ParamField>

<ParamField path="briefing" type="string">
  Instructions for briefing the person before connecting the caller. Legal only inside
  `warm`. Omit for the runtime’s standard briefing instructions.
</ParamField>

## 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`:

```yaml agent.yaml theme={null}
destinations:
  billing_line: BILLING_PHONE_NUMBER
  supervisor_line: SUPERVISOR_PHONE_NUMBER
```

### Every key a destination takes

One line per symbol, and both halves are names.

<ParamField path="<symbol>" type="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.
</ParamField>

A number written on the right is refused too:

```text theme={null}
agent.yaml:60: destination "billing_line" is a literal. agent.yaml is
  the portable half of a package, so a destination names an environment variable holding
  the number: billing_line: BILLING_PHONE_NUMBER
```

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:

| Route                     | Cold                 | Warm                  | Mechanism                                                                                               |
| ------------------------- | -------------------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
| LiveKit `sip`             | yes                  | **yes, the only one** | SIP REFER on the caller's existing leg, and LiveKit's `WarmTransferTask` for the held and briefed shape |
| Pipecat `daily-sip`       | yes, on a phone call | **refused**           | Daily transfers the existing SIP phone leg                                                              |
| Pipecat `cloud-websocket` | yes, differently     | **refused**           | one request replaces the live call's markup at the carrier                                              |
| LiveKit `connector`       | **refused**          | **refused**           | the transport carries media only, with no transfer control                                              |

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:

```text theme={null}
pipecat: telephony warm_transfer: telephony route (pipecat, cloud-websocket, twilio) does
  not emit warm transfer: a warm handoff has to act on how the destination's leg ended,
  which on this route needs a callback endpoint you host, and hosting nothing is what
  this route is for; warm transfer compiles on (livekit, sip) trunks today. Connection
  "twilio_voice" declares transport: cloud-websocket
```

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:

```text theme={null}
livekit: cold transfer needs a telephony Connection: it hands the caller's own phone leg to
  the destination, and a session that did not arrive by phone has no leg to hand over
```

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:

```text theme={null}
agent.yaml:47: escalation "send_to_billing" is declared but no agent reaches it; add it to the
  escalations: of one of these agents: front_desk, billing
```

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

```yaml theme={null}
      ring_timeout: 25s
      on_unavailable: return_to_caller
```

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

<Columns cols={2}>
  <Card title="LiveKit route" icon="phone-forwarded" href="/transfers/livekit">
    Cold and warm over a SIP trunk.
  </Card>

  <Card title="Pipecat on Twilio" icon="phone" href="/transfers/pipecat-twilio">
    Cold, with nothing hosted by you.
  </Card>
</Columns>
