Skip to content

Transitions & Routing

Runsight workflows are directed graphs. Blocks connect through transitions that tell the engine which block to run next. This page explains every connection mechanism — from simple A-to-B transitions through conditional branching — and how the execution engine resolves the next block at runtime.

The simplest connection. A TransitionDef maps one block to the next.

workflow section
workflow:
name: Pipeline
entry: research
transitions:
- from: research
to: draft
- from: draft
to: publish
- from: publish
to: null # terminal — workflow ends here
FieldTypeDescription
fromstrSource block ID
tostr or nullTarget block ID, or null for terminal

Setting to: null marks the block as terminal — the workflow ends after it executes. If a block has no transition at all (not listed in transitions and no depends pointing to it), it is also terminal.

Each block can have at most one plain transition. Attempting to add a second raises a validation error.

When a block needs to route to different targets based on its output, use conditional_transitions. Extra keys beyond from and default map decision strings to target block IDs.

workflow section
workflow:
name: Review Pipeline
entry: classifier
conditional_transitions:
- from: classifier
urgent: handle_urgent
normal: handle_normal
default: handle_normal
FieldTypeDescription
fromstrSource block ID
defaultstr or nullFallback target if no key matches
(extra keys)strDecision key mapped to target block ID

The engine resolves which key to use via the block’s exit handle — see the resolution order below.

A block cannot have both a plain transition and a conditional transition. The engine enforces mutual exclusivity at build time.

Exit ports declare the named outputs a block can produce. Any block type can have exits, but they are most commonly used with linear blocks (via the delegate tool) and gate blocks (automatic pass/fail).

block with exits
blocks:
reviewer:
type: linear
soul_ref: review_soul
exits:
- id: approve
label: Approved by reviewer
- id: reject
label: Rejected — needs revision

Each ExitDef has:

FieldTypeDescription
idstrUnique exit port ID
labelstrHuman-readable label

When a block has exits and the workflow has conditional_transitions for that block, the transition keys should match the exit IDs. The engine validates this at build time — a transition key that does not match any declared exit (or "default") produces a validation error.

Exit conditions let you map output content patterns to exit handles without requiring the LLM to call a tool. The engine checks them after block execution.

pattern-based exit routing
blocks:
classifier:
type: linear
soul_ref: classifier_soul
exit_conditions:
- contains: "APPROVED"
exit_handle: approve
- regex: "reject|deny"
exit_handle: reject
exits:
- id: approve
label: Approved
- id: reject
label: Rejected

Each ExitCondition has:

FieldTypeDescription
containsstr or nullSubstring match against block output
regexstr or nullRegex match against block output
exit_handlestrExit handle to set when the condition matches

Conditions are evaluated in order. The first match wins. If no condition matches, the exit handle remains null and the engine falls through to plain transitions.

Output conditions evaluate structured data from a block’s result to pick a named branch. They use the condition engine with operators like equals, contains, gt, and more.

output_conditions on a block
blocks:
analyze:
type: code
code: |
def main(data):
score = len(data.get("text", ""))
return {"quality": "high" if score > 100 else "low"}
output_conditions:
- case_id: high_quality
condition_group:
conditions:
- eval_key: quality
operator: equals
value: high
- case_id: low_quality
default: true

Each CaseDef has:

FieldTypeDefaultDescription
case_idstrrequiredDecision string emitted when this case matches
condition_groupConditionGroupDefnoneConditions to evaluate (omit when default: true)
defaultboolfalseWhether this is the fallback case

A ConditionGroupDef contains:

FieldTypeDefaultDescription
combinatorstr"and""and" or "or" — how conditions combine
conditionsList[ConditionDef]requiredIndividual conditions to evaluate

Each ConditionDef has:

FieldTypeDescription
eval_keystrDot-notation path into the block’s result
operatorstrOne of the supported operators (see below)
valueAny or nullComparison value (omit for unary operators)
CategoryOperators
Stringequals, not_equals, contains, not_contains, starts_with, ends_with, is_empty, not_empty, regex
Numericeq, neq, gt, lt, gte, lte
Universalexists, not_exists

Routes (shorthand for output conditions + transitions)

Section titled “Routes (shorthand for output conditions + transitions)”

routes combine output conditions and conditional transitions in a single, compact block. They are mutually exclusive with output_conditions — you cannot use both on the same block.

routes shorthand
blocks:
review:
type: code
code: |
def main(data):
return {"status": "approved"}
routes:
- case: publish
when:
conditions:
- eval_key: status
operator: equals
value: approved
goto: publish
- case: archive
default: true
goto: archive

Each RouteDef has:

FieldTypeDefaultDescription
casestrrequiredCase ID for this route
whenConditionGroupDefnoneCondition group (ignored on default routes)
gotostrrequiredTarget block ID
defaultboolfalseWhether this is the fallback route

Routes require exactly one default route. At parse time, the engine expands routes into output conditions and conditional transitions — they are pure sugar.

After a block finishes executing, the engine follows this resolution order in _resolve_next:

  1. Read exit handle — check state.results[block_id].exit_handle. If the block set one (via the delegate tool, gate pass/fail, or exit conditions), use it.
  2. Evaluate output conditions — if no exit handle was set and the block has output_conditions, evaluate them against the block’s output. The winning case_id becomes the exit handle.
  3. Conditional transition lookup — if the block has conditional_transitions, use the exit handle as a lookup key in the condition map.
  4. Default fallback — if no key matches, fall back to the "default" key in the condition map. If no default exists, the engine raises a KeyError.
  5. Plain transition — if no conditional transitions exist, follow the plain transition (if any). If none, the block is terminal.

Any block can specify an error_route — a target block that runs when the block fails with an exception. See YAML DX Shortcuts for syntax details.

Error routing also handles soft errors: if a block completes but its exit handle is "error" (for example, a workflow block with on_error: catch that caught a child failure), the engine routes to the error target instead of the normal transition.

Instead of writing explicit transitions, use depends on individual blocks. The engine auto-generates transitions from the dependency to the dependent block. See YAML DX Shortcuts for details and examples.