# Common errors

This page lists the error strings you're most likely to see, what they
mean, and how to fix them. Errors are grouped by where they fire: install and
PATH, schema validation, integrity validation, lint warnings, runtime,
config / init, and the database.

## Installation and PATH

### `roscoe: command not found` after installing the desktop app

The desktop app installs the `roscoe` binary at `~/.roscoe/bin/roscoe`
(`%USERPROFILE%\.roscoe\bin\roscoe.exe` on Windows). The app itself runs fine
either way, since it launches `roscoe` directly. A terminal can't find the
command until that folder is on your `PATH`.

**Let the app do it.** The setup wizard offers to add the folder for you, and
Settings has the same control if you skipped it. On macOS and Linux that route
can also undo itself later, which the manual steps below cannot. On Windows the
app can add the folder but not remove it again, so undoing it means the same
GUI steps below.

If you would rather do it by hand:

On macOS or Linux, add the folder to your shell startup file, then open a new
terminal:

```bash
echo 'export PATH="$HOME/.roscoe/bin:$PATH"' >> ~/.zshrc   # or ~/.bashrc
```

For fish, in `~/.config/fish/config.fish`:

```fish
fish_add_path -g -P -- "$HOME/.roscoe/bin"
```

`-g -P` is not optional. Without them `fish_add_path` writes the _universal_
`$fish_user_paths` instead. That value persists after you delete the line, so
removing it later appears to do nothing.

On Windows, use the GUI. Do not use `setx` to edit `PATH`. It truncates the
value at 1024 characters. Reading the old value back with PowerShell also
expands any `%USERPROFILE%`-style entries into frozen literals, which breaks
other software that relies on them.

1. Open **Edit environment variables for your account** from the Start menu.
2. Select **Path**, then **Edit**. In the window that opens, select **New**.
3. Paste `%USERPROFILE%\.roscoe\bin`, then select **OK**.

Then open a new Command Prompt.

The standalone [CLI installer](/docs/getting-started/install) adds this folder
to `PATH` for you, so a terminal install of `roscoe` skips this step.

## Schema validation errors

These come from the Zod schema in `packages/schema/src/workflow.ts` and
`packages/schema/src/node.ts`. They fire on `roscoe validate`,
`POST /api/workflows`, `PUT /api/workflows/:id`, and the MCP (Model Context
Protocol) tools that write or validate.

### `Name must be kebab-case (lowercase letters, numbers, hyphens, underscores)`

The `name:` field at the top of your workflow YAML must match
`/^[a-z0-9_-]+$/`.

```yaml
# Bad
name: My Workflow

# Good
name: my-workflow
```

The same rule applies to workflow ids passed to MCP or REST writes. The
`id` parameter must match `[a-zA-Z0-9_-]+`, which is looser than `name`.
Uppercase is allowed here because file stems on disk can carry it.

### `quorum (<n>) cannot exceed agentCount (<m>)`

A `consensus` node can never approve when `quorum` (the number of agreeing
agents it needs) is larger than `agentCount` (how many agents vote). Bound:
`agentCount` is `2..7`, `quorum >= 1`, and `quorum <= agentCount`.

**Fix:** lower `quorum` or raise `agentCount`.

## Integrity validation errors

These come from `validateWorkflowIntegrity()`. They run after the schema
passes and check the relationships between states.

### `initial state '<id>' does not exist in states`

The `initial:` field at the top of the workflow points at a state id that
isn't defined under `states:`.

**Fix:** add the state, or change `initial:` to match an existing key.

### `state '<src>' transition '<event>' targets unknown state '<target>'`

A transition in `on:` points at a state that isn't defined.

```yaml
# Bad: `finish` isn't defined
states:
  greet:
    type: script
    script: console.log('hi'); export default true;
    validator: { kind: boolean }
    on:
      'true': finish
      'false': finish
```

**Fix:** add the missing state (in the example above, an `end` node named
`finish`).

The id `done` is the one exception: the integrity validator treats it as a
terminal even when you never declare it, so targeting `done` never raises this
error. Any other id, including a `type: end` state called `finish`, must be
declared like every other state.

### `state '<id>' (ai_agent) cannot branch — use at most one outgoing transition`

`ai_agent` nodes are designed for free-form output, not routing. They take at
most one outgoing edge, or none at all if the node ends the run. If you need
branching, switch the type to `ai_judge` and add a validator.

```yaml
# Bad: two transitions on an ai_agent
analyse:
  type: ai_agent
  on:
    next: review
    skip: done

# Good: one outgoing edge
analyse:
  type: ai_agent
  on:
    next: review
```

### `state '<id>': enum validator requires at least one route`

You declared `validator: { kind: enum }` but `routes:` is empty or
missing.

**Fix:** list the route names you want and wire them as keys in `on:`:

```yaml
classify:
  type: ai_judge
  validator:
    kind: enum
    routes: [critical, warning, info]
  on:
    critical: page-oncall
    warning: notify
    info: silent-end
```

