How transitions work
When a node completes, it produces an outcome with a stage outcome (succeeded, failed, partially_succeeded, or skipped) and optional signals like a preferred label or suggested next node. A node’s retry policy runs before routing starts. Fabro then selects the next step in this order:
- Direct jump — An outcome’s
jump_to_nodevalue bypasses edge selection. - Condition match — Edges with a
conditionattribute are evaluated first. If one or more conditions match, the edge with the highestweightwins (lexical tiebreak on target node ID). - Preferred label — If the node’s outcome includes a preferred label (for example, from a human gate selection), the edge whose
labelmatches is chosen. - Suggested next — If the node suggests a specific next node ID, the edge pointing to that node is chosen.
- Failure policy — For a failed outcome with no explicit route, the effective
on_failurepolicy (node-levelon_failurefirst, then graph-level) decides what happens next.exitskips the unconditional fallback.succeedpromotes the outcome tosucceededand routes it as a success.routecontinues to the unconditional fallback. - Unconditional fallback — Edges without conditions are considered last, again using
weightthen lexical tiebreak. - Retry target — For a failed outcome with no selected edge, Fabro checks node-level and graph-level
retry_targetandfallback_retry_targetvalues.
Failed-node routing policy
Theon_failure attribute controls what happens to a failed node when no explicit recovery route matches:
Set it at the graph level to apply the policy to every node, or on a node to control that node alone. A node-level
on_failure overrides the graph level. A node without the attribute inherits the graph policy.
This lets a linear workflow stop at the first failed work node:
stop-on-failure.fabro
route so its failure continues down the unconditional edge, and a default graph can mark one critical node as exit:
mixed-policies.fabro
succeed for a best-effort node whose failure must not block the workflow. Its failure becomes a succeeded outcome, so the node’s normal success routing applies:
best-effort-node.fabro
succeed, Fabro first checks explicit routes against the original failed outcome. If a condition="outcome=failed" edge, a matching preferred label, a matching suggested next node, or a handler jump applies, the outcome stays failed and that route is taken. Otherwise Fabro rewrites the outcome to succeeded before it records the node, so goal gates, the run context, events, and routing all see the promoted outcome. Edge selection then runs again: condition="outcome=succeeded" edges and unconditional edges apply. The original failure details stay on the stage.completed event and in the checkpoint, and the outcome’s notes record which scope promoted it. A promoted outcome is not failed, so retry targets do not apply to it.
Both exit and succeed apply only to the failed outcome. They do not change routing for succeeded, partially_succeeded, or skipped outcomes.
Conditioned edges, matching preferred labels, and matching suggested node IDs are explicit recovery routes. They take priority under every policy. An unmatched preferred label or suggested node ID does not make an unconditional edge explicit.
Retry targets also remain available under exit. Fabro checks them after it skips the unconditional fallback. Use graph-level max_node_visits or node-level max_visits to bound workflows whose retry targets return to a failing path.
A failed human gate never falls through to an unconditional edge as a failure, regardless of policy. Node-level on_failure="route" does not change that; route an interrupted gate explicitly with condition="outcome=failed". Under succeed, an interrupted gate with no explicit route is promoted like any other node and then follows its success routing.
When exit stops routing, Fabro checkpoints the failed node without a next node and ends the run as failed. It does not execute the graph’s exit node or emit an edge selection for an edge it did not take. An explicit recovery route can still reach the exit node normally.
For a parallel node, exit and succeed see the final outcome returned by the parallel handler. exit can stop routing for a failed parallel outcome; succeed promotes it. Neither adds branch-level fail-fast behavior, and a partially_succeeded parallel outcome continues normally. Inside the fan-out, a branch node whose effective policy is succeed counts as succeeded in the parent’s aggregate when it fails. Branches have no edge routing, so there is no explicit route to prefer.
auto_status=true is the deprecated spelling of node-level on_failure="succeed". Fabro still accepts it as an alias and validation warns with the replacement. See Node Outcomes.Edge attributes
Conditions
Edge conditions are boolean expressions evaluated against the stage outcome and run context. Conditions go in thecondition attribute on an edge:
Available keys
Operators
A bare key with no operator is a truthiness check — it passes if the value is non-empty, not
"false", and not "0":
Combining conditions
Use&& (AND), || (OR), and ! (NOT) to build compound expressions. && binds tighter than ||:
Agent transitions
Agent and prompt nodes can influence which edge is taken by including a JSON object in their response with routing directives. Fabro scans the LLM output for the last JSON object containing any of these fields:
Fabro automatically scans LLM output for these JSON objects — no special configuration is needed. However, you do need to instruct the LLM to emit the JSON in your prompt. For example:
Human gate transitions
Human gates use edge labels to present options to the user. The selected label becomes thepreferred_label in the outcome, and Fabro matches it to the corresponding edge:
[A], [R], [S] prefixes are keyboard accelerators — Fabro strips them when matching, so the user can type just the letter.
Unconditional edges
An edge without acondition attribute is the normal fallback. When a node has a single outgoing edge, it doesn’t need a condition:
on_failure="exit" skips this fallback after explicit routes are checked, and on_failure="succeed" promotes the outcome to succeeded before taking it. The default on_failure="route" keeps the behavior shown above.
Weight tiebreaking
When multiple edges match (e.g. two unconditional edges),weight determines the winner. Higher weight wins:
Random selection
By default, tiebreaking between candidate edges is deterministic (highest weight, then lexical node ID). Settingselection="random" on a node switches to weighted-random tiebreaking for its outgoing edges:
path_a is chosen ~75% of the time and path_b ~25%. Edges with weight ≤ 0 are treated as weight 1. The cascade priority (conditions → preferred label → suggested next → unconditional) is unchanged — randomness only affects the pick-one-from-candidates step within each tier.
selection="random" cannot be combined with conditional edges on the same node. Validation rejects this combination because condition evaluation order would conflict with random selection. Use unconditional edges with weights instead.