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

# Agent configuration

> Every block of the agent file, and the tool files beside it.

`agent.yaml` is the declarative description of the agent: what it says, what it
can do, and who it can reach. Nothing in it belongs to one target; the runtime
half lives in [targets.yaml](/reference/targets-yaml).

An optional `manifest: manifest` links the company contract copied into the
package root. If that file exists, the link is required. See
[Manifest](/reference/manifest) for the rules and saved defaults.

YAML decoding is strict. An unknown field is an error with the file and the
line, not a shrug.

Durations use Go duration syntax, for example `90s`, `15m`, or `1h30m`.

```yaml agent.yaml theme={null}
version: 1
name: acme-greeter
entry_agent: greeter

models:
  think:
    reasoning:
      provider: openai
      model: gpt-5.6-terra
  speak:
    voice:
      provider: slng
      model: "deepgram/aura:2"
      voice: "aura-2-thalia-en"
  listen:
    transcriber:
      provider: slng
      model: "deepgram/nova:3"
  turn:
    detector:
      provider: local
      model: silero

agents:
  greeter:
    instructions: instructions.md
    think: reasoning
    speak: voice

channels:
  web:
    kind: realtime_audio

capacity:
  peak_sessions: 2
  max_sessions: 5
  avg_session_duration: 3m
```

That `turn.detector` binding runs as written on Pipecat, which forwards a
model identity unchecked. `silero` is a voice activity detector, not a turn
detector, and LiveKit checks the identity and refuses it. A LiveKit target
names its own detector in [targets.yaml](/reference/targets-yaml):

```yaml targets.yaml theme={null}
targets:
  livekit:
    provider: livekit
    version: "1.8.1"
    sdk_language: python
    models:
      detector:
        provider: livekit
        model: turn-detector-mini
```

One `agent.yaml`, two targets, and only the override changes. That is the
split this page and [targets.yaml](/reference/targets-yaml) hold between them.

## All keys

<ParamField path="version" type="integer" required>
  Schema version. Accepts `1`. Required; omission is refused.
</ParamField>

<ParamField path="name" type="string" required>
  What the deployed agent is called. Accepts lowercase letters, digits, single hyphens; 3
  to 64 characters. Required; omission is refused.
</ParamField>

<ParamField path="entry_agent" type="string" required>
  Agent that answers. Accepts declared agent name. Required; omission is refused.
</ParamField>

<ParamField path="architecture" type="cascade | realtime | live">
  Which pipeline to build. Left out, it is `cascade`. It decides which `models`
  sections are legal, so it is read before the rest of the file. See
  [Architecture](/build/architecture/overview).
</ParamField>

<ParamField path="models" type="map" required>
  Model palette grouped by kind. Accepts sections `think`, `speak`, `listen`, `turn`,
  and the speech to speech sections `realtime` and `live`.
  Required; omission is refused.
</ParamField>

<ParamField path="listen" type="string">
  Listen entry to use. Accepts declared `models.listen` name. Selects the sole listen
  chain head; required when there are two or more.
</ParamField>

<ParamField path="turn" type="string">
  Turn entry to use. Accepts declared `models.turn` name. Selects the sole turn entry;
  required when there are two or more.
</ParamField>

<ParamField path="variables" type="map">
  Per-call values. Accepts lower snake case names. If omitted, there are no declared
  session values.
</ParamField>

<ParamField path="shapes" type="list">
  Field groups a variable's `type:` can refer to. Accepts one or more named field groups,
  `CapWords` names. If omitted, there are no custom types.
</ParamField>

<ParamField path="prefetch" type="list">
  Facts resolved once per call, before the greeting, in the order written. Accepts one
  entry per fact, each with a `name:`. If omitted, no pre-fetch runs.
</ParamField>

<ParamField path="secrets" type="list of strings">
  Environment values the generated project reads. Accepts UPPER\_SNAKE names. If omitted,
  there is no explicit secret inventory; inferred requirements still apply and missing
  declarations warn.
</ParamField>

<ParamField path="destinations" type="map of strings">
  Phone destinations an escalation may use. Accepts lower snake case names to UPPER\_SNAKE
  env names. If omitted, there are no transfer destinations; required when an escalation
  uses one.
</ParamField>

<ParamField path="knowledge" type="map">
  Folders of documents a tool can search. Accepts 3 to 64 characters of `[a-z0-9_]`. If
  omitted, there are no document bases.
</ParamField>

<ParamField path="agents" type="map" required>
  Agent prompts, models, nested tasks, and other callable names. Accepts one or more lower
  snake case names. Required; omission is refused.
</ParamField>

<ParamField path="task_groups" type="map">
  Ordered task sequences, named by the agents that run them. Accepts lower snake case
  names. If omitted, there are no task groups.
</ParamField>

<ParamField path="handoffs" type="map">
  Agent to agent, and never returns. Accepts lower snake case names. If omitted, there are
  no agent handoffs.
</ParamField>

<ParamField path="escalations" type="map">
  Agent to a person. Accepts lower snake case names. If omitted, there are no human
  transfers.
</ParamField>

<ParamField path="tools" type="list of strings">
  Tool files to load. Accepts loaded tool file names. If omitted, no tool files are
  loaded.
</ParamField>

<ParamField path="conversation" type="object">
  Greeting, interruption, inactivity, and limits. Accepts keys below. If omitted, code
  targets open with a model-written greeting and default interruption behavior. SLNG
  requires an explicit greeting.
</ParamField>

<ParamField path="tracing" type="object">
  Tracing provider. Accepts `provider: langfuse` or `provider: coval`. If omitted, no
  tracing is configured.
</ParamField>

<ParamField path="channels" type="map" required>
  How people reach the agent. Accepts one or more channel definitions. Required; omission
  is refused.
</ParamField>

<ParamField path="capacity" type="object">
  Traffic estimate. Accepts positive values; constraints below. Required for code targets
  or telephony; otherwise no traffic estimate is declared.
</ParamField>

## `name`

What this agent is called. Required on every target.