### `state '<id>': boolean validator requires targets for all events: 'true', 'false'`

A `boolean` validator must declare both `'true'` and `'false'` transitions.
Same rule for `condition` (`'true'` and `'false'`), `confidence` validators
(`'true'` and `'false'`), and `enum` (every name in `routes` must appear).

A `consensus` node depends on which validator it carries. In its gate modes,
`boolean` and `confidence`, it routes on `approved` and `rejected`. In its
`answer` and `most_consistent` modes it routes on `decided` and `undecided`
instead.

Quote the keys: in YAML, `true:` parses as a real boolean key.

```yaml
# Bad: only one branch
on:
  'true': done

# Good
on:
  'true': done
  'false': failed
```

### `state '<id>': boolean validator requires events 'true', 'false' but on map has only: 'pass', 'fail'`

You wrote your own routing names instead of the validator's locked events.
Boolean validators always route on `'true'` / `'false'`; you can't rename
them. Use `kind: enum` if you want custom keys.

### `closed-loop cycle (<a> → <b> → <a>) has no maxIterations on any participating node`

You wrote a workflow with a cycle made entirely of automated nodes (no
`human` gate) and didn't declare `maxIterations` on any of them. The
runner has no programmatic upper bound for the loop, so the loader
refuses it.

**Fix:** add `maxIterations: N` (1-50) to any one node in the cycle. The
cap on a single node bounds the whole loop because every iteration must
pass through it. Put it where the constraint reads most naturally. Usually
that's on the node whose retries cost the most (the AI-driven fix step).

```yaml
fix:
  type: ai_agent
  maxIterations: 5
  on: { done: review }
review:
  type: ai_judge
  validator: { kind: boolean }
  on:
    'true': done
    'false': fix # back-edge: cap on `fix` bounds the loop
```

Cycles containing at least one `human` node are exempt: the human's
clicks bound the loop. See
[the closed-loop feedback pattern](/docs/recipes/closed-loop-feedback)
for the full walkthrough, including stall detection.

### Run failed with `__loopError.kind = "iteration_exhausted"`

A closed-loop workflow hit its `maxIterations` cap. The run-detail page
renders an Alert banner with the offending node and the last output. The
structured payload is also on `WorkflowRun.context.__loopError`.

**Fix:** triage the last output. Decide whether the cap is too tight or
the loop is broken upstream. Then raise the cap or fix the gate.
Common causes:

- The AI judge is too strict (raise the cap or relax the prompt).
- The fix step isn't acting on the reviewer's feedback (check that the
  fix node's prompt references `{{ review.reasoning }}`).
- The cap is set lower than the realistic worst case.

### Run failed with `__loopError.kind = "stalled"`

A node with `detectStall: true` produced an identical output and chosen
transition three times in a row. The runner short-circuited rather than
waste the rest of the cap on a stuck loop.

**Fix:** look at the last verdict in the banner. If the reviewer is
giving genuinely identical feedback, the loop isn't making progress.
Either the fix step isn't reading the verdict, or the reviewer is too
narrow. If the verdicts only _look_ identical (boolean result + similar
words) but encode different reasoning, you may want `detectStall: false`
on that node.

## Lint warnings

These come from `lintWorkflow()` (MCP `lint_workflow` tool, or the lint
panel in the web editor). They don't block writes. They're advisory.

### `dead_state — state '<id>' has no inbound transitions and is not the initial state`

You defined a state but nothing routes into it. Either delete it, or wire
a transition that points to it.

### `unreachable_state — state '<id>' is not reachable from the initial state`

There's no path from `initial:` to this state via the transition graph.
Common cause: a typo in a transition target that integrity validation
already caught. Fix that first.

### `unknown_model — state '<id>': model '<model>' is not in roscoe.yaml models.available`

Your AI node references a model id that isn't in the `models.available`
list in `roscoe.yaml`. The lint pass surfaces this so the run doesn't
fail at execution time.

**Fix:** add the model id to `roscoe.yaml`:

```yaml
models:
  default: claude-haiku-4-5
  available:
    - claude-haiku-4-5
    - claude-sonnet-5
    - claude-opus-4-8
```

Or change the workflow to reference one already in the list.

## Runtime errors

These fire while a run is executing.

### `claude CLI not found` / `not authenticated`

An AI node tried to run but Roscoe couldn't reach an AI backend (the service
that runs the model, either the Anthropic API or the `claude` CLI). The runner
prefers `ANTHROPIC_API_KEY` (Anthropic SDK); without it, it falls back to
shelling out to `claude -p`.

**Fix one of:**

