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

# Tasks

> Run one step of a call with its own prompt and tools, save typed values, and return control.

A task is one step of a call. It has its own instructions and tools. When it
finishes, control returns to the agent that called it. That agent is the owner.
Inside a task group, each task is one step of the group.

To the model, a task is one more entry in its function list, next to the
agent's tools. Calling it switches to the task's prompt and tools. When the
task finishes, the owner's prompt and tools come back.

Use a task when one bounded piece of work needs a smaller prompt or a smaller
tool list. Use a [task group](/build/orchestration/task-groups) when several
tasks must run in order. Use a [handoff](/build/orchestration/handoffs) when a
different agent should own the rest of the call.

New to tasks? [Your first task](/build/orchestration/first-task) adds one to
the package `unmute init` writes, one key at a time.

On this page:

* [Quickstart](#quickstart) - one task, start to finish
* [Every key a task takes](#every-key-a-task-takes) - all eleven
* [Say what success looks like](#say-what-success-looks-like) - let the tool end the step
* [Choose conversation history](#choose-conversation-history) - what the task can read
* [What returns](#what-returns) - and what does not
* [Advanced](#advanced) - lists, sharing, opening and announcing
* [Troubleshooting](#troubleshooting) - the two that come up most

## Quickstart

A task is one entry under an agent's `tasks:`. This one identifies the caller
and saves what it learns:

```yaml agent.yaml theme={null}
variables:
  customer_id:
    type: Id

agents:
  appointment_desk:
    tasks:
      - name: verify_customer
        when: Identify the caller before handling an appointment.
        instructions: tasks/verify-customer.md
        tools:
          - lookup_customer
        assign:
          - customer_id: result.customer_id
```

```sh theme={null}
unmute validate my-agent
```

Four keys carry it: **what it is** (`name`, `instructions`), **when to run it**
(`when`), **what it can use** (`tools`), and **what it saves** (`assign`).

## Declare a task

```yaml agent.yaml theme={null}
variables:
  customer_id:
    type: Id
    description: The customer record selected for this call.
  customer_name:
    type: string
    description: The customer's name.

agents:
  appointment_desk:
    # the agent's other keys stay as they are
    tasks:
      - name: verify_customer
        when: Identify the caller before handling an appointment.
        instructions: tasks/verify-customer.md
        tools:
          - lookup_customer
        assign:
          - customer_id: result.customer_id
          - customer_name: result.customer_name
```

### Every key a task takes

Two are required. The rest are how you shape the step.

<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="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="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. The destination variable already owns the type
  and description, so the task does not repeat them.
</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="handoffs" type="list of strings">
  Names from the top-level `handoffs` catalog. Omit for no handoffs from this task.
</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="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>

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

There is no `tasks:`, no `task_groups:` and no `escalations:` key on a task. A
task cannot run another task or reach a person directly, and that is structure
rather than a rule to remember: there is no key to write it in.

## How a task finishes

Every task gets a `finish` call. The model uses it to say the task is done and
to hand over the values in `assign:`. The `finish:` key is different: it names
tools whose own result ends the task, so the model does not have to make that
call. The next section covers the key.

The `finish` call for this example takes `customer_id` and `customer_name`. It
checks every value before saving any of them. If one value is invalid, the task
stays open and no value changes.

A task that saves nothing omits `assign:`. The model can still call `finish`.

## Say what success looks like

How does the task know it worked? Without `finish:`, the model decides. It runs
a tool, reads the result, and calls `finish`. With `finish:`, the tool's own
result decides. You name the tools that end the task, and what a success from
each one looks like.

|                | Who decides the task is finished                  |
| -------------- | ------------------------------------------------- |
| no `finish:`   | the model, by calling `finish`                    |
| with `finish:` | the tool's own result, checked against `success:` |

This also removes a model request. A task that runs a tool and then asks the
model to call `finish` spends a whole request on a decision the tool already
made. The saving grows with the number of tasks a call runs through.

```yaml agent.yaml theme={null}
      - name: manage_booking
        instructions: tasks/booking.md
        tools:
          - find_slots
          - save_booking
        finish:
          - tool: save_booking
            success:
              - status: booked
          - tool: save_booking
            success:
              - status: cancelled
        assign:
          - appointment: result.appointment
```

When one of those tools returns a result in which every `success:` field holds
one of its allowed values, the task saves its `assign:` from that result and
ends. The result never reaches the model. Anything else is an ordinary result:
it goes back to the model and the task stays open. That is what keeps a failed
booking a conversation rather than a saved one.

A `success:` item is `field: value`, or a list of values that are alternatives.
Items on different fields are all required. Every value has to be one the tool
declares in its own output `enum:`, so a typo is a refusal at compile time
rather than a task that never ends by itself.

`result.<field>` in `assign:` names an output property of every listed tool, so
one tool returning `appointment` and another returning nothing is refused.

Four things follow from a task that ends on its tool:

* **The listed tools close.** Once one of them succeeds, none of the task's
  `finish:` tools runs again in that run of the task. A new booking is a new
  request from the caller, and a new run of the task.
* **The `finish` call is still there.** The model uses it for a request the
  task cannot serve, for values the task already holds, and for recording a
  result whose save was refused. That repair keeps the values the tool
  returned, so a reference the tool handed back is never retyped by the model.
* **The appended text changes.** The text Unmute adds to your instructions
  still tells the model to call `finish` when the step is complete. It then
  says that the named tools end the step by themselves, and that `finish` is
  not to be called after one of them succeeds. Your own instructions should not
  contradict it.
* **The owner speaks.** The task says nothing after its tool. The next step of
  a group opens, or the owner speaks once when control returns, so the
  acknowledgement belongs in the owner's prompt.

## What the model gets for a task

The prompt a task runs on has four parts. You write the first one.

1. **Your instructions file**, as written.
2. **The appended finish rule.** Unmute adds it after your text. It names the
   `finish` call, lists the values from `assign:`, and gives the model a way
   out for a request the task cannot serve. You do not write "call finish when
   you are done" yourself.
3. **The values your placeholders name.** Each `{{variable}}` in your
   instructions is replaced with the saved value. No other saved value is
   added.
4. **The history you chose** with `context.history`. When omitted, it is
   `messages`.

For the task declared above, the appended rule reads:

```text theme={null}
When this step is complete, call `finish` with: customer_id, customer_name.

`unserved_request` is for a request this step cannot serve. Do this step's own work first, and never use it to skip that work: the caller's original reason for being here is not an unserved request. If a handoff here covers what they want, call that handoff instead. Only when no tool and no handoff here can serve what the caller is asking, call `finish` with their request in `unserved_request`, in their own words, rather than refusing or explaining what you cannot do here. The agent that owns this step reads that status and takes the caller from there.
```

With `finish:`, a sentence is added between those two paragraphs. It says the
named tools end the step by themselves and that `finish` is not to be called
after one of them succeeds. On the Pipecat target the call has a longer name
that includes the task's name. The rule is the same.

## A task's contract

Four keys make a task that knows what it saves, when it is done, and when it
can be skipped. They live in three places.

| Key                    | What it declares                                                         | Where it is written                          | If you leave it out                                                    | Taught on                                                                              |
| ---------------------- | ------------------------------------------------------------------------ | -------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `assign:`              | which values the task saves, and into which variables                    | on the task                                  | the task saves nothing, and can still finish                           | this page, and [Variables](/reference/variables#assigning-a-task-result-to-a-variable) |
| `finish:`              | which tools end the task by their own result, checked against `success:` | on the task                                  | the model decides the task is done, by calling `finish`                | [Say what success looks like](#say-what-success-looks-like)                            |
| `confirm:`             | which task has to hear the caller agree before a value is used           | on the variable, naming the task             | the value is settled the moment it arrives, and any prompt may read it | [Variables](/reference/variables#confirm-marks-a-value-the-caller-has-to-agree-to)     |
| `skip_when_confirmed:` | which variable, once confirmed, lets a group skip this step              | on the step entry in a task group's `steps:` | the step runs every time the group runs                                | [Task groups](/build/orchestration/task-groups#skip-a-step-whose-work-is-already-done) |

## Read a saved value

Saving and sharing are separate choices. Put a placeholder in the prompt that
needs the value:

```markdown tasks/booking.md theme={null}
The verified customer is {{customer_name}} with ID {{customer_id}}.
Help them manage their appointment.
```

The task receives saved state only through the values its prompt names.
Its history may separately contain facts spoken earlier. A description does not
send a value to the model. An unset value renders as `none recorded yet.`

A dotted reference selects one field and does not send its siblings:

```markdown theme={null}
Move appointment {{appointment.id}} to {{appointment.date}}.
```

See [Variables](/reference/variables) for shapes, dotted paths, append
assignments, and confirmation.

## Choose conversation history

`context.history` is optional. When omitted, it is `messages`.

| `history`  | The task receives                                                                    |
| ---------- | ------------------------------------------------------------------------------------ |
| `messages` | Earlier caller and agent speech. Tool calls and replies are removed together.        |
| `full`     | Earlier speech and paired tool records. Earlier instructions are removed.            |
| `last_n`   | The newest `max_messages` entries, without a broken tool pair.                       |
| `reset`    | No earlier conversation. Its own instructions and explicit placeholders still apply. |
| `summary`  | A generated summary. Compiles on LiveKit only; Pipecat refuses this value.           |

A task needs a code target, Pipecat or LiveKit. On SLNG a task cannot run at
all today, so every history value is refused there, `messages` included. Tasks
on SLNG are coming.

```yaml agent.yaml theme={null}
      - name: reschedule_booking
        instructions: tasks/reschedule-booking.md
        tools:
          - save_booking
        context:
          history: reset
```

```markdown tasks/reschedule-booking.md theme={null}
Move appointment {{appointment_id}} to {{appointment_date}}
at {{appointment_time}}.

If the requested date or time is unavailable, ask the caller.
Confirm the change before making it.
```

This reset task sees those three saved values because its prompt names them. It
does not receive the sentence that triggered the task. If the new date was not
saved, the task must ask for it again.

History controls model input; it does not redact speech already retained in a
different context. Use `reset` when the receiver should inherit no transcript.
The [step-by-step context guide](/best-practices/context-scope) shows how to
save the request first and keep the owner informed after the task finishes.

## What returns

The owner gets its pre-task context back plus one neutral status:

```json theme={null}
{"status":"completed"}
```

or:

```json theme={null}
{"status":"unserved"}
```

Task arguments, tool results, private task turns, and the text of an unserved
request do not cross back. The owner reads saved values through its own prompt
placeholders.

Every generated `finish` call includes an optional `unserved_request`. A task
uses it when it cannot complete the request with its tools or handoffs. A
non-empty value skips all assignments. The owner sees the `unserved` status and
asks the caller what they need; it does not receive the private request text.

After a tool reports success, call `finish` right away with the result to save.
Let the owner confirm it once, using its own placeholders. If the task waits
for another turn, it may receive a new request before saving the first result.
An `unserved` status does not undo an action already completed by a tool.

Better still, let the tool end the task. See
[Say what success looks like](#say-what-success-looks-like).

For requests that another agent can handle, a task may declare `handoffs:`.
Taking one ends the task and any remaining task-group steps. See
[Handoffs](/build/orchestration/handoffs).

## Advanced

### Append and project values

Append one result to a list with `+`:

```yaml agent.yaml theme={null}
variables:
  appointments:
    type: list[Appointment]

assign:
  - appointments+: result.appointment
```

An absent item adds nothing. Repeating the same structured item does not add a
duplicate. Plain values may repeat.

An assignment may select a nested field:

```yaml agent.yaml theme={null}
assign:
  - appointment: result.appointment
  - appointment_id: result.appointment.id
```

The whole-object assignment establishes the result shape. The compiler then
checks the nested path and derives its type.

### Share a task across agents

Define a task once. Another agent can attach it by name:

```yaml agent.yaml theme={null}
agents:
  appointment_desk:
    tasks:
      - name: verify_customer
        when: Identify the caller before booking.
        instructions: tasks/verify-customer.md
        assign:
          - customer_id: result.customer_id

  billing_desk:
    tasks:
      - verify_customer
```

Task names are unique across the package. Share a task only when both agents
should be able to run it: a task within reach is a task the model may choose.

### Open by listening

A task's first turn is a model request. When the task opens with one fixed
question, `opening: listen` speaks that question and waits, and makes no
request:

```yaml agent.yaml theme={null}
      - name: take_stylist_note
        when: The caller wants a note left for the stylist.
        announce: What would you like me to pass on to your stylist?
        opening: listen
        instructions: tasks/stylist-note.md
```

The line is the `announce:` line, spoken once and recorded as the task's own
first turn, so a caller answering it is answering something the task can see.
The default is `generate`: the model opens the task from its instructions.

A listening task with no `announce:` is a warning, not a refusal: the caller
hears nothing until they speak, which is occasionally what you want and usually
a mistake.

### Announce a task

`announce:` is an optional fixed line spoken as the task starts. Omit it for
a silent transition. If you add one, tell the model not to repeat it with a
second "let me check" line:

```yaml agent.yaml theme={null}
      - name: manage_booking
        when: The caller wants to manage a booking.
        announce: Let me pull up the diary.
        instructions: tasks/booking.md
```

### Task or second agent

|                 | Task              | Second agent                                                   |
| --------------- | ----------------- | -------------------------------------------------------------- |
| control returns | yes               | no                                                             |
| saved values    | through `assign:` | already live in call state; read only by explicit placeholders |
| prompt          | one step          | a whole role                                                   |

## Try it

`my-agent` is the package `unmute init` wrote in
[Your first agent](/build/your-first-agent). With a task in it:

```sh theme={null}
unmute validate my-agent
unmute dev my-agent
```

In the dev page the task shows as its own row, labelled `HANDOFF`, so you can
see it was entered. [The dev loop](/dev/overview) explains the rest of that
page.

The salon is the shipped example with tasks, a task group and handoffs. These
commands need a clone of the unmute repo:

```sh theme={null}
unmute validate examples/salon-concierge
unmute dev examples/salon-concierge --target pipecat
```

## Troubleshooting

### The model does the task's work itself and never enters it

The agent holds every tool the task holds, so the model takes the short route
and the task's `assign:` never runs. `unmute validate` warns and names the tool
to move.

**Fix:** take the tool off the agent and leave it on the task. A task the model
can bypass is a task it will bypass.

### A task saves nothing on a call where it clearly ran

Every assigned value is checked before any is saved. If one value is invalid,
nothing changes and the task stays open so the model can correct it.

**Fix:** check the destination's type against what the tool actually returns.
An `object` or `array` output landing on a plain destination is refused at
compile time; a wrong value at run time keeps the task open instead.

### An older package uses a retired key

| Earlier configuration                 | What to do now                                                     | Why                                                                                     |
| ------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| A task `result:` schema               | Put the types on variables and use task `assign:`                  | One declaration defines the saved value and the `finish` argument                       |
| Task or handoff `expect:`             | Save needed facts first and reference them in the receiving prompt | Sharing is explicit, including with `reset`                                             |
| Task `requires:`                      | Put the order in the owner's prompt and the task's `when:`         | The model can follow the flow; injected tools still guard missing or unconfirmed values |
| An automatically appended state block | Add placeholders to each prompt that needs saved facts             | Saving a value no longer shares it with every prompt                                    |
| An omitted history choice             | Review whether `messages` is appropriate                           | `messages` is now the default                                                           |

Ordinary tool `input:` and `output:` schemas are unchanged. Run
`unmute validate` after updating an older package; retired keys produce a
located error with migration advice.

## Where to go next

<Columns cols={2}>
  <Card title="Task groups" icon="list-ordered" href="/build/orchestration/task-groups">
    Run several tasks in a fixed order. This is the next page.
  </Card>

  <Card title="Making a task actually run" icon="footprints" href="/best-practices/step-scoping">
    What to give a task so the model enters it, and what to take away from the owner.
  </Card>

  <Card title="Designing declared state" icon="database" href="/best-practices/state-design">
    Choosing what to save with `assign:`, and how to type it.
  </Card>
</Columns>