```yaml agent.yaml theme={null}
name: acme-support
```

The deployed name is this joined to the target it was compiled for, so
`acme-support` on a target called `slng` deploys as `acme-support-slng`, and on
a target called `livekit_eu` as `acme-support-livekit-eu`. The target half is
there for one collision the package half cannot solve: a package with two
targets of the same provider would otherwise deploy one name twice and overwrite
itself.

Where the deployed name lands:

| Target    | What carries it                                                                                |
| --------- | ---------------------------------------------------------------------------------------------- |
| `slng`    | the pushed agent's `name`, which is how a push finds the agent to update                       |
| `pipecat` | `agent_name` in `pcc-deploy.toml`, its secret set, and every `pipecat cloud agent ...` command |
| `livekit` | the worker's `agent_name`, which is what a SIP dispatch rule matches                           |

`name:` on its own, without the target, labels the generated project: the
pyproject distribution name, the logger, the trace name, the README title.

### Why unmute does not infer it

Both candidates look like names and neither is an identity.

* The **target** is called `slng`, `livekit` or `pipecat`, because that is what
  the docs, the examples and the console all call it. Unmute used to deploy
  under it, so two packages in one organisation claimed one live agent and the
  second deploy replaced the first, prompt, models and attached tools included.
* The **folder** is named by whoever cloned the repository. It changes on a
  rename, a copy, or a CI checkout into another path, and it changes silently.

### Shape

Lowercase letters, digits and single hyphens, starting with a letter, 3 to 64
characters. The name is written into a PEP 508 `name =` in `pyproject.toml`, a
Pipecat Cloud agent, a LiveKit `agent_name` and an SLNG agent. SLNG is the
loosest of the four and pyproject the strictest, so unmute holds one shape all
four accept rather than rewriting yours per target.

### Renaming an agent that is already deployed

A rename does not move a deployment. It leaves the old one running and creates a
second, so after changing `name:`:

* **slng**: the old agent stays in your organisation. Delete it, or leave it and
  point your sessions at the new id.
* **pipecat**: `pipecat cloud deploy` creates a new agent, and the old one keeps
  billing. Delete it with `pipecat cloud agent delete <old-name>`, and re-create
  the secret set under the new name.
* **livekit**: the agent itself is fine, because `lk agent deploy` targets the
  id in the preserved `livekit.toml` and re-registers the worker under the new
  name. The **SIP dispatch rule** is what breaks: it still names the old worker,
  so inbound calls ring and nothing answers. Delete it
  (`lk sip dispatch delete <id>`) and re-run `telephony-setup.sh`, which skips
  the step while a rule for that trunk still exists.

There is no way to keep the old bare name. An agent deployed as `livekit` was
named after the target, and the new name always carries the package half, so the
first compile after this change renames every existing deployment once. Do that
rename deliberately, with the steps above, rather than discovering it on a call.

## `models`

Six sections. The section an entry sits in decides its kind: `think` (LLM),
`speak` (TTS), `listen` (STT), `turn` (turn detection), and `realtime` and
`live`, a
[live model](/models/live) that does the first three as one and compiles on
both code targets. The first four are maps keyed by entry name; `live` is a list
whose items carry `name:`. Entry names share one namespace across sections and
are yours to choose.

```yaml theme={null}
models:
  think:
    reasoning:
      provider: openai
      model: gpt-5.6-terra
      params:
        reasoning_effort: "none"
```

Which fields are legal depends on the section:

| Field                                                                     | Legal section                                                                                                                                                                                                                        |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `provider`, `model`, `endpoint_env`, `placement`, `params`, `description` | `think`, `speak`, `listen`, `turn`                                                                                                                                                                                                   |
| `voice`, `speed`                                                          | `speak`                                                                                                                                                                                                                              |
| `language`                                                                | `speak`, `listen`                                                                                                                                                                                                                    |
| `temperature`, `top_p`, `top_k`                                           | `think`                                                                                                                                                                                                                              |
| `semantic_endpointing`                                                    | `turn`: `required`, `preferred`, or `off`                                                                                                                                                                                            |
| `pace`                                                                    | `turn`: `snappy`, `balanced`, or `patient`. How quickly the agent decides the caller has finished. Sets the ceiling on a turn, and the floor when `endpointing_delay` is absent. Defaults to `balanced`. No per-target override      |
| `endpointing_delay`                                                       | `turn`: a positive duration, the window of silence before the caller counts as finished. The floor on every turn, and only the floor. LiveKit refuses under `250ms`                                                                  |
| `eager`                                                                   | `turn`, Pipecat only, with `provider: listen`: `true` answers the transcriber's predicted end of turn before it is confirmed, dropping the early reply if the caller goes on. Off unless set; costs one model request per prediction |

A `turn` entry's `provider` is `local` (the on-device pair) or, on Pipecat,
`listen`, which hands the decision to the listening model's own turn detection.
`listen` takes no `model` of its own and needs a Deepgram Flux or Cartesia Turns
listening model. Under `listen` the `pace` ceiling becomes the transcriber's own
end-of-turn timeout, and `semantic_endpointing`, `endpointing_delay` and
`interruption.minimum_words` are refused because nothing reads them; see
[Turn detection](/models/turn-detection).
\| `fallback` | `think`, `listen` |
\| `name`, `provider`, `model`, `voice`, `backend`, `description` | `live`, and nothing else: every other field is refused on a live entry by name, with its line. `backend` names a `models.think` entry with `provider: openai` that runs the model's tools and reasoning. See [Live model](/models/live) |
\| `name`, `provider`, `model`, `voice`, `turn_detection`, `description` | `realtime`, and nothing else. `turn_detection` is `server_vad`, `semantic` or `local`. `voice` and an agent `speak:` binding are mutually exclusive, and neither is refused. See [Realtime](/build/architecture/realtime) |
\| `prompt_suffix` | `think`: literal text appended to every prompt this binding sends, up to 512 characters, no `{{variables}}`. A per-target override cannot name a different value. See [Context Router](/optimization/context-router) |
\| `agent_id`, `upstream` | `think`, and only on a binding routed through the SLNG Context Router; refused on any other binding. See [Context Router](/optimization/context-router) |