- Install the [Claude Code CLI](https://claude.ai/code) so `claude` is on
  PATH and authenticated.
- Set `ANTHROPIC_API_KEY` in your environment.

See [LLM backends](/docs/configuration/llm-backends) for the full backend
auto-detection flow.

### AI node response parse failure

The AI node ran but the model's response didn't parse against the
validator's expected JSON envelope. The runner retries up to `maxRetries`
times (default 1), then fails the node. The ceiling depends on the node type:
5 for `ai_agent` and `ai_judge`, 3 for `consensus`, `round_robin`, and `map`.

**Fixes:**

- Bump `maxRetries` on the node, up to that node type's ceiling.
- Tighten the prompt. Make the JSON shape explicit and give a literal
  example in the prompt body.
- Lean on the `reasoning:` field to absorb the model's verbosity before the
  structured `result:`.
- Switch to a stronger model (`claude-sonnet-5` or `claude-opus-4-8`)
  for the validator-driven nodes.

### Script node hangs

A `script` node can pin the run if it never returns. Two common causes:
awaiting a promise that never resolves, or a command run through `Bun.$`
that blocks. Blocking commands include reading stdin, waiting on a network
call with no timeout, or an interactive prompt.
Every script has a wall-clock ceiling: `timeoutSeconds` on the node, 300s
by default. After that, Roscoe kills the worker's whole process tree and
fails the node. That's a backstop, not an excuse to skip bounding the work
yourself.

**Fixes:**

- Lower `timeoutSeconds` on the node so a stuck script fails fast instead
  of burning the full five minutes.
- Give any command you shell out to its own timeout so it rejects instead
  of hanging. Check its result with `.nothrow()` rather than blocking
  forever on it.
- Don't read from stdin (the worker has none wired up).
- Avoid interactive commands in workflows entirely. The worker has no
  terminal (TTY) attached, so a prompt never gets an answer.

For `test_workflow` specifically: `timeout_ms` cannot interrupt a single
non-yielding `script` before the node's own `timeoutSeconds` fires. Lower
`timeoutSeconds` for that workload.

## Schema / init errors

### `Workflow failed schema validation` with `name: Name must be kebab-case`

You set `name:` to something containing capitals or spaces. See above.

### `Merged roscoe.yaml failed validation: <path>: Unrecognized key`

The `roscoe.yaml` schema in `packages/schema/src/config.ts` is strict. It
rejects keys it doesn't recognise, and `packages/core/src/config-resolver.ts`
reports it in that form. Common typos: `model:` (should be `models:`),
`port:` (should be under `ports:`).

Use the JSON Schema seeded by `roscoe init` for autocomplete in VS Code (it
gets wired into `.vscode/settings.json` automatically when you run init
inside a repo).

### `models.default must appear in models.available`

The default model has to be reachable in the available list, or no
workflow can pick it up.

```yaml
# Bad
models:
  default: claude-opus-4-8
  available: [claude-haiku-4-5]

# Good
models:
  default: claude-haiku-4-5
  available: [claude-haiku-4-5, claude-opus-4-8]
```

### `[roscoe] Could not load roscoe.yaml: <reason>`

`roscoe serve` prints this when the config fails to parse. The server
exits before binding a port. Common causes: YAML syntax error,
unknown keys, missing required field.

**Fix:** open `roscoe.yaml` and check it against
`.roscoe/roscoe-config.schema.json` (also seeded by `roscoe init`). Note that
`roscoe validate` checks workflow YAML, not `roscoe.yaml`.

## Database errors

### `Roscoe can't open your database` (web UI recovery screen)

The SQLite file at `~/.roscoe/roscoe.db` is corrupted or otherwise
unreadable. Most often the database is sitting inside a syncing folder
(Dropbox, iCloud, OneDrive).

**See the full guide:** [Database recovery](/docs/troubleshooting/database-recovery).

### `503 Service Unavailable` from most API routes

Same root cause: the DB couldn't open at boot, so most routes short-circuit to
503 until you run the recovery flow.

A handful still answer, because they need the filesystem rather than the
database:

- health, session, and version
- the database reset itself
- the serve restart and install-update controls
- the MCP and CLI-path status checks
- the two onboarding routes and the analytics config

`ALLOW_WHEN_UNAVAILABLE` in `apps/server/src/app.ts` is the list. Getting a
200 from one of those does not mean the database recovered.

## Where to look

- Workflow YAML schema: `packages/schema/src/workflow.ts`,
  `packages/schema/src/node.ts`
- Integrity validator: `validateWorkflowIntegrity` in
  `packages/schema/src/workflow.ts`
- Lint warnings: `lintWorkflow` in `packages/core/src/workflow-writer.ts`
- Runtime errors: `packages/core/src/runner.ts` and the executor files
  in `packages/core/src/executors/`
- Config schema: `packages/schema/src/config.ts`

If none of the above explains what you are seeing, mail us at
support@roscoe.run. Include the error text and the workflow YAML that
produced it.

## Where to next

- [Database recovery](/docs/troubleshooting/database-recovery) — restore a
  corrupted or unreadable store.
- [Closed-loop feedback](/docs/recipes/closed-loop-feedback) — the
  retry-until-approved loop pattern.
- [LLM backends](/docs/configuration/llm-backends) — how AI nodes reach a
  model.
