Skip to main content
A task is worth having when it does something its owner cannot do as well: a narrower prompt, a smaller context, a typed result that lands in a saved value. None of that happens if the model never enters it. The model does not read your intent. To the model, a task is one more entry in its function list, next to the owner’s own tools, and it picks. These are the rules that decide which way it picks. On this page:

Give the task a tool the owner does not have

If the owner already holds every tool the task holds, the owner can finish the job without entering the task. It usually will. The task’s assign: then never runs and nothing is saved. This is the shape that produces it. A scheduling desk declared:
agent.yaml
The desk books the appointment itself, because the tool is in its own list, and appointment stays empty. The prompt is not the fix. A workflow that says “then run the booking task in the same turn” is a request to the model, and a tool within reach beats a request. The fix is to take the tool off the owner and leave it on the task:
agent.yaml
unmute validate warns when an agent holds every tool of one of its tasks, and names the tool to move. It is a warning and not a refusal because whether the model takes the short route depends on your prompt and your when:, neither of which the compiler reads. Read the warning and decide.
A task that declares no tools at all is fine. Its prompt, context choice, and saved assignments are reasons to enter it that have nothing to do with tools.

Order steps with the prompt

Step here means a task an agent runs on its own. Two tasks that must hold their order every time belong in a task group, which runs its steps in the order you list them. This section is for the looser case: a task that usually follows another one. A task that must run only after another one does not need a code gate for that. It needs two sentences: one in the owner’s own instructions, and one in the task’s own when:. Number the flow in the owner’s instructions, in the order the caller actually moves through it:
instructions.md
Then give the later task a when: that names the situation and, in one clause, what has to be true first:
agent.yaml
The model has something real to check that clause against when the owner’s prompt names the value the earlier task saved:
instructions.md
A tool that silently reads a value, through inject: or a webhook path, still refuses to run while that value is empty or unconfirmed. The model is told which task supplies it. When nothing supplies it, the model is told to ask the caller. Check the order with a scripted text conversation before you check it on the phone. It drives the agent through a fixed script with the real model and the real tools, so you see which task ran, and in what order, without picking up a phone. See scripts/text_run_livekit.py.

Keep a read on the owner, keep the write on the task

The rule above has a natural shape once you apply it. Lookups that answer questions belong on the owner, because the caller can ask at any time and entering a task to answer a price question is slow. Anything that records, saves, or changes something belongs on the task, alone, because that is the action whose result you want saved. Split that way, the model has a real reason to enter: the task holds the only route to the thing the caller is asking for.

Let the tool end the task

When a task’s work ends on one tool, name that tool under finish: with the result values that count as success. The task then ends on the tool’s own success result, saves its assign: from that result, and never waits for the model to make the finish call. A result that is not a success goes back to the model and the task stays open.
agent.yaml
The tool has to declare the success values in its output enum:, and every tool under finish: has to return what assign: saves. See Say what success looks like.

Scope history down and let saved values carry the facts

A task may not need the transcript if its prompt names every fact it needs. Start with messages. Use reset only when the saved inputs cover the job; otherwise the caller will have to repeat details that were only spoken. reset is the strongest scope. A reset task never receives the turn that triggered it. Save a fact before the boundary and reference it in the reset prompt when the receiver needs it. If the fact was only spoken and never saved, the reset task asks for it again.

Give every task an escape, and read it

A task offers its own tools, its declared handoffs, and the finish call. When the caller asks for something none of those cover, a task with no way out refuses, the caller presses, and it refuses again. Every generated task carries a reserved unserved_request field for this. The task does its own work, then names the request it could not serve in its own words, and makes the finish call. You do not write that rule yourself: the compiler appends it to every task prompt. The owner receives only an unserved status, not the private request text. It asks what the caller needs and handles the new request with its own tools or handoffs:

Do not let two prompts disagree

A task’s prompt and its owner’s prompt are written at different times and read together. When they disagree, the model picks one, and you cannot tell which. The pairing that bites most often is “do not run the same task again for a request that just finished” on one side and “act on the unserved request” on the other. A second booking is a new request, not a repeat of the last one, but nothing says so unless you write it. When you add a rule to a task, read the owner’s prompt in the same sitting and check it still agrees.

Where the mechanics live

Tasks

Declaring a task, its saved values, and what success looks like.

Task groups

Tasks in a fixed order, and skipping a step whose work is already done.

Designing declared state

What to save, and who reads it next.

Choosing a context scope

What each scope costs, and what has to travel as a saved value.