Common errors

Validation, lint, and runtime errors and how to fix them.

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:

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

For fish, in ~/.config/fish/config.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 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_-]+$/.

# 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.

# 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.

# 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::

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.

# 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).

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 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:

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 so claude is on PATH and authenticated.
  • Set ANTHROPIC_API_KEY in your environment.

See 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.

# 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.

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#

View this page as Markdown

Predictable workflows from unpredictable AI