diff --git a/docs/core-concepts/how-fabro-works.mdx b/docs/core-concepts/how-fabro-works.mdx index 2e173d49c..2fe9c59ab 100644 --- a/docs/core-concepts/how-fabro-works.mdx +++ b/docs/core-concepts/how-fabro-works.mdx @@ -41,7 +41,7 @@ The engine walks the graph starting from the start node. For each node it: 1. **Resolves context** — Assembles the node's input from prior stage outputs, run context, and the workflow goal. The [fidelity](/workflows/stages-and-nodes#agent) setting controls how much prior context is included. 2. **Dispatches to a handler** — Each [node type](/workflows/stages-and-nodes) has a handler: the agent handler runs an LLM tool loop, the command handler runs a shell script, the human handler waits for input, and so on. -3. **Collects the outcome** — The handler returns a status (`success`, `fail`, `partial_success`), optional routing directives, and any context updates. +3. **Collects the outcome** — The handler returns a [status](/execution/outcomes) (`success`, `fail`, `partial_success`, `retry`, or `skipped`), optional routing directives, and any context updates. 4. **Selects the next edge** — Fabro evaluates outgoing edges using conditions, labels, and weights to pick the next node. See [Transitions](/workflows/transitions). 5. **Checkpoints** — After each stage, Fabro writes a checkpoint so the run can be resumed if interrupted. diff --git a/docs/docs.json b/docs/docs.json index e7e5b6c54..3db7ea916 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -53,6 +53,7 @@ "execution/environments", "execution/context", "execution/checkpoints", + "execution/outcomes", "execution/failures", "execution/retros", "execution/observability", diff --git a/docs/execution/failures.mdx b/docs/execution/failures.mdx index 2daca6724..95d36434f 100644 --- a/docs/execution/failures.mdx +++ b/docs/execution/failures.mdx @@ -93,7 +93,7 @@ The engine resolves retry configuration in this order: Not all errors trigger a node retry. The handler's `should_retry` check must return true — generally, only errors classified as transient are retried. Deterministic errors (auth failures, bad config) fail immediately without consuming retry attempts. -When a handler returns a `Retry` status instead of `Fail`, retries always proceed (if attempts remain). If retries are exhausted and the node has `allow_partial=true`, the outcome is promoted to `PartialSuccess` instead of failing. +When a handler returns a `Retry` status instead of `Fail`, retries always proceed (if attempts remain). If retries are exhausted and the node has `allow_partial=true`, the outcome is promoted to `PartialSuccess` instead of failing. See [Node Outcomes — Retry loop](/execution/outcomes#retry-loop) for a detailed flow diagram. ## Model fallbacks @@ -222,7 +222,7 @@ Loop restart edges also have their own separate circuit breaker (`restart_failur ## Goal gates -Goal gates are quality checkpoints that are enforced when the workflow reaches an exit node. A node marked with `goal_gate=true` must have completed with `success` or `partial_success` — otherwise the run cannot finish. +Goal gates are quality checkpoints that are enforced when the workflow reaches an exit node. A node marked with `goal_gate=true` must have completed with `success` or `partial_success` — otherwise the run cannot finish. See [Node Outcomes — Goal gate interaction](/execution/outcomes#goal-gate-interaction) for how `partial_success` and `allow_partial` interact with goal gates. ```dot verify [shape=box, goal_gate="true"] diff --git a/docs/execution/outcomes.mdx b/docs/execution/outcomes.mdx new file mode 100644 index 000000000..741c7ba0a --- /dev/null +++ b/docs/execution/outcomes.mdx @@ -0,0 +1,130 @@ +--- +title: "Node Outcomes" +description: "The five stage statuses and the attributes that control them" +--- + +Every node execution produces an **outcome** containing a status that drives edge routing, retry logic, and goal gate checks. This page defines the five statuses and the attributes that influence them. + +## The five statuses + +| Status | Meaning | +|---|---| +| `success` | The handler completed normally | +| `fail` | The handler encountered an unrecoverable error | +| `partial_success` | The handler did not fully succeed but produced usable results — typically from retries exhausted with `allow_partial=true` | +| `retry` | The handler wants to re-execute — consumed by the [retry loop](#retry-loop) and never appears in edge conditions | +| `skipped` | The node was not executed (e.g. a branch not taken in a parallel fan-out) | + + +`retry` is internal to the engine. It triggers re-execution inside the retry loop and is never visible in edge `condition` expressions. The four externally-visible statuses are `success`, `fail`, `partial_success`, and `skipped`. + + +## How handlers produce statuses + +Each node type has its own rules for which statuses it can return: + +| Handler | Produces | Conditions | +|---|---|---| +| **Command** | `success`, `fail` | `success` when exit code is 0; `fail` otherwise | +| **Agent / Prompt** | `success`, `fail`, `partial_success`, `retry`, `skipped` | Defaults to `success`. The LLM can set any status via a [routing directive](/agents/outputs#routing-directives) JSON object in its response. Backend errors produce `retry` (if retryable) or `fail`. | +| **Parallel** | `success`, `partial_success`, `fail` | Depends on the `join_policy`. `wait_all`: `success` if no failures, `partial_success` if some branches failed. `first_success` / `k_of_n` / `quorum`: `success` if threshold met, else `fail`. | +| **Human** | `success` | Always succeeds — the user's selection becomes a routing signal via `preferred_label` | +| **Conditional** | `success` | Always succeeds — routing is handled by the engine's edge selection | +| **Start / Exit / Wait** | `success` | Always succeed | + +## Retry loop + +When a handler returns `retry` status, the engine enters the retry loop. If retry attempts remain (per the node's [retry policy](/execution/failures#retry-policies)), the handler re-executes after a backoff delay. If attempts are exhausted, the final status depends on `allow_partial`: + +``` +┌─────────────┐ +│ Run handler │ +└──────┬──────┘ + │ + ▼ + ┌──────────┐ success/fail/ + │ Status? │───partial_success/───▶ Done (use as-is) + └────┬─────┘ skipped + │ retry + ▼ + ┌──────────────┐ yes ┌──────────────┐ + │ Attempts │─────────▶│ Backoff + │──┐ + │ remain? │ │ re-execute │ │ + └──────┬───────┘ └──────────────┘ │ + │ no │ + ▼ ┌──────────┘ + ┌──────────────┐ │ + │allow_partial?│ │ (loops back to + └──────┬───┬───┘ │ "Run handler") + yes │ │ no │ + ▼ ▼ + partial_ fail + success +``` + +Handler errors follow the same loop: retryable errors (transient infrastructure) re-execute if attempts remain; non-retryable errors (authentication, bad config) fail immediately without consuming retry attempts. + +## `allow_partial` + +When `allow_partial=true` and the retry loop exhausts all attempts on a `retry` status, the outcome is promoted to `partial_success` instead of `fail`. This lets the workflow continue past nodes that could not fully succeed. + +| Attribute | Type | Default | +|---|---|---| +| `allow_partial` | Boolean | `false` | + +```dot +implement [ + label="Implement", + retry_policy="standard", + allow_partial=true, + prompt="Implement the feature." +] +``` + +In this example, if the agent returns `retry` and all 5 standard-policy attempts are used, the node finishes with `partial_success` rather than failing the run. + +See [Retry policies](/execution/failures#retry-policies) for the available presets and backoff settings. + +## `auto_status` + +When `auto_status=true`, any non-`success` and non-`skipped` status is silently overridden to `success` after the handler completes. This is applied after the retry loop, so retries still happen normally — only the final outcome is overridden. + +| Attribute | Type | Default | +|---|---|---| +| `auto_status` | Boolean | `false` | + +```dot +scan [ + label="Scan", + shape=parallelogram, + auto_status=true, + script="find . -name '*.log' | head -20" +] +``` + +Use `auto_status` for nodes whose failure should never block the workflow — optional scans, best-effort cleanup steps, or informational commands where the output matters more than the exit code. + +## Goal gate interaction + +Nodes marked with `goal_gate=true` are checked when the workflow reaches the exit node. A goal gate is satisfied if its last outcome was `success` **or** `partial_success`. Any other status (`fail`, `skipped`) causes the workflow to fail, even though execution reached the exit. + +This means `allow_partial=true` on a goal gate node lets the gate pass even if the node exhausted its retries — the promoted `partial_success` counts as passing. + +See [Goal gates](/execution/failures#goal-gates) for retry target resolution and failure behavior. + +## Outcome in edge conditions + +The four externally-visible statuses can be used in edge `condition` expressions via the `outcome` key: + +```dot +gate -> deploy [label="Pass", condition="outcome=success"] +gate -> fix [label="Fix", condition="outcome=fail"] +gate -> review [label="Partial", condition="outcome=partial_success"] +gate -> skip [label="Skipped", condition="outcome=skipped"] + +// Common pattern: treat partial as passing +gate -> deploy [condition="outcome=success || outcome=partial_success"] +gate -> fix [condition="outcome=fail"] +``` + +See [Transitions](/workflows/transitions) for the full edge selection logic and operator reference. diff --git a/docs/reference/dot-language.mdx b/docs/reference/dot-language.mdx index 899ab3def..029811bb8 100644 --- a/docs/reference/dot-language.mdx +++ b/docs/reference/dot-language.mdx @@ -187,8 +187,9 @@ Start nodes can also be identified by ID (`start` or `Start`). Exit nodes can be | `max_retries` | Integer | Override default retry count | | `retry_policy` | String | Named preset: `none`, `standard`, `aggressive`, `linear`, `patient` | | `retry_target` | String | Node ID to jump to on retry | -| `goal_gate` | Boolean | When `true`, workflow fails if this node doesn't succeed | -| `auto_status` | Boolean | Auto-generate status updates | +| `goal_gate` | Boolean | When `true`, workflow fails if this node didn't finish with `success` or `partial_success`. See [Node Outcomes](/execution/outcomes#goal-gate-interaction). | +| `auto_status` | Boolean | When `true`, overrides any non-`success`/non-`skipped` status to `success` after the handler completes. See [Node Outcomes](/execution/outcomes#auto_status). | +| `allow_partial` | Boolean | When `true` and retries are exhausted on a `retry` status, promotes the outcome to `partial_success` instead of `fail`. Default `false`. See [Node Outcomes](/execution/outcomes#allow_partial). | | `selection` | String | Edge tiebreaking strategy: `deterministic` (default) or `random` (weighted-random). Cannot be combined with conditional edges. | ### Agent and prompt nodes @@ -257,7 +258,7 @@ Op ::= '=' | '!=' | '>' | '<' | '>=' | '<=' | Key | Resolves to | |---|---| -| `outcome` | Stage status: `success`, `fail`, or `partial_success` | +| `outcome` | Stage status: `success`, `fail`, `partial_success`, or `skipped`. See [Node Outcomes](/execution/outcomes#outcome-in-edge-conditions). | | `preferred_label` | Label selected by a human gate or LLM routing directive | | `context.KEY` | Value from the run context | | `KEY` | Shorthand for context lookup (without the `context.` prefix) | diff --git a/docs/workflows/transitions.mdx b/docs/workflows/transitions.mdx index 9c31c7d3c..d41e011fa 100644 --- a/docs/workflows/transitions.mdx +++ b/docs/workflows/transitions.mdx @@ -7,7 +7,7 @@ After each node finishes, Fabro must decide which edge to follow to the next nod ## How transitions work -When a node completes, it produces an **outcome** with a status (`success`, `fail`, `partial_success`) and optional signals like a preferred label or suggested next node. Fabro evaluates the outgoing edges in a fixed priority order: +When a node completes, it produces an **outcome** with a [status](/execution/outcomes) (`success`, `fail`, `partial_success`, or `skipped`) and optional signals like a preferred label or suggested next node. Fabro evaluates the outgoing edges in a fixed priority order: 1. **Condition match** — Edges with a `condition` attribute are evaluated first. If one or more conditions match, the edge with the highest `weight` wins (lexical tiebreak on target node ID). 2. **Preferred label** — If the node's outcome includes a preferred label (e.g. from a human gate selection), the edge whose `label` matches is chosen. @@ -37,7 +37,7 @@ gate -> implement [label="Fix", condition="outcome=fail"] | Key | Resolves to | |---|---| -| `outcome` | The stage status: `success`, `fail`, or `partial_success` | +| `outcome` | The stage status: `success`, `fail`, `partial_success`, or `skipped`. See [Node Outcomes](/execution/outcomes). | | `preferred_label` | The label selected by a human gate | | `context.KEY` | A value from the run context (e.g. `context.tests_passed`) | | `KEY` | Shorthand for context lookup (without the `context.` prefix) |