# Workflow YAML structure

Every Roscoe workflow is a single `.workflow.yaml` file describing a finite
state machine. This page covers the top-level shape of that file: what fields
are required, how states reference one another, and the integrity rules the
loader checks before a run can start. Read this first when you are writing a
workflow from scratch or trying to understand someone else's.

## The top-level fields

```yaml
name: my-pipeline
version: 1
description: A short summary shown in the web UI and CLI listing.
initial: greet
states:
  greet:
    type: script
    label: Print greeting
    script: "console.log('hello'); export default true;"
    validator:
      kind: boolean
    on:
      'true': done
      'false': done
  done:
    type: end
    label: Done
    outcome: success
```

| Field         | Required | Notes                                                                                                                                                                                                                                                                                                                     |
| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | yes      | Kebab-case: `[a-z0-9_-]+`. Must be unique within `ROSCOE_WORKFLOWS_DIR`.                                                                                                                                                                                                                                                  |
| `version`     | yes      | Always the literal `1`. Reserved for future schema revisions.                                                                                                                                                                                                                                                             |
| `description` | no       | Free text. Appears in the web UI and the CLI's workflow list.                                                                                                                                                                                                                                                             |
| `initial`     | yes      | The id of the state to enter first. Must be a key in `states`.                                                                                                                                                                                                                                                            |
| `states`      | yes      | A map of state id to state definition. State ids are arbitrary strings.                                                                                                                                                                                                                                                   |
| `inputs`      | no       | Declared input contract. Required when this workflow is called as a [subworkflow](/docs/nodes/subworkflow) or run with `--input` from the CLI.                                                                                                                                                                            |
| `outputs`     | no       | Declared output contract. Required for callers to read named values out of a subworkflow run.                                                                                                                                                                                                                             |
| `spendingCap` | no       | Default per-run cost ceiling: `{ usd?, tokens? }`. The run stops (`cancelled`, `stop_reason: spending_cap_exceeded`) once its total cost or tokens exceed a set value. Off when omitted; a per-run cap (`--spending-cap`, MCP `spending_cap_usd`) overrides it. See [/docs/running/cli](/docs/running/cli#spending-caps). |

There is no `nodes:` array and no `edges:` block. Transitions live inline on
each state under its `on` map (see below).

## States

Every entry in `states` has a `type` and a `label`. Every type except `end`
also declares an `on` map describing where to go next.

```yaml
states:
  <state-id>:
    type: <node-type>
    label: Human-readable label
    # ...type-specific fields...
    on:
      <event>: <next-state-id>
```

The available `type` values are:

- `script` — runs JavaScript/TypeScript on Bun and branches on the value it returns.
- `ai_agent` — calls a model and produces text. Exactly one outgoing transition.
- `ai_judge` — calls a model and routes on a JSON envelope.
- `consensus` — fans out to N agents and routes on quorum.
- `round_robin` — judges candidate ideas head-to-head and routes on `decided` / `tie`.
- `map` — runs the same step over a list and routes on `success` / `failure`.
- `condition` — evaluates an expression against `variables`.
- `human` — waits for a human approval event.
- `subworkflow` — runs another workflow as a single node; routes on `success` / `failure` / `cancelled`.
- `end` — terminates the run with `outcome: success | failure | cancelled`.

See [Node types](/docs/nodes/ai-agent) for the per-type fields.

## The `on` map

The `on` map is how a state hands control to the next state. Keys are the
_transition events_ the executor can emit; values are the state ids to jump
to.

```yaml
on:
  'true': test
  'false': notify-failure
```

Quote keys that are YAML keywords. `true`, `false`, `yes`, and `no` are all
parsed by YAML as booleans unless quoted. Always write `'true'` and `'false'`
(or use `"true"`) for boolean validators. Numeric or symbol-like keys should
be quoted too.

### What events are emitted?

The events depend on the node type and validator:

| Node type                                      | Events emitted                                                                |
| ---------------------------------------------- | ----------------------------------------------------------------------------- |
| `ai_agent`                                     | A single user-defined event (e.g. `done`). Cannot branch.                     |
| `ai_judge` / `script` boolean validator        | `'true'` and `'false'`                                                        |
| `ai_judge` / `script` confidence validator     | `'true'` and `'false'`                                                        |
| `ai_judge` / `script` enum validator           | One key per declared route                                                    |
| `consensus` boolean / confidence validator     | `approved` (quorum reached) and `rejected` (quorum not reached)               |
| `consensus` answer / most_consistent validator | `decided` (agents converged) and `undecided` (they didn't)                    |
| `round_robin`                                  | `decided` (a clear winner) and `tie` (level / cyclic)                         |
| `map`                                          | `success` (at least one branch completed) and `failure` (all branches failed) |
| `condition`                                    | `'true'` and `'false'`                                                        |
| `human`                                        | Whatever events you declare (e.g. `approved`, `rejected`)                     |
| `subworkflow`                                  | Locked: `success`, `failure`, `cancelled`                                     |
| `end`                                          | None. Terminal.                                                               |

See [Validators](/docs/authoring/validators) for the JSON envelope that AI
nodes use.

### The `done` sentinel

The target id `done` is treated as a terminal sentinel. You can write
`on: { 'true': done }` even when no state with id `done` exists, and the run
will end at that transition. Any other unknown target id is an integrity
error. If you want a richer terminal state, define an explicit `end` node and
point at it instead: that lets you set an `outcome` and a `message`.

## Worked example

A three-state workflow: a script preflight, an AI agent step, then an explicit
end.

```yaml
name: summarise-build
version: 1
description: Run a build, summarise the output with Claude, then finish.
initial: build

states:
  # 1. Run a build command. Validator branches on the value the script returns.
  build:
    type: script
    label: Run build
    script: |
      import { $ } from 'bun';
      const res = await $`npm run build`.nothrow();
      export default res.exitCode === 0;
    validator:
      kind: boolean
    on:
      'true': summarise # returned true
      'false': failed # returned false

  # 2. ai_agent has exactly one outgoing transition. Pick any event name —
  #    `done` is conventional. Reference the prior step's stdout via
  #    {{ build.stdout }}.
  summarise:
    type: ai_agent
    label: Summarise build output
    model: claude-haiku-4-5
    prompt: |
      Summarise this build output in two sentences:

      {{ build.stdout }}
    on:
      done: success

  # 3. Terminal nodes: an explicit success and an explicit failure.
  success:
    type: end
    label: Done
    outcome: success
    message: Build summarised.

  failed:
    type: end
    label: Build failed
    outcome: failure
    message: The build command failed.
```

See [Output chaining](/docs/authoring/output-chaining) for the
`{{ build.stdout }}` syntax.

## Integrity errors you will hit

The loader runs an integrity pass before any run starts. The most common
errors:

**Unknown transition target.** A value in `on:` does not match any state id
and is not the sentinel `done`.

```yaml
on:
  'true': sumarise # typo — should be `summarise`
```

**ai_agent with multiple outgoing transitions.** `ai_agent` is intentionally
non-branching. Use `ai_judge` if you need to route on the model's answer.

```yaml
ask:
  type: ai_agent
  on:
    'true': a # error: ai_agent cannot branch
    'false': b
```

**Boolean validator missing a target.** Boolean and confidence validators
require _both_ `'true'` and `'false'` keys when you declare any of them.

```yaml
gate:
  type: ai_judge
  validator:
    kind: boolean
  on:
    'true': pass
    # error: missing 'false'
```

**Wrong key names.** If you write `pass`/`fail` instead of `'true'`/`'false'`
for a boolean validator, the loader rejects the workflow rather than failing
silently at runtime.

**Enum validator with no routes.** Enum validators must declare at least one
route, otherwise the workflow can never advance past the node.

```yaml
classify:
  type: ai_judge
  validator:
    kind: enum
    routes: [] # error: must list at least one route
```

**Initial state does not exist.** `initial: foo` with no `foo` key under
`states` is rejected.

**Closed-loop cycle has no `maxIterations`.** Any cycle made entirely of
automated nodes (no `human` gate) must declare `maxIterations` on at least
one participating node. Otherwise the loop has no programmatic upper bound,
and the only stop is the global 1000-step safety guard. Add the cap to any
node in the cycle and the workflow loads.

```yaml
# Bad — fix and judge form a cycle with no cap anywhere
states:
  fix:
    type: ai_agent
    on: { done: judge }
  judge:
    type: ai_judge
    validator: { kind: boolean }
    on:
      'true': done
      'false': fix # <- back-edge closes the cycle

# Good — cap on either node bounds the whole loop
fix:
  maxIterations: 5
```

See [/docs/recipes/closed-loop-feedback](/docs/recipes/closed-loop-feedback)
for the full pattern, including stall detection and the human-gate exemption.

## Where to next

- [Output chaining](/docs/authoring/output-chaining) — reference upstream
  outputs in prompts, scripts, and conditions.
- [Validators](/docs/authoring/validators) — how outputs become transition
  keys.
- [Condition expressions](/docs/authoring/expressions) — branching on
  context variables.
- [Node types](/docs/nodes/ai-agent) — per-type field reference.
