fabro/docs/public/execution/outcomes.mdx
Bryan Helmkamp 302e2445b4
refactor(model): move provider facts into catalog (#298)
## Summary

Moves provider-specific facts out of `AdapterKind` metadata and into
provider catalog data, leaving adapters responsible for runtime protocol
behavior. This makes providers that share an adapter mostly TOML-driven
while still surfacing adapter construction failures during readiness
checks.

## What Changed

- Provider TOML now owns auth mode, API-key/header policy, billing
policy, agent profile, base URLs/env overrides, extra headers, and probe
markers.
- Auth, install, config, diagnostics, and server flows resolve provider
credentials from catalog auth config, including API-key, header-only,
and no-auth providers.
- LLM client registration now reports adapter construction failures,
validates final adapter requests before HTTP dispatch, and preserves
custom primary auth headers.
- Billing and docs now use provider-owned billing policy instead of
adapter metadata, and the old adapter metadata surface is removed.

## Reviewer Notes

OpenAI-compatible `base_url` validation now happens during
adapter/client registration rather than catalog build. That keeps
catalog parsing adapter-agnostic while still letting readiness and model
listing reflect providers that cannot register.

## Verification

- `cargo check -p fabro-model -p fabro-auth -p fabro-llm -p fabro-server
-p fabro-cli`
- `cargo nextest run -p fabro-llm -- adapter_registry`
- `cargo nextest run -p fabro-model -- catalog`
- `cargo nextest run -p fabro-auth -- api_key`
- `cargo nextest run -p fabro-server -- install`
- `cargo +nightly-2026-04-14 fmt --check --all`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:59:08 -04:00

133 lines
6.6 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` | Depends on the `join_policy`. `wait_all`: `succeeded` if no failures, `partially_succeeded` if some branches failed. `first_success`: `succeeded` if threshold met, else `failed`. |
| **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.
## `auto_status`
When `auto_status=true`, any non-`succeeded` and non-`skipped` outcome is silently overridden to `succeeded` 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
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.
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.