Skip to main content
A flow is an ordered sequence of tasks. Use one when the order must be a runtime guarantee. A task that runs inside a flow is called a step of that flow. The runtime calls a flow a task group. It also costs less than the same tasks run one at a time. The agent chooses the flow once; each step after the first opens by itself. Two tasks the agent picks between spend a model request on the choice between them, and that request speaks nothing and does nothing. On this page:

Declare a flow

state.py
agent.yaml
The agent that writes the flow block defines it. Another agent runs the same flow by listing its bare name under its own flows:. These three tasks have no when:. The flow decides when they run, so they need no trigger of their own. A task an agent runs on its own does need one, and a task with no when: that no flow lists is refused, because nothing would ever run it. Each steps entry names a task defined in the package, under some agent’s tasks:. It does not have to be the agent that runs the flow: the flow is what makes each step reachable. save: derives each task’s finish arguments and saves the values needed later.

Every key a flow takes

string
required
A lower snake case name, unique across the package.
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.
string
The situation the model reads to decide whether to run the flow. Omission is accepted but supplies no trigger guidance, so write one.
string or list of strings
One fixed spoken line, with no {{placeholders}}, when the flow starts. Omit for no announcement. Write several as a list and the caller hears one each time.
list of strings
State fields the model fills from the conversation when it picks the flow. The steps read them. Omit to fill none.
list of strings
Steps that run without holding the caller. Each name must be one of steps. Omit to run every step in turn.
string
default:"shared"
Accepts shared or isolated. Omit it for shared. Each member still applies its own context.history; an isolated flow cannot be widened by a member’s full.
string
default:"return"
Accepts return, transfer, or end. Omit it for return.
string
Required with then: transfer: an existing agent name. Refused for return or end; there is no inferred destination.
string
Only results is accepted. Omission also means results.

Skip a step whose work is already done

A step is a bare task name, or an item that says how the flow treats it:
agent.yaml
The flow skips that step when the named variable is confirmed at the moment the flow starts, and runs it otherwise. A bare step always runs. This is what makes a second booking on one call cost nothing extra: the caller is identified once, and the flow knows it. skip_when_confirmed: names a variable the skipped step itself confirms, with confirm: on the variable. Confirmed here is a real mark on the variable, set when the step named under confirm: saves a value. It is not the same as a prompt sentence saying the caller was verified: the compiler reads the mark, never the prose. Anything else is refused: a variable nobody confirms would leave the step running forever or never, and a variable another step confirms would let this flow skip somebody else’s work. There is a side effect, and it is the point. A step some flow names this way withdraws the confirmation of the values it confirms every time it is entered, including on its own outside the flow. So a caller correcting their number re-verifies it, and the next flow run does not skip the step on the strength of the number they just replaced. Values derived from a withdrawn one follow it.

What stops a flow

A flow stops when a step ends unserved, whichever context_scope it has: the later steps do not run, and the owner is handed the unserved status. Running the next step would answer a question nobody asked. A handoff can arrive while a tool that ends the step is still running, or in the same response as one. Either way the step’s own work is committed first, and the caller moves after. The booking is not lost on the way out. A request the caller makes at a reset or an isolated boundary reaches nobody: the step that hears it has no way to pass words on other than unserved_request, and an isolated step’s context does not carry back. Keep the request in the caller’s own words in unserved_request and let the owner act on it.

Context between steps

shared lets later tasks inherit the flow’s running conversation. isolated starts every member without inherited flow conversation. Each task still applies its own context.history. Omitted history means messages. An isolated flow also prevents a member’s full policy from recovering conversation outside that flow. Task results are private. The next task sees only neutral completion status, the conversation allowed by its history choice, and saved values explicitly referenced in its prompt.
tasks/select-appointment.md
The flow owner gets its original context back plus completed or unserved. It reads saved values through its own placeholders. A member task may hand off directly to another agent. That ends the current task and skips the remaining flow steps.

LiveKit status

Flows use a LiveKit API that upstream marks experimental. The package still validates and compiles normally.

Where to go next

Handoffs

The shape that does not return: a lasting change of role.

Choosing a structure

Compare tasks, flows, and handoffs.

Making a task actually run

Why a task you declared never runs, and how to fix it.