Skip to main content
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 flow, each task is one step of the flow. 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 flow when several tasks must run in order. Use a handoff when a different agent should own the rest of the call. New to tasks? Your first task adds one to the package unmute init writes, one key at a time. On this page:

Quickstart

A task is one entry under an agent’s tasks:. This one identifies the caller and saves what it learns:
state.py
agent.yaml
Four keys carry it: what it is (name, prompt), when to run it (when), what it can use (tools), and what it saves (save).

Declare a task

state.py
agent.yaml

Every key a task takes

Two are required. The rest are how you shape the step.
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.
string
required
Path to the task’s Markdown prompt inside the package. No prompt is inferred.
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 flow; it cannot be attached elsewhere by bare name.
list of strings or objects
Tools loaded by the package. Each item is a bare tool name, or a block that starts - name: <tool> and adds confirm: and outcomes: for this step only. Omit for no ordinary tools in this task; it does not inherit its owner’s tools.
list of strings
Lines said to read the call back before the tool runs. The tool runs on the caller’s yes. Lines read {{input.x}}. Needs a then: done outcome on the same tool.
list of objects
What the step does with each result of this tool, top to bottom, first match wins. See Say what success looks like.
list of one-key pairs
A typed match on the result, such as - status: booked or - status: [booked, moved].
null
Matches every result no if: above matched. Last, and at most one.
list of one-key pairs
Pairs of variable: result.field saved only when this outcome matches.
list of strings
Lines said for this result. Several lines are alternatives. Lines read {{result.x}}.
string
Accepts done or failed. Omitted means the step goes on.
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.
object
What the step does as it opens: say, listen, call and with. Omitted means the model writes the opening turn. See Start a step.
list of strings
Bare names of handoffs an agent defines. Omit for no handoffs from this task.
list of strings
State fields the model fills from the conversation when it starts the task, such as the service the caller asked for. Omit to fill none.
object
The history fields. Omit for history: messages. A returning task restores its owner’s earlier context and adds only completion or unserved status.
string
A models.think entry name. LiveKit only. If omitted, use the entry agent’s think profile, even when another agent defines the task.
There is no tasks:, no flows: and no transfers: 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 save:. A tool outcome with then: done is different: the tool’s own result ends the task, so the model does not have to make that call. The next section covers it. 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 save:. The model can still call finish.

Say what success looks like

How does the task know it worked? Without an outcome, the model decides. It runs a tool, reads the result, and calls finish. With an outcome that says then: done, the tool’s own result decides. You name the tool in the step’s tools: and say what each result looks like. 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.
agent.yaml
When save_booking returns a result that matches the if:, the step says the line, saves its save: rows from that result and ends. The result never reaches the model. Anything else falls to the next outcome, or goes back to the model when none matches. That is what keeps a failed booking a conversation rather than a saved one. An outcome is how a step reacts to one tool result. These rules hold for all of them:
  • Top to bottom, first match wins. An outcome starts with if: or else:. else: takes every result no if: matched, comes last, and appears once.
  • then: done ends the step as completed. then: failed ends it as failed, and a flow stops unless the step is non-blocking. With no then: the step goes on. Only a step’s outcomes take then:.
  • Order them then: first. Write the outcomes with then:, then those without, then else:. else: cannot say then: done. An else: with then: failed cannot follow an outcome without then:.
  • No then: needs say:. Its save: reads result. only.
  • Saving. A then: done outcome with no save: of its own saves the step’s save: rows from the tool’s result. A step cannot mix done outcomes with and without their own save:.
  • confirm: needs a done outcome. The tool entry needs at least one then: done outcome. The read-back is said before the tool runs, and the tool runs on the caller’s yes.
  • What a line reads. {{state.x}} reads State. A confirm: line reads {{input.x}}. An outcome say: reads {{result.x}}. A line with no tool, such as a greeting or a nudge, reads State only.