`pace` and `endpointing_delay` are the two turn-timing settings and they do
different jobs: the pace sets the ceiling on a turn, the duration sets only the
floor. [Turn taking](/optimization/turn-taking) has every legal value and what
each becomes on each target.

<ParamField path="provider" type="string">
  A provider supported for this role and target, listed on the model pages. Required for
  an API binding; there is no inferred API provider. `local` selects local placement.
</ParamField>

<ParamField path="model" type="string">
  Provider model id, passed through as written. Required where the selected integration
  requires a model; otherwise its default applies. LiveKit turn bindings accept only
  `turn-detector-mini` or `turn-detector`.
</ParamField>

<ParamField path="voice" type="string">
  Voice id on a `speak` entry. No voice is chosen by Unmute when omitted; the selected
  integration may require one or use its own default.
</ParamField>

<ParamField path="speed" type="number">
  Speaking speed on a `speak` entry. Provider-defined values and limits; omitted leaves
  the provider default.
</ParamField>

<ParamField path="language" type="string">
  BCP-47 language tag, such as `en` or `en-US`, on `listen` or `speak`. Omit to leave
  language selection to the integration.
</ParamField>

<ParamField path="temperature" type="number">
  Sampling temperature on a `think` entry. Provider-defined values and limits; omitted
  leaves the provider default.
</ParamField>

<ParamField path="top_p" type="number">
  Nucleus sampling value on a `think` entry. Provider-defined values and limits; omitted
  leaves the provider default.
</ParamField>

<ParamField path="top_k" type="integer">
  Sampling count on a `think` entry. Provider-defined values and limits; omitted leaves
  the provider default.
</ParamField>

<ParamField path="endpoint_env" type="string">
  An UPPER\_SNAKE environment variable name holding a custom endpoint URL. Omit to use the
  integration’s endpoint. Required for Pipecat’s unlisted-provider path.
</ParamField>

<ParamField path="placement" type="string">
  Accepts `api` or `local`. If omitted, `provider: local` selects local placement; another
  named model selects API placement. Target-specific turn detection may decide placement
  itself.
</ParamField>

<ParamField path="params" type="object">
  Provider parameter names and values. Omit to add no extra parameters. Provider limits
  apply; Unmute does not define a universal accepted set. The Responses directive below is
  checked separately.
</ParamField>

<ParamField path="fallback" type="list of strings">
  Names from the same `think` or `listen` section, in retry order. Omit for no fallback
  chain. Cycles and other roles are refused; Pipecat refuses generated fallback.
</ParamField>

<ParamField path="description" type="string">
  An author note. Omit for no note.
</ParamField>

