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

# Tools

> What a tool is, the eight ways one can run, how each target treats them, and how you attach one to an agent.

A tool is two things: a contract the model sees, and something that runs when the
model calls it. Both live in one file, `tools/<name>.yaml`.

The file stem is the tool name. It must be lower snake case and cannot start with
an underscore.

On this page:

* [The eight execution blocks](#the-eight-execution-blocks) - the ways a tool runs
* [How each target treats a tool](#how-each-target-treats-a-tool) - what each one supports
* [Choosing a kind](#choosing-a-kind) - stop at the first fit
* [Fields inside each execution block](#fields-inside-each-execution-block) - every key, per block
* [Which fields the block allows](#which-fields-the-block-allows) - the shared contract fields
* [The three behavior fields](#the-three-behavior-fields) - announce, effect, interruption
* [Define once, attach by name](#define-once-attach-by-name) - one definition, two lists
* [Troubleshooting](#troubleshooting) - the refusals you will meet

```yaml tools/find_slots.yaml expandable theme={null}
description: >-
  List Sage and Stone slots for one service and date, and the caller's own
  bookings. Call this before offering a time and before changing a booking.

input:
  type: object
  properties:
    service:
      type: string
      enum:
        - haircut
        - hair-color
        - blowout
    date:
      type: string
      description: Preferred date in YYYY-MM-DD form

local:
  handler: tools/find_slots.py
```

The top of the file is the contract. `description` and `input` are everything
the model knows about this tool. Write the description as an instruction rather
than a label, and let the schema do real work: the `enum` above means the model
cannot ask for a service the salon does not offer.

The schema is also the whole argument list. Generated Pipecat tools turn an
extra argument into a normal corrective tool result, so the model can retry
with only the declared fields. A handler failure before any result is returned
the same way, without exposing the private exception to the model or leaving a
call stuck in progress. Keep workflow prerequisites in the prompt; do not make
their names look like extra tool inputs in the description.

The one block near the bottom is the execution: it says how the tool runs. Every
tool file has exactly one.

## The eight execution blocks

| Block              | The tool is                                            | Where it is taught                        |
| ------------------ | ------------------------------------------------------ | ----------------------------------------- |
| `webhook:`         | an HTTP call to a URL named by an environment variable | [Webhook tools](/build/tools/webhook)     |
| `local:`           | a Python function in your package                      | [Python tools](/build/tools/python)       |
| `mcp:`             | a remote MCP server that offers its own tools          | [MCP servers](/build/tools/mcp)           |
| `builtin:`         | a tool the runtime already has, selected by id         | [Prebuilt tools](/build/tools/prebuilt)   |
| `slng:`            | a tool the SLNG platform already hosts                 | [Hosted tools](/build/tools/hosted)       |
| `client:`          | a tool the caller's own application fulfils            | gated, see below                          |
| `provider_hosted:` | a tool the model provider runs itself                  | gated, see below                          |
| `knowledge:`       | a search over a folder of your own documents           | [Knowledge bases](/build/tools/knowledge) |

Exactly one, and the compiler holds you to it. A file carrying two blocks, and a
file carrying none, are both refused with their line: see
[Troubleshooting](#troubleshooting).

### The two gated blocks

`client:` and `provider_hosted:` exist in the schema and no target emits them. The
capability table denies both on every provider, so writing one fails with the
target named:

```text theme={null}
livekit: LiveKit client tools are not proven by its driver
```

```text theme={null}
pipecat: Pipecat provider-hosted tools are not proven by its driver
```

They are listed here so you know the names mean nothing yet, and so a refusal you
meet reads as a decision rather than a bug.

Both blocks have no fields, but YAML still needs an explicit body: write
`client: {}` or `provider_hosted: {}`.

## How each target treats a tool

| Tool kind          | LiveKit | Pipecat | SLNG   |
| ------------------ | ------- | ------- | ------ |
| `webhook:`         | yes     | yes     | yes    |
| `local:`           | yes     | yes     | **no** |
| `builtin:`         | yes     | yes     | yes    |
| `mcp:`             | yes     | yes     | yes    |
| `slng:`            | yes     | yes     | yes    |
| `knowledge:`       | yes     | yes     | **no** |
| `client:`          | no      | no      | no     |
| `provider_hosted:` | no      | no      | no     |

The same tool file compiles to three different runtimes, and they do not all
support the same things. This table is the capability table, which is what the
compiler actually reads, so a `no` here is a refusal you will meet at compile time
rather than a surprise on a call.

Three of those rows are worth a sentence.

**`local:` and `webhook:` no longer reach SLNG.** The platform owns a tool's
code, version and gate pipeline, and unmute creates no tool there. A tool your
SLNG organisation already has is reached with `slng:`; a brand new one starts in
the SLNG dashboard. Both blocks work exactly as before on LiveKit and Pipecat.

**`slng:` needs nothing local, and works everywhere because the definition
travels when it has to.** SLNG resolves a hosted reference by name at deploy
time: no mirror, no hash, no `unmute pull`. LiveKit and Pipecat build and run
the tool themselves, so they need a real copy; `unmute pull` is what fetches
one, and for a code tool its module, into your package, with no network
needed again after that. One limit is not in this table: a hosted tool that
declares Python dependencies is refused on LiveKit and Pipecat, which build
one dependency list for the whole project. See
[Hosted tools](/build/tools/hosted).

**`knowledge:` needs a runtime of ours to live in.** LiveKit and Pipecat compile to
a Python project, so the documents ride in the image and the search runs in the
process. The SLNG target writes a deployment body and SLNG runs the agent, so
there is no image to carry a folder and no process of ours to index it in. Put the
facts in the agent's instructions, or compile to a code target.

**`client:` and `provider_hosted:` are names with nothing behind them.** They exist
in the schema and no target emits them. They are documented so a refusal reads as
a decision rather than a bug.

### Attaching a tool to a task

| Tool kind on a task | LiveKit | Pipecat                      | SLNG            |
| ------------------- | ------- | ---------------------------- | --------------- |
| `mcp:`              | yes     | **no**, list it on the agent | yes             |
| `knowledge:`        | yes     | **no**, list it on the agent | **no**          |
| everything else     | yes     | yes                          | no tasks at all |

A tool listed on a task rather than on an agent is a narrower thing, and two kinds
are not available everywhere.

Pipecat's reason is the same for both: a task tool there is a flows handler holding
a `FlowManager`, not a decorated function holding `FunctionCallParams`. The SLNG
target writes one agent with one prompt, so it has no tasks to scope anything to.

### The behavior fields

| Field                                        | LiveKit | Pipecat | SLNG   |
| -------------------------------------------- | ------- | ------- | ------ |
| `announce`                                   | yes     | yes     | yes    |
| `inject`                                     | yes     | yes     | yes    |
| `auth`                                       | yes     | yes     | yes    |
| `interruption` other than `provider_default` | warns   | yes     | **no** |

LiveKit runs a tool to completion, so a per-tool interruption value has nothing to
act on and it says so. SLNG owns its own turn taking and has no per-tool setting at
all.

<Note>
  Every refusal above names the target and tells you what to do instead. If you get
  one you do not understand, `unmute validate` prints the same message with the tool
  name attached.
</Note>

## Choosing a kind

Work down this list and stop at the first one that fits.

| If the tool needs to                                    | Use          | Why not the others                                                                   |
| ------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------ |
| end the call                                            | `builtin:`   | the runtime already has it; do not write your own                                    |
| answer from your own documents                          | `knowledge:` | a webhook to a search service is a network hop and a service to run                  |
| call an API you already have                            | `webhook:`   | no code of yours to deploy, and the URL is an environment variable                   |
| run logic, or touch something with no HTTP API          | `local:`     | it ships inside the image, so it costs no network hop                                |
| offer a whole catalogue of tools someone else maintains | `mcp:`       | one block instead of a file per tool                                                 |
| run a tool SLNG already hosts                           | `slng:`      | the platform owns its code and version, so there is nothing of yours to keep in step |

**Prefer fewer tools.** Every tool is text in the model's context on every turn,
and two tools with overlapping descriptions is the most common reason a model calls
the wrong one. A tool the agent list does not name is not offered at all, which is
the cheapest way to narrow a choice.

**Write the description as an instruction, not a label.** `description` and
`input` are the whole of what the model knows. Say when to call it, and say when
not to.

## Fields inside each execution block

Each tool file declares exactly one execution block. Its page lists the fields,
types, required conditions, and defaults:

* [Webhook](/build/tools/webhook#the-block): `url_env`, `base_url`, `path`, and `auth`.
* [Python](/build/tools/python#the-block): `handler` and `dependencies`.
* [MCP](/build/tools/mcp#the-block): `server`, `url_env`, `transport`, `auth`, and `tools`.
* [Prebuilt](/build/tools/prebuilt#the-block): `id` and `instructions`.
* [Knowledge](/build/tools/knowledge#the-knowledge-block-on-a-tool): `base`.
* [Hosted](/build/tools/hosted#hosted-reference-fields): `slng` names a published tool.

`client` and `provider_hosted` take an empty object and are gated on every target.
SLNG accepts published tool references; it refuses authored local and webhook bodies.

## Which fields the block allows

The contract fields are shared, with one exception that matters:

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

An `mcp:` file carries none of them, because the server owns each tool's contract.
A `builtin:` file takes no `input` or `output`. The registry supplies its contract
and default description, and your `description` is added on top if you write one.
A `knowledge:` file takes no `input`, `output`, `inject` or `effect` either: the
tool asks for one string and returns passages, so there is nothing to describe and
nothing to merge into. It does take `description` and `announce`, and you should
write both.

## The three behavior fields

```yaml theme={null}
interruption: provider_default
effect: returns_data
announce: Let me check the calendar.
```

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

All three are optional, and each is honored differently per target. Pipecat maps
`interruption` onto its own cancel-on-interruption setting, while LiveKit runs
tools to completion, so a non-default value warns there. `effect` is fixed by the
registry on a `builtin:` tool, and a conflicting value fails.

### Using `announce:`

The line is spoken once, as the tool starts, before the tool's own work. Nothing
waits for it to finish playing, so the caller hears the tool's answer no later
than they would without the line. It covers the wait, it does not add one.

**Reach for it when the tool is slow enough that the silence reads as a dropped
call.** A request to a service you do not control, a handler that queries a
database or a calendar. If the tool answers instantly, the announcement is just a
sentence in the way.

**Do not put one on every tool.** Two tools firing back to back means the caller
hears two announcements in a row, which is worse than the pause you were trying
to cover.

<AccordionGroup>
  <Accordion title="Writing the line">
    The sentence is fixed, so it is spoken word for word every time that tool
    runs. Everything else follows from that.

    | Write this                         | Not this                                                             | Why                                          |
    | ---------------------------------- | -------------------------------------------------------------------- | -------------------------------------------- |
    | `Let me check the calendar.`       | `Let me find you some great times!`                                  | you do not know yet what you will find       |
    | `One moment while I look that up.` | `I'm querying the availability API.`                                 | the caller does not have your architecture   |
    | `Give me one second.`              | `Please hold while I retrieve your account details from our system.` | it should be shorter than the wait it covers |

    Pick something that still sounds fine the third time. If the model calls a
    tool repeatedly within one conversation, that is an argument for not
    announcing it at all.
  </Accordion>

  <Accordion title="The mistake worth avoiding">
    If your instructions already tell the agent to say it is checking something,
    delete that line when you add `announce:`. Otherwise the model speaks its own
    version, the tool speaks the fixed one, and the caller hears both.
  </Accordion>

  <Accordion title="The rules">
    | Rule                                                 | What happens if you break it                                                  |
    | ---------------------------------------------------- | ----------------------------------------------------------------------------- |
    | legal on `webhook:`, `local:` and `knowledge:` only  | refused by tool name; an `mcp:` file is refused at load, with the line number |
    | a fixed sentence, no `{{variables}}`                 | refused by tool name                                                          |
    | a blank value reads as absent                        | nothing is spoken and nothing is emitted                                      |
    | on Pipecat, list the tool on an agent, not on a task | refused by name, telling you to move it                                       |

    It adds no new interruption rule: if the caller speaks over the line, the
    tool's own `interruption:` value decides what happens. LiveKit emits the line
    for a tool listed on an agent or on a task. A target whose driver has no
    lowering for the field fails validation with that driver's own reason, rather
    than dropping the line quietly.
  </Accordion>
</AccordionGroup>

## Define once, attach by name

**Define each tool once.** The full definition exists only in
`tools/<name>.yaml`: `description`, `input`, optional `output` and `inject`, and
one execution block. A local handler lives beside it in `tools/<handler>.py`.
Do not put any of those fields in `agent.yaml`.

Every `tools:` entry in `agent.yaml` is a string name:

* the top-level list loads `tools/<name>.yaml`,
* `agents.<name>.tools` grants an agent access, and
* `agents.<name>.tasks[].tools` grants one nested task access.

```yaml agent.yaml theme={null}
tools:
  - check_slots
  - cancel_appointment

agents:
  appointment_desk:
    instructions: instructions.md
    think: reasoning
    speak: voice
    tools:
      - check_slots
      - cancel_appointment
```

For a task-scoped tool, attach the same loaded name to the task instead,
where the task is nested inside its agent:

```yaml agent.yaml theme={null}
agents:
  appointment_desk:
    tasks:
      - name: find_slot
        instructions: tasks/find-slot.md
        tools:
          - check_slots
```

The agent and task lists are visibility scopes. Attach a tool only where it is
called; do not grant it to both unless both really call it. Never replace a
name with an inline mapping of `description`, `input`, `output`, `local`, or
`webhook`.

A tool's optional `output:` remains its own JSON Schema. A task does not copy
that schema. Its `assign:` list derives finish fields from destination
variables and saves only the values needed later.

## Troubleshooting

### A tool file has two execution blocks, or none

Every tool file runs exactly one way, so both shapes are refused. Two blocks:

```text theme={null}
tools/check_slots.yaml:7: two execution blocks (local and webhook): a tool runs
  exactly one way
```

None at all, where the message is also the list:

```text wrap theme={null}
tools/check_slots.yaml: no execution block: add one of webhook, local, mcp, builtin,
  client, provider_hosted, knowledge, slng
```

**Fix:** keep the block the tool really needs and delete the other, or add one of
the blocks the second message lists.

### A tool you wrote is never offered

A file in `tools/` that the package level list does not name is not loaded at all,
and nothing complains.

**Fix:** check the top-level `tools:` list in `agent.yaml` first, then the agent's
or task's own list.

### `output:` is refused on an slng target

It used to be allowed there, for a reason. The compiler turned it into a pydantic
`Output` class inside the code it uploaded, and SLNG read the tool's result shape
off that class. unmute uploads no code now, so the field reaches nothing and is
refused rather than dropped.

**Fix:** delete the field from the tool file. A tool SLNG hosts carries its own
`Output` model, written where the tool was written. Full story in [Python
tools](/build/tools/python#what-the-slng-sandbox-expects) and [Hosted
tools](/build/tools/hosted).

## Where to go next

<Columns cols={2}>
  <Card title="Webhook tools" icon="webhook" href="/build/tools/webhook">
    The everyday case: call your own API.
  </Card>

  <Card title="Python tools" icon="file-code" href="/build/tools/python">
    When the call needs code of your own.
  </Card>

  <Card title="MCP servers" icon="plug" href="/build/tools/mcp">
    Offer a whole server's tools at once.
  </Card>

  <Card title="Prebuilt tools" icon="package" href="/build/tools/prebuilt">
    The ones the runtime already has.
  </Card>

  <Card title="Knowledge bases" icon="book-open" href="/build/tools/knowledge">
    Answer from a folder of your own documents.
  </Card>
</Columns>
