Transitions & Branching
Task chains are state machines. When a task finishes running its handler, the chain evaluates its transition rules to determine which task to execute next.
"transition": {
"branches": [
{ "operator": "equals", "when": "tool-call", "goto": "run_tools" },
{ "operator": "default", "when": "", "goto": "end" }
]
}
How transitions work
- The current task returns a result string (the "eval").
- The engine checks the
transition.branchesarray from top to bottom. - It evaluates the
whencondition against the eval string using theoperator. - The first branch that evaluates to
truedetermines the next step (goto).
If the branch specifies "goto": "end", the chain terminates successfully.
on_failure
A task ID to jump to when the current task raises an error — evaluated before any branch conditions. If on_failure is absent and the task errors, the chain terminates.
"transition": {
"on_failure": "error_handler",
"branches": [
{ "operator": "default", "when": "", "goto": "next_step" }
]
}
Operators
| Operator | How it matches | Example |
|---|---|---|
equals | Exact string match | "when": "tool-call" matches "tool-call" |
contains | Substring match | "when": "fail" matches "api_failure" |
starts_with | Prefix match | "when": "err" matches "error_timeout" |
ends_with | Suffix match | "when": "_ok" matches "write_ok" |
edge_traversed_at_least | Fires once an edge has been traversed N times this run; reads engine state, not task output | "edge": "chat->run_tools", "when": "20" |
default | Always matches | Used as the fallback at the end of the array |
What do tasks return?
Each handler returns a different eval string that you can branch on:
chat_completion: Returns"stop","tool-call", or"length".execute_tool_calls: Returns"ok"or"error".tools: Usually returns"ok"or"failed".route: Returns the chosen label — one of this task's declaredequalsbranchwhenvalues (or the raw model answer, which thedefaultbranch catches). Input passes through unchanged.noop: Passes the input through; eval string mirrors the input value.raise_error: Terminates the chain with an error — no branch is evaluated.
Place a default branch last as the fallback. For agentic loops, put an edge_traversed_at_least branch ahead of the loop branch to bound iterations.
Reading edge counts from a prompt
The same counter that backs edge_traversed_at_least is exposed to system_instruction (and other template fields) as a macro:
<span v-pre>{{edge_count:from_task_id->to_task_id}}</span>
It expands at every task step to the live count of how many times that edge has been traversed in the current chain run, starting at 0. Resolves to 0 for edges that have never fired (typos won't break the prompt mid-turn).
This unlocks a self-paced agent pattern: instead of splitting a 20-round budget across a main agent + a recovery agent that hands off at round 10 with a frozen "10 of 20" warning, you have one chat task whose system_instruction shows the live count. The model sees the budget grow on every turn and self-paces accordingly. When the edge_traversed_at_least ceiling fires, the chain still routes to a tool-less terminal task for a clean wrap-up. See Self-paced agent with dynamic budget for the full example.