mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
A failed node with an effective `succeed` policy and no explicit recovery route now finishes as `succeeded` and follows normal success routing. The original failure stays on the outcome so the stage.completed event and the checkpoint keep the diagnostic, and the outcome notes record which scope promoted it. - OnFailure gains a Succeed variant; Node::on_failure resolves the deprecated auto_status=true attribute as an alias, with an explicit on_failure winning - The core executor applies the policy before the lifecycle observes the result, so the recorded outcome, context keys, goal gates, events, and routing all see the effective outcome; this replaces AutoStatusLifecycle - Explicit routes take priority: a matching condition, preferred label, suggested next node, or handler jump keeps the outcome failed. A failed outcome takes an unconditional edge only under route, so under succeed any edge selection is an explicit route - succeed applies only to failed, matching exit; the auto_status alias no longer promotes partially_succeeded - Parallel branches promote after their retry loop, so a failed succeed branch counts as succeeded in the parent aggregate - Validation accepts succeed and adds an auto_status_deprecated warning that suggests on_failure="succeed" - Document the policy table, semantics, and deprecation; add a changelog entry Closes #807 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
7.5 KiB
Text
137 lines
7.5 KiB
Text
---
|
|
title: "Node Outcomes"
|
|
description: "The four stage outcomes and the attributes that control them"
|
|
---
|
|
|
|
Every node execution produces an **outcome** that drives edge routing, retry logic, and goal gate checks. This page defines the four externally visible outcomes and the attributes that influence them.
|
|
|
|
## The four outcomes
|
|
|
|
| Outcome | Meaning |
|
|
|---|---|
|
|
| `succeeded` | The handler completed normally |
|
|
| `failed` | The handler encountered an unrecoverable error |
|
|
| `partially_succeeded` | The handler did not fully succeed but produced usable results — typically from retries exhausted with `allow_partial=true` |
|
|
| `skipped` | The node was not executed (e.g. a branch not taken in a parallel fan-out) |
|
|
|
|
<Note>
|
|
Retry intent is internal to the engine. It triggers re-execution inside the retry loop and is never visible in edge `condition` expressions. Retryable failures emit `stage.retrying` events while the node is still active, then finish as one of the four outcomes above.
|
|
</Note>
|
|
|
|
## How handlers produce outcomes
|
|
|
|
Each node type has its own rules for which outcomes it can return:
|
|
|
|
| Handler | Produces | Conditions |
|
|
|---|---|---|
|
|
| **Command** | `succeeded`, `failed` | `succeeded` when exit code is 0; `failed` otherwise |
|
|
| **Agent / Prompt** | `succeeded`, `failed`, `partially_succeeded`, `skipped` | Defaults to `succeeded`. The LLM can set any outcome via a [routing directive](/agents/outputs#routing-directives) JSON object in its response. Backend errors request retry when retryable or finish as `failed`. |
|
|
| **Parallel** | `succeeded`, `partially_succeeded`, `failed` | Waits for every branch. `succeeded` when all branches succeed, `failed` when all branches fail, and `partially_succeeded` for mixed or partial results. A static fan-out with no branches remains partial; a valid `for_each` source with zero items succeeds. |
|
|
| **Human** | `succeeded` | Always succeeds — the user's selection becomes a routing signal via `preferred_label` |
|
|
| **Conditional** | `succeeded` | Always succeeds — routing is handled by the engine's edge selection |
|
|
| **Start / Exit / Wait** | `succeeded` | Always succeed |
|
|
|
|
## Retry loop
|
|
|
|
When a handler returns a retryable failure, 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 outcome depends on `allow_partial`:
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ Run handler │
|
|
└──────┬──────┘
|
|
│
|
|
▼
|
|
┌──────────┐ succeeded / failed /
|
|
│Outcome? │─── partially_succeeded / skipped ──▶ Done (use as-is)
|
|
└────┬─────┘
|
|
│ retryable failure
|
|
▼
|
|
┌──────────────┐ yes ┌──────────────┐
|
|
│ Attempts │─────────▶│ Backoff + │──┐
|
|
│ remain? │ │ re-execute │ │
|
|
└──────┬───────┘ └──────────────┘ │
|
|
│ no │
|
|
▼ ┌──────────┘
|
|
┌──────────────┐ │
|
|
│allow_partial?│ │ (loops back to
|
|
└──────┬───┬───┘ │ "Run handler")
|
|
yes │ │ no │
|
|
▼ ▼
|
|
partially_succeeded failed
|
|
```
|
|
|
|
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 retryable failure, the outcome is promoted to `partially_succeeded` instead of `failed`. 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 a retryable failure and all 5 standard-policy attempts are used, the node finishes with `partially_succeeded` rather than failing the run.
|
|
|
|
See [Retry policies](/execution/failures#retry-policies) for the available presets and backoff settings.
|
|
|
|
## Succeed on failure
|
|
|
|
When a node's effective `on_failure` policy is `succeed`, a `failed` outcome with no explicit recovery route is promoted to `succeeded`. This is applied after the retry loop, so retries still happen normally — only the final outcome changes. The original failure details stay on the `stage.completed` event and in the checkpoint, and the outcome's notes record the promotion.
|
|
|
|
| Attribute | Type | Default |
|
|
|---|---|---|
|
|
| `on_failure` | String | inherits the graph-level `on_failure` (default `route`) |
|
|
|
|
```dot
|
|
scan [
|
|
label="Scan",
|
|
shape=parallelogram,
|
|
on_failure="succeed",
|
|
script="find . -name '*.log' | head -20"
|
|
]
|
|
```
|
|
|
|
Use `on_failure="succeed"` 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. An explicit `condition="outcome=failed"` edge still takes priority; the promotion applies only when no explicit route matches. The policy applies only to `failed` and leaves `partially_succeeded` unchanged. See [Failed-node routing policy](/workflows/transitions#failed-node-routing-policy) for the full set of policies.
|
|
|
|
<Note>
|
|
`auto_status=true` is the deprecated spelling of `on_failure="succeed"`. Fabro still accepts it as an alias, and validation reports an `auto_status_deprecated` warning with the replacement. Unlike the old attribute, the alias no longer promotes `partially_succeeded` outcomes.
|
|
</Note>
|
|
|
|
## Goal gate interaction
|
|
|
|
Node outcomes and workflow status are separate. A node can finish with `failed`
|
|
and the workflow can still reach `exit` and complete successfully if routing
|
|
handles that outcome. Mark a node with `goal_gate=true` when its failure must
|
|
make the workflow fail.
|
|
|
|
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 `succeeded` **or** `partially_succeeded`. Any other outcome (`failed`, `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 `partially_succeeded` outcome counts as passing. Likewise, a `succeeded` outcome promoted by `on_failure="succeed"` satisfies the gate.
|
|
|
|
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=succeeded"]
|
|
gate -> fix [label="Fix", condition="outcome=failed"]
|
|
gate -> review [label="Partial", condition="outcome=partially_succeeded"]
|
|
gate -> skip [label="Skipped", condition="outcome=skipped"]
|
|
|
|
// Common pattern: treat partial as passing
|
|
gate -> deploy [condition="outcome=succeeded || outcome=partially_succeeded"]
|
|
gate -> fix [condition="outcome=failed"]
|
|
```
|
|
|
|
See [Transitions](/workflows/transitions) for the full edge selection logic and operator reference.
|