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

  1. The current task returns a result string (the "eval").
  2. The engine checks the transition.branches array from top to bottom.
  3. It evaluates the when condition against the eval string using the operator.
  4. The first branch that evaluates to true determines 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

OperatorHow it matchesExample
equalsExact string match"when": "tool-call" matches "tool-call"
containsSubstring match"when": "fail" matches "api_failure"
starts_withPrefix match"when": "err" matches "error_timeout"
ends_withSuffix match"when": "_ok" matches "write_ok"
edge_traversed_at_leastFires once an edge has been traversed N times this run; reads engine state, not task output"edge": "chat->run_tools", "when": "20"
defaultAlways matchesUsed 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 declared equals branch when values (or the raw model answer, which the default branch 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.