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 ~/.bashrcFor 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.
- Open Edit environment variables for your account from the Start menu.
- Select Path, then Edit. In the window that opens, select New.
- 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-workflowThe 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': finishFix: 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: reviewstate '<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-endstate '<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': failedstate '<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 loopCycles 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-8Or 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
claudeis on PATH and authenticated. - Set
ANTHROPIC_API_KEYin 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
maxRetrieson 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 structuredresult:. - Switch to a stronger model (
claude-sonnet-5orclaude-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
timeoutSecondson 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:
validateWorkflowIntegrityinpackages/schema/src/workflow.ts - Lint warnings:
lintWorkflowinpackages/core/src/workflow-writer.ts - Runtime errors:
packages/core/src/runner.tsand the executor files inpackages/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 — restore a corrupted or unreadable store.
- Closed-loop feedback — the retry-until-approved loop pattern.
- LLM backends — how AI nodes reach a model.