<ParamField path="semantic_endpointing" type="string">
  On `turn`, accepts `required`, `preferred`, or `off`. Omit to keep the target’s semantic
  detector. `off` removes it; see [turn
  detection](/models/turn-detection#semantic-endpointing).
</ParamField>

<ParamField path="pace" type="string">
  On `turn`, accepts `snappy`, `balanced`, or `patient`. Omitted means `balanced`. Sets
  the ceiling and, unless `endpointing_delay` is present, the floor. Cannot be authored in
  a per-target override.
</ParamField>

<ParamField path="endpointing_delay" type="string">
  On `turn`, a positive Go duration such as `300ms`. Sets only the silence floor; LiveKit
  requires at least `250ms`. Omit to use the pace’s floor. See [turn
  taking](/optimization/turn-taking).
</ParamField>

<ParamField path="agent_id" type="string">
  Required on a Context Router think binding: a stable id of at most 128 printable ASCII
  characters, with no whitespace or colon. No id is generated. See [Context
  Router](/optimization/context-router) for scope rules.
</ParamField>

<ParamField path="upstream" type="object">
  Required on a Context Router think binding; no upstream is inferred. Names the provider
  and its credentials. See [upstream
  fields](/optimization/context-router#upstream-fields). Refused on other bindings.
</ParamField>

<ParamField path="prompt_suffix" type="string">
  Literal text on a `think` entry, up to 512 characters, with no `{{variables}}`. Appended
  to every prompt using the profile. Omit to append nothing. A target override cannot
  declare a different value.
</ParamField>

Entries you do not reference are legal alternates.

The provider catalogue has two wildcard routes. Pipecat accepts an unlisted
listen, speak, or think provider only with `endpoint_env`, through an
OpenAI-compatible integration. LiveKit accepts an unlisted think provider
through LiveKit Inference; its listen and speak provider lists are closed.
Never invent a provider name for either route.

`model` and `voice` are passthrough. Most `params` are too: a name the target's
settings object has no field for rides that target's overflow field and reaches
the provider. The narrow exception is `api: responses` on a LiveKit OpenAI
reasoning binding. Unmute checks that directive, selects the Responses client,
and turns `reasoning_effort` into the API's nested reasoning setting.
[Reasoning model](/models/llm) shows the target-local form and explains why.

Do not guess model ids, voice ids, or params. Use values the user supplied or
values verified in the provider's own documentation.

## `variables`

```yaml theme={null}
variables:
  caller_name:
    type: string
    source: call_start
    default: there
    description: Caller's first name.
```

<ParamField path="type" type="string" required>
  A single-line Python type expression. Accepts `str`, `int`, `float`, `bool`, `Phone`,
  `Date`, `Time`, `Id`, `EmailStr`, `NameEmail`, a declared shape name, `Literal[...]`,
  `list[T]`, or `T | None`. The aliases `string`, `integer`, `number`, and `boolean` also
  work. See the [type grammar](/reference/variables#the-type-grammar) for composition
  rules and target limits.
</ParamField>

<ParamField path="description" type="string">
  A sentence explaining what the value means. Used to describe the task finish argument
  derived from this variable. Required for `source: conversation`; otherwise omit for no
  extra description. The saved value is still shared only through placeholders.
</ParamField>

<ParamField path="default" type="matching the declared type">
  A scalar starting value, checked against `type`. Shapes, lists, and `NameEmail` take no
  authored default. If omitted, a scalar or shape starts unset and a list starts as `[]`.
  An unset value renders as `none recorded yet.` in a prompt.
</ParamField>

<ParamField path="source" type="string">
  Accepts `call_start`, `conversation` (SLNG only), or a [system call
  fact](/reference/variables#sources). If omitted, the variable has no automatic call-fact
  source; a task, pre-fetch, or session payload can still fill it. Route support is
  checked for system sources. `conversation` requires a description and forbids a default;
  inbound code-target calls require a default for `call_start`.
</ParamField>

<ParamField path="confirm" type="string">
  The name of the task that must hear the caller agree to a pre-fetched value. Until
  confirmed, only that task can read it in a prompt and tools cannot silently use it. If
  omitted, the value is usable as soon as it arrives.
</ParamField>

Full detail on [variables](/reference/variables).

## `shapes`

A pre-fetch cannot fill a shape or a list. Use a task to produce grouped fields;
use separate scalar variables for clock and lookup pre-fetch results. A single
pre-fetch may assign several scalar variables from one result.

```yaml theme={null}
shapes:
  - name: Appointment
    description: One thing being booked, moved or cancelled.
    fields:
      - scheduled_date: Date
      - scheduled_time: Time
```

A top-level list. Each item names one group of fields a variable can use as its
`type:`. Task finish fields derive that type from their assignment
destinations. Full field reference and what it takes to reach the model as
structured data:
[variables](/reference/variables#shapes-groups-fields-into-a-named-type).

## `prefetch`

```yaml theme={null}
prefetch:
  - name: today
    clock: now
    timezone: Europe/Madrid
    assign:
      - booking_date: result.date
      - booking_weekday: result.day_of_week
```

One reading of the clock fills both variables above, at no extra cost: an
entry assigns as many variables as the result has fields, from one call.

An ordered list of facts resolved once per call, before the greeting: the
clock, a fact the call itself carries, or the result of one already-declared
tool with `writes:` declared on the entry. Entries resolve top to bottom, and
one that cannot resolve is skipped rather than failing the call.

A `clock:` entry also carries its own `timezone:`, an IANA zone name. It is
required there, never defaulted, because a container's own clock is UTC, so a
business elsewhere needs it to date a call correctly. It sits on the entry
rather than on the package, because two entries may honestly want two zones.

A `tool:` entry needs `writes: true` or `writes: false`. There is no default,
because a pre-fetch runs unasked on every call. `writes: true` compiles: it is
named in `compile-report.json` and the runbook, rather than printing a
warning.

Full field reference, the ordering rule, every result field a clock gives you,
and what an empty value does to a prompt:
[variables](/reference/variables#prefetch-resolves-a-value-before-the-call-starts).

## `secrets`

```yaml theme={null}
secrets:
  - OPENAI_API_KEY
  - SLNG_API_KEY
  - SIP_TRUNK_HOSTNAME
  - SIP_AUTH_USERNAME
  - SIP_AUTH_PASSWORD
  - SIP_FROM_NUMBER
  - BILLING_PHONE_NUMBER
  - SUPERVISOR_PHONE_NUMBER
```

A list of UPPER\_SNAKE environment variable names. Never values, and never usable
in a `{{template}}`. That list is a worked example: two model keys, the four
names a SIP connection maps, and the two desks the agent can transfer to.

Declare every environment name the generated project reads. That means names
written in tool and connection fields, destination values, literal `os.environ`
reads in local handlers, provider API keys inferred from the model catalogue,
and the names your tracing provider needs. Names the driver or platform
supplies, such as `REDIS_URL` or `DAILY_API_KEY`, stay out. See
[secrets](/reference/secrets).

## `destinations`

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

The symbols an escalation can name. A value is only the UPPER\_SNAKE
name of an environment variable holding an E.164 number or a `sip:` URI, read at
call time. A number written here is refused, because `agent.yaml` is the portable
half of a package:

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

The model never sees a number, and cannot dial one that is not listed here.

## `knowledge`

Each base has a name of 3 to 64 characters using `[a-z0-9_]`. The name is
the map key and becomes the search collection and build folder name.

```yaml theme={null}
knowledge:
  refunds:
    documents: knowledge/refunds
  services:
    documents: knowledge/services
    embed: openai
```

Folders of your own documents an agent can search, so it quotes them instead of
guessing. Each folder is read, split and embedded once when the agent starts, and
held in memory, so content is fixed until the next compile.

<ParamField path="documents" type="string" required>
  Path to a folder inside the package containing `.txt`, `.md`, or `.pdf` documents. No
  folder is inferred.
</ParamField>

<ParamField path="embed" type="string">
  An [embedding service](/build/tools/knowledge#embedding-models). Omitted means `openai`.
  Keyword mode makes no embedding call.
</ParamField>

<ParamField path="mode" type="string">
  Accepts `meaning`, `keyword`, or `hybrid`. Omitted means `hybrid`.
</ParamField>

<ParamField path="chunk_size" type="integer">
  Passage size in tokens, from 1 to 2048. Omitted means `90`.
</ParamField>

<ParamField path="chunk_overlap" type="integer">
  Tokens shared by neighboring passages, from 0 through `chunk_size`. Omitted means `20`.
</ParamField>

<ParamField path="top_k" type="integer">
  Maximum passages returned by a lookup, from 1 to 20. Omitted means `3`.
</ParamField>

<ParamField path="min_score" type="number">
  Minimum accepted result score, from 0 to 1. Omit for no score filtering. Scores are
  similarities, not probabilities.
</ParamField>

The five retrieval fields are per base, because a price list and a prose policy
want different treatment. `mode: keyword` is the one with a structural
consequence: it uses BM25, needs no embedding service, no credential and no
network call, and the emitted image installs no embeddings package. `min_score`
needs care, and needs `mode: meaning` to do anything useful. These are
similarity scores, not probabilities, and in practice they land well below 1,
so a value near 1 returns nothing. On `hybrid` the unscored keyword results
pass through any cutoff. See [Knowledge bases](/build/tools/knowledge) for how
to set it. `top_k` times `chunk_size` is roughly what reaches the model on
every lookup, and the compiler warns above about 1500 tokens.

A tool reaches a base by name, with a
[`knowledge:` block](/build/tools/knowledge), and an agent reaches it by being
given that tool. The base selects a search mode and may set a minimum score.

Full behaviour, every message, and what a lookup gives the model:
[Knowledge bases](/build/tools/knowledge).

## `agents`

```yaml theme={null}
agents:
  appointment_desk:
    instructions: instructions.md
    think: reasoning
    speak: voice
    tools:
      - check_slots
```

<ParamField path="instructions" type="string" required>
  Path to a Markdown prompt inside the package. No prompt is inferred.
</ParamField>

<ParamField path="think" type="string" required>
  Name of an entry in `models.think`. No profile is inferred. Required under
  `architecture: cascade`, and refused under the other two, where one model does
  this job.
</ParamField>

<ParamField path="realtime" type="string">
  Name of an entry in `models.realtime`, in place of `think` and `speak`. Legal
  only under `architecture: realtime`.
</ParamField>

<ParamField path="live" type="string">
  Name of an entry in `models.live`, in place of `think` and `speak`. Legal only
  under `architecture: live`.
</ParamField>

<ParamField path="speak" type="string" required>
  Name of an entry in `models.speak`. No profile is inferred.
</ParamField>

<ParamField path="tools" type="list of strings">
  Names of loaded tool files this agent may call. Omit for no ordinary tools.
</ParamField>

<ParamField path="tasks" type="list of definitions or names">
  Nested task definitions or bare names of tasks defined by another agent. Omit for no
  tasks. See [task fields](/build/orchestration/tasks#every-key-a-task-takes).
</ParamField>

<ParamField path="task_groups" type="list of strings">
  Names from the top-level `task_groups` catalog. Omit for no groups.
</ParamField>

<ParamField path="handoffs" type="list of strings">
  Names from the top-level `handoffs` catalog. Omit for no agent handoffs.
</ParamField>

<ParamField path="escalations" type="list of strings">
  Names from the top-level `escalations` catalog. Omit for no human transfers.
</ParamField>

## `tasks`

A task is nested inside the agent that defines it, not written in a top-level
catalog. Each item of an agent's `tasks:` list is either a full definition or
a bare string naming a task another agent already defines:

```yaml theme={null}
agents:
  appointment_desk:
    tasks:
      - name: customer_record
        when: Identify the caller before handling an appointment request.
        announce: One moment.
        instructions: tasks/customer-record.md
        tools:
          - lookup_customer
        assign:
          - customer_id: result.customer_id

  billing_desk:
    # appointment_desk already defines customer_record. A bare name runs the
    # same task from here, so there is one definition and both agents offer it.
    tasks:
      - customer_record
```

<ParamField path="name" type="string" required>
  A lower snake case name, unique across all agents in the package. To reuse an existing
  task, write its bare name instead of defining it again.
</ParamField>

<ParamField path="instructions" type="string" required>
  Path to the task’s Markdown prompt inside the package. No prompt is inferred.
</ParamField>

<ParamField path="when" type="string">
  The situation the model reads to decide whether to start the task. If omitted, the task
  is a definition only and must be used in a task group; it cannot be attached elsewhere
  by bare name.
</ParamField>

<ParamField path="announce" type="string">
  A fixed spoken line with no `{{placeholders}}`. Omit it for no fixed announcement.
  Required when `opening` is `listen`.
</ParamField>

<ParamField path="opening" type="string">
  Accepts `generate` or `listen`. Omitted means `generate`, so the model writes the
  opening turn. `listen` speaks `announce` and waits for the caller without a model
  request.
</ParamField>

<ParamField path="tools" type="list of strings">
  Names of tool files loaded by the package. Omit for no ordinary tools in this task; it
  does not inherit its owner’s tools.
</ParamField>

<ParamField path="handoffs" type="list of strings">
  Names from the top-level `handoffs` catalog. Omit for no handoffs from this task.
</ParamField>

<ParamField path="assign" type="list of one-key pairs">
  Pairs of `variable: result.field`, including dotted result paths. Use `variable+` to
  append one list item. Omit to save no values; the task can still finish. Types and
  descriptions come from the destination variables.
</ParamField>

<ParamField path="finish" type="list of objects">
  Tools whose successful results finish the task automatically. Each entry requires `tool`
  and a non-empty `success` list of one-key output field/value pairs. Values must be
  declared output enum choices; a list means alternatives. If omitted, the model ends the
  task by calling its generated finish tool.
</ParamField>

<ParamField path="think" type="string">
  A `models.think` entry name. LiveKit only. If omitted, use the entry agent’s think
  profile, even when another agent defines the task.
</ParamField>

<ParamField path="context" type="object">
  The [history fields](/reference/agent-yaml#context). Omit for `history: messages`. A
  returning task restores its owner’s earlier context and adds only completion or unserved
  status.
</ParamField>

`result`, `expect`, and `requires` are retired task fields. A package that
writes one gets a located migration error. Declare variables, save them with
`assign:`, and reference only the values a receiving prompt needs.

Two agents defining a task under the same name is refused:

```text theme={null}
agent.yaml:17: task "verify_customer" is defined by agent "concierge" and again by
  agent "complaint_specialist". A task name is one name across the package: keep
  one definition and let the other agent name it, "- verify_customer"
```

`when` makes a task callable; `assign` says which values it saves.
A task may omit `assign:` and still finish. Every generated finish also takes
optional `unserved_request`; it returns only an `unserved` status to the owner.
Ordinary tool `input:` and `output:` JSON Schemas are unchanged.

### `context`

Used by tasks and by handoffs.

<ParamField path="history" type="string">
  Accepts `full`, `messages`, `last_n`, `summary`, or `reset`. Omitted means `messages`:
  keep speech and remove tool calls with their replies. `summary` is LiveKit only; SLNG
  refuses authored context settings.
</ParamField>

<ParamField path="max_messages" type="integer">
  Required and positive with `history: last_n`. There is no default count. Refused with
  other history modes.
</ParamField>

<ParamField path="summarizer" type="string">
  Required with `history: summary`: a `models.think` entry name. No model is inferred.
  Refused with other history modes.
</ParamField>

<ParamField path="include_tool_calls" type="boolean">
  Accepts `true` or `false`. Omitted behaves as `true` in modes that keep tool records. It
  cannot add tool records to `messages` or `reset`. Explicit `false` is LiveKit only.
</ParamField>

| `history`  | What the receiver gets                                                             |
| ---------- | ---------------------------------------------------------------------------------- |
| `messages` | Caller and agent speech; tool calls and replies are removed together               |
| `full`     | Earlier speech and paired tool records, without earlier instructions               |
| `last_n`   | The newest `max_messages` entries, keeping tool calls paired with replies          |
| `reset`    | No earlier conversation, including the sentence that triggered the task or handoff |
| `summary`  | A generated summary of earlier conversation, using `summarizer`                    |

`include_tool_calls: true` does not add tool records to `messages` or `reset`.
To keep old tool results, use `full` or `last_n`. Pipecat supports those two
modes plus `messages` and `reset`; it refuses `summary` and explicit
`include_tool_calls: false`. SLNG refuses task and handoff context settings.

For example, these are two separate context blocks:

```yaml theme={null}
context:
  history: last_n
  max_messages: 8
```

```yaml theme={null}
context:
  history: summary
  summarizer: reasoning
```

History controls entry into the receiver. A returning task always restores
the owner's earlier conversation and adds only a status. Saved values reach
either prompt only through explicit placeholders. See
[Reduce context sharing step by step](/best-practices/context-scope).

## `task_groups`

An agent runs a group by naming it in its own `task_groups:` list. The group
carries its own `when:`, the situation the model reads to decide whether to
run it:

```yaml theme={null}
agents:
  appointment_desk:
    task_groups:
      - appointment_flow

task_groups:
  appointment_flow:
    when: The caller wants to book, reschedule, or cancel an appointment.
    steps:
      - identify_customer
      - select_appointment
    context_scope: shared
    then: return
    merge: results
```

<ParamField path="steps" type="list of strings or objects" required>
  One or more task names, in execution order. An object requires `task` and may set
  `skip_when_confirmed` to a variable that task confirms. Omit that condition to run the
  step every time. An empty or absent steps list is refused.
</ParamField>

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

<ParamField path="announce" type="string">
  One fixed spoken line, with no `{{placeholders}}`, when the group starts. Omit for no
  announcement.
</ParamField>

<ParamField path="context_scope" type="string" required>
  Accepts `shared` or `isolated`. There is no default. Each member still applies its own
  `context.history`; an isolated group cannot be widened by a member’s `full`.
</ParamField>

<ParamField path="then" type="string" required>
  Accepts `return`, `transfer`, or `end`. There is no default.
</ParamField>

<ParamField path="then_target" type="string">
  Required with `then: transfer`: an existing agent name. Refused for `return` or `end`;
  there is no inferred destination.
</ParamField>

<ParamField path="merge" type="string">
  Only `results` is accepted. Omission also means `results`.
</ParamField>

The group decides whether members share the group's running conversation. Each
member still applies its own `context.history`. An isolated group cannot be
widened by a member's `full`.

## `handoffs`, `escalations`

Everything the model can hand the caller to and not get back, in one block
per kind. The block an entry is written in is what it is, so there is no
`kind:` field.

### `handoffs`

The conversation becomes another agent, and never comes back.

<ParamField path="to" type="string" required>
  An existing agent name. The conversation moves to that agent and does not return. No
  destination is inferred.
</ParamField>

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

<ParamField path="announce" type="string">
  Exact text spoken before handing over. Omit for a silent handoff.
</ParamField>

<ParamField path="context" type="object">
  The [history fields](/reference/agent-yaml#context). Omitted means `messages`. Saved
  values are visible only where the receiving prompt names them.
</ParamField>

Saved state stays with the call, but the receiving model sees only values its
prompt references. A reset handoff gets no automatic briefing or triggering
sentence.

`announce` is exact spoken text, not a model instruction: write the short
sentence the caller should hear. Omit it for a silent handoff.

### `escalations`

Puts the caller through to a person.

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

A human transfer names its shape with a block, so a warm only field cannot be
written on a cold transfer:

| Block  | Fields                                                      |
| ------ | ----------------------------------------------------------- |
| `cold` | `destination`, `ring_timeout`, `on_unavailable`             |
| `warm` | `destination`, `briefing`, `ring_timeout`, `on_unavailable` |

`on_unavailable` is `return_to_caller` or `hangup`; omitted means
`return_to_caller`. `ring_timeout` must be a positive Go duration. When omitted,
Pipecat uses 25 seconds. LiveKit leaves the value unset, so the LiveKit platform
default applies.
Pipecat `cloud-websocket` requires explicit `on_unavailable: hangup`; it cannot
reconnect the original media stream.
`destination` is a symbol resolved in the top level
[`destinations:`](#destinations) map above.

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

Whether a route can carry the shape you asked for is decided by the connection.
A warm transfer is supported only on LiveKit `sip`; Unmute does not support warm
transfer on any Pipecat target.
A warm transfer on a route with no leg to move is refused by name:

```text wrap 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
```

## `tools`

```yaml theme={null}
tools:
  - check_slots
  - end_call
```

Which `tools/<name>.yaml` files to load. Availability is decided by the `tools`
list on each agent and task.

This top-level list and every agent or task list contain names only.
**Define each tool once.** Put its full contract in `tools/<name>.yaml`; never
inline `description`, `input`, `output`, `inject`, or an execution block under
a `tools:` list in `agent.yaml`.

### Tool files

One file per tool, in `tools/`. The top level is the contract with the model;
exactly one execution block says how it runs.

<ParamField path="description" type="string">
  Required for local, webhook, and knowledge tools: explains when the model should call
  the tool. Builtins use their registry description if omitted; hosted `slng` tools
  inherit their published description. Refused on MCP sources.
</ParamField>

<ParamField path="input" type="object">
  Required for authored local and webhook contracts: a JSON Schema with `type: object`
  describing model arguments. Refused on builtin, MCP, knowledge, and hosted `slng` tools,
  which own their schemas.
</ParamField>

<ParamField path="output" type="object">
  An author-side JSON Schema with `type: object`. Omit for no declared result schema. Used
  by assignments and success checks; it is not a general runtime result validator or a
  model prompt. Refused on builtin, MCP, knowledge, and hosted `slng` tools.
</ParamField>

<ParamField path="inject" type="list of one-key pairs">
  Hidden argument/value pairs. Values are scalars or strings with `{{variable}}`
  placeholders. Omit to inject nothing. Legal on local, webhook, and hosted `slng` tools;
  builtin `send_sms` requires its literal `from_number` setting.
</ParamField>

<ParamField path="interruption" type="string">
  Accepts `provider_default`, `continue`, or `cancel`. Omitted means `provider_default`.
  Refused on MCP sources. Target support is listed above.
</ParamField>

<ParamField path="effect" type="string">
  Accepts `returns_data` or `ends_conversation`. Omitted means `returns_data`, except
  builtins whose effect comes from the registry. Refused on MCP and knowledge tools.
</ParamField>

<ParamField path="announce" type="string">
  A fixed spoken sentence with no `{{variables}}`. Omit for no announcement. Legal on
  local, webhook, knowledge, and hosted `slng` tools.
</ParamField>

| Execution block   | Fields                                                                                                                                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `local`           | optional `handler`; omitted means `tools/<tool-name>.py`. Optional `dependencies`, exact `name==version` pins for a SLNG per-tool environment; refused on LiveKit and Pipecat, which build one dependency list for the whole project |
| `webhook`         | `url_env` is required on LiveKit and Pipecat; `base_url` is a legacy literal HTTPS base that neither code target reads. SLNG refuses authored webhooks; optional `path` and `auth`; a non-empty path starts with `/`                 |
| `mcp`             | required `url_env`; optional `server` (the platform's name for it, when it differs from this tool's name), `transport`, `auth`, and non-empty unique `tools` entries                                                                 |
| `builtin`         | required `id`; optional `instructions`                                                                                                                                                                                               |
| `knowledge`       | required `base`, naming an entry in [`knowledge:`](#knowledge)                                                                                                                                                                       |
| `slng`            | a published tool name, or the [legacy hash block](/build/tools/hosted#the-legacy-form-still-loads)                                                                                                                                   |
| `client`          | no fields; write `client: {}`; gated on every target today                                                                                                                                                                           |
| `provider_hosted` | no fields; write `provider_hosted: {}`; gated on every target today                                                                                                                                                                  |

A `knowledge` file takes no `input`, `output`, `inject` or `effect`: the tool owns
both sides of its contract, taking one string and returning passages.

An `mcp` file is the block and nothing else. The seven contract fields above are
all illegal on one, because the server describes its own tools.

<ParamField path="transport" type="string">
  Accepts `sse` or `streamable_http`. Inside `mcp`, omission uses the runtime’s
  URL-based transport choice; a URL ending in `/mcp` selects streamable HTTP.
</ParamField>

Builtin ids: `end_call`, `send_sms`. `end_call` compiles on every target.
`send_sms` is a capability SLNG curates, so it compiles on the `slng` target
only. It takes one setting from the package: `inject:` with a single
`from_number`. That is the sender, as a literal number in international format
starting with a plus sign. The model supplies the recipient and the body
itself, and SLNG reads the Twilio credentials from your vault under
`TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN`, which `unmute deploy` checks.

`webhook.auth`:

<ParamField path="type" type="string" required>
  Accepts `bearer` or `api_key`. Required when `auth` is present; no scheme is inferred.
</ParamField>

<ParamField path="token_env" type="string" required>
  An UPPER\_SNAKE environment variable name holding the token, never the token itself.
  There is no default.
</ParamField>

<ParamField path="header" type="string">
  An HTTP header name, legal only with `type: api_key`. Omitted means `X-API-Key`. Bearer
  authentication uses `Authorization: Bearer`.
</ParamField>

## `conversation`

```yaml theme={null}
conversation:
  greeting:
    speaks_first: agent
    text: "Hi, how can I help?"
  interruption:
    enabled: true
```

<ParamField path="greeting" type="object">
  Opening behavior using `speaks_first` and optional `text`. If omitted, LiveKit and
  Pipecat generate an opening line. SLNG requires an explicit greeting with text.
</ParamField>

<ParamField path="greeting.speaks_first" type="string">
  Required when `greeting` is present. Accepts `agent` or `user`; there is no default
  inside an authored block. SLNG requires `agent`.
</ParamField>

<ParamField path="greeting.text" type="string">
  The exact opening line, with eligible variable placeholders. Requires `speaks_first:
      agent`. If omitted on a code target, the model writes the greeting; SLNG requires text.
</ParamField>

<ParamField path="interruption" type="object">
  Barge-in settings. If omitted, interruptions remain enabled. Pipecat phone routes also
  protect the greeting by default.
</ParamField>

<ParamField path="interruption.enabled" type="boolean">
  Required when `interruption` is present. Accepts `true` or `false`. There is no default
  inside an authored block.
</ParamField>

<ParamField path="interruption.protect" type="list of strings">
  Accepts `greeting`, `tool_calls`, or both on Pipecat. Omit to protect the greeting on a
  Pipecat phone route and nothing on a browser route. Set `[]` to protect nothing.
  Non-empty protection is refused with `enabled: false`.
</ParamField>

<ParamField path="interruption.minimum_words" type="integer">
  A positive count sets how many words count as an interruption on code targets. Omitted
  or zero leaves the runtime’s default word threshold.
</ParamField>

<ParamField path="interruption.ignore_phrases" type="list of strings">
  Phrases that do not interrupt on code targets. Omit for no authored ignored phrases.
</ParamField>

<ParamField path="inactivity" type="object">
  Optional `nudge_after` and `end_after` timers. Omit for no authored inactivity timers.
  SLNG refuses this block.
</ParamField>

<ParamField path="inactivity.nudge_after" type="string">
  A positive Go duration, such as `15s`, before an idle nudge. Omit for no authored nudge
  timer.
</ParamField>

<ParamField path="inactivity.end_after" type="string">
  A positive Go duration, such as `45s`, before ending an idle call. Omit for no authored
  idle end timer.
</ParamField>

<ParamField path="max_duration" type="string">
  A positive Go duration, such as `15m`, limiting a call on code targets. Omit for no
  package-defined limit. SLNG refuses this field.
</ParamField>

<ParamField path="thinking_audio" type="string">
  Accepts `none` or `subtle`. Omitted means no thinking audio. `subtle` is LiveKit only.
</ParamField>

## `tracing`

```yaml theme={null}
tracing:
  provider: langfuse
```

### Tracing fields

<ParamField path="provider" type="string" required>
  Accepts `langfuse` or `coval`. Required inside `tracing`; no provider is inferred. Omit
  the whole `tracing` block to disable tracing. Supported on LiveKit and Pipecat; refused
  on SLNG.
</ParamField>

`provider` takes one of two values, `langfuse` or `coval`. Tracing works on both
targets.

| Provider   | Required environment names                                        |
| ---------- | ----------------------------------------------------------------- |
| `langfuse` | `LANGFUSE_BASE_URL`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` |
| `coval`    | `COVAL_API_KEY`                                                   |

LiveKit uses the room name as the Langfuse session ID. Pipecat uses the runner
session ID as both its conversation ID and the Langfuse session ID.
Pipecat tracing owns the process OpenTelemetry provider and startup fails if another SDK provider is installed first.

With `coval`, each trace is attached to the Coval simulation that placed the
call, and the agent finds that simulation ID on the call itself. See
[tracing](/tracing/overview) for how the ID reaches the agent on each target.

Traces can contain caller speech, model input and output, and tool arguments and results.
Use only fake identities and fake customer data for release tests. Use a separate
project on your tracing provider for those tests, and do not send real customer
data until its access and retention rules are approved.

## `channels`

```yaml theme={null}
channels:
  web:
    kind: realtime_audio
  phone:
    kind: telephony
    inbound: true
    outbound: true
```

<ParamField path="kind" type="string" required>
  Accepts `realtime_audio` or `telephony`. No kind is inferred.
</ParamField>

<ParamField path="inbound" type="boolean">
  Accepts `true` or `false` for telephony only. Omitted does not enable inbound calls. At
  least one of `inbound` and `outbound` must be `true`.
</ParamField>

<ParamField path="outbound" type="boolean">
  Accepts `true` or `false` for telephony only. Omitted does not enable outbound calls.
  Required as `true` for warm transfer or voicemail handling.
</ParamField>

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

<ParamField path="on_voicemail" type="string">
  Accepts `hangup` or `leave_message` where supported by the route. Requires `kind:
      telephony` and `outbound: true`. Omit for no package-defined voicemail action.
</ParamField>

### Three rules a telephony channel brings with it

All three are enforced, fail validation, and are easier to read here than to
meet by trial and error.

**At least one direction must be enabled.** A channel with both `inbound: false`
and `outbound: false` has no call leg and is refused.

**A warm transfer needs `outbound: true`.** A warm transfer dials the
destination itself, so the agent places a call, and a channel that only receives
them cannot:

```text theme={null}
channel "phone" needs outbound: true; a warm transfer places a call to its destination
```

Write `outbound: true` even on a line people only ring in on. It describes what
the agent does, not what the number is for.

**`capacity.peak_starts_per_second` becomes required.** The moment any channel
is `telephony`, the field stops being optional and must be positive:

```text theme={null}
capacity.peak_starts_per_second must be positive for telephony
```

Calls arrive in bursts and each one starts a session, so a rate is the number
the compiler sizes workers from. One is a fine answer for a first line.

## `capacity`

Capacity is required for LiveKit and Pipecat, the two code targets. A telephony channel also makes `peak_starts_per_second` required.

```yaml theme={null}
capacity:
  peak_sessions: 5
  max_sessions: 10
  avg_session_duration: 5m
```

<ParamField path="peak_sessions" type="integer" required>
  Expected concurrent sessions at peak; must be positive. No estimate is inferred.
</ParamField>

<ParamField path="max_sessions" type="integer" required>
  Maximum concurrent sessions; must be positive and at least `peak_sessions`. No ceiling
  is inferred.
</ParamField>

<ParamField path="peak_starts_per_second" type="number">
  Required and positive for any telephony channel. Omit on a browser-only package to
  declare no call-start rate.
</ParamField>

<ParamField path="avg_session_duration" type="string" required>
  A positive Go duration, such as `5m`. No duration is inferred.
</ParamField>

The compiler turns these into worker and quota numbers, marked
`[unbenchmarked]`.

## Reachability

Models are a palette, so an unused model entry is legal. Other declarations must
be reachable from `entry_agent`. An unused handoff, escalation, destination,
task group, or non-entry agent is a build error, and so is a tool no agent's
`tools:` list names. Attach it to the reachable graph or remove it.

A task is reachable a different way, because it is defined inside the agent
that lists it rather than in a top-level catalog. A task with no `when:` and no
task group naming it in `steps:` is a build error too. Give it a `when:` so an
agent can decide to run it, or list it as a step of a task group that is
reached.

## Where to go next

<Columns cols={2}>
  <Card title="targets.yaml" icon="file-code" href="/reference/targets-yaml">
    The runtime half.
  </Card>

  <Card title="Models" icon="list" href="/models/stt">
    Which `provider:` values each target accepts.
  </Card>
</Columns>