An if: 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. A tool in a step is not always a bare name. A top-level tools: entry can carry the same outcomes: for every agent that uses the tool; see Tools in the agent file. On the slng target, a task takes one outcome with then: done for now, because SLNG supports one finishing tool per task today. It may carry up to 5 say: lines. SLNG says them itself when the tool’s result matches. A line may read {{state.x}} for a whole saved value, or {{result.x}} for a field of the tool’s result. SLNG ends a step only as done, so then: failed is refused, and so is a confirm: read-back. The tool needs a known output: a typed local tool, or a hosted slng: tool with a mirror.
agent.yaml
result.<field> in the step’s save: names an output property of every tool that ends the step, so one tool returning appointment and another returning nothing is refused. Four things follow from a task that ends on its tool:
  • The ending tools close. Once one of them succeeds, none of them 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 unless the outcome has a say:. The next step of a flow 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 save:, 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 {{state.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:
With a then: done outcome, 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.

Read a saved value

Saving and sharing are separate choices. Put a placeholder in the prompt that needs the value:
tasks/booking.md
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:
See Variables for models, dotted paths, append rows, and confirmation.

Choose conversation history

context.history is optional. When omitted, it is messages. Tasks compile on Pipecat, LiveKit and SLNG. SLNG takes every history value except summary. Twilio refuses tasks, including every task history value.
agent.yaml
tasks/reschedule-booking.md
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 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:
or:
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 saves nothing. 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.

A finished task will not run again until the caller speaks

A task the owner already ran is refused, not re-run, while the caller has said nothing since it returned. A completed result with no new caller turn above it is the model reading its own finished work as a fresh request: the owner reruns the whole step, repeats its announcement, and answers nothing. The refusal arrives before the step’s start: runs, so a refused call is silent. Nothing to configure. Write the owner’s prompt to expect it: a bare “yes” above a completed task is the caller answering the question that task already asked and acted on, so the owner should read its saved values and reply. Better still, let the tool end the task. See 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 flow steps. See Handoffs.

Advanced

Append and project values

Append one result to a list with +:
state.py
agent.yaml
An absent item adds nothing. Repeating the same structured item does not add a duplicate. Plain values may repeat. A save: row may select a nested field:
agent.yaml
The whole-object row establishes the result’s fields. The compiler then checks the nested path and derives its type. A step must return every whole value it saves. Mark one it may leave out with ? after the result field, as - appointment: result.appointment?. A path like result.appointment.id reads part of a value the step already returns, so it takes no ?.

Share a task across agents

Define a task once. Another agent can attach it by name:
agent.yaml
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.

Start a step

By default a task’s first turn is a model request. start: replaces that turn with something said or run in code. It takes four keys, and each is optional.
  • say: is said as the step opens. Several lines are alternatives.
  • listen: true waits for the caller after the line, with no model request.
  • call: and with: run a tool in code first. with: lists the tool’s inputs, such as - day: state.requested_day.
agent.yaml
The line is spoken once and recorded as the task’s own first turn, so a caller answering it is answering something the task can see. A task with listen: true and no say: is a warning, not a refusal: the caller hears nothing until they speak, which is occasionally what you want and usually a mistake. A call: runs the tool before the model speaks:
agent.yaml
A with: value with no ? is one the tool requires. A value with ? lets the call run without it. The compiler checks both against the tool’s input schema. Then the step does one of four things:
  • A required value is empty: the call does not run, and the step says say: and listens.
  • The value is already confirmed: the step is skipped.
  • The tool has a confirm: line: the read-back is said instead of say:, and the tool runs on the caller’s yes.
  • Otherwise the result goes through the tool’s outcomes:, and the line the outcome picks is said.
If you add a say:, tell the model not to repeat it with a second “let me check” line.

Task or second agent

Try it

my-agent is the package unmute init wrote in Your first agent. With a task in it:
In the dev page the task shows as its own row, labelled HANDOFF, so you can see it was entered. The dev loop explains the rest of that page. The salon is the shipped example with tasks, a flow and handoffs. These commands need a clone of the unmute repo:

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

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

Flows

Run several tasks in a fixed order. This is the next page.

Making a task actually run

What to give a task so the model enters it, and what to take away from the owner.

Designing declared state

Choosing what to save with save:, and how to type it.