Skip to content
Like what we’re building? Star on GitHub

Iterion DSL — Validation Diagnostics

All diagnostic codes emitted during compilation (ir.Compile) and validation (ir.Validate), plus the bundle-consistency codes (C2xx) that iterion validate reports for a packaged bot. Diagnostics are either errors (block execution) or warnings (informational).

The compiler carries its own copy of each row's Fix (pkg/dsl/ir/diag_catalog.go): iterion validate prints it as the fix: line under every finding, the studio shows it in the diagnostic badge, and the MCP local_validate result carries it as hint. A code the compiler can emit that has no catalogue entry fails TestDiagCatalogCoversEveryCode, so a finding never arrives without a next step. Parse-stage codes (E0xx, pkg/dsl/parser/diagnostic.go) carry a fix line the same way.

Compilation Diagnostics

CodeSeverityDescriptionCauseFix
C001errorUnknown node referenceAn edge references a node that is not declaredDeclare the node or fix the name typo
C002errorUnknown schema referenceA node's input: or output: references an undeclared schemaDeclare the schema or fix the name
C003errorUnknown prompt referenceA node's system: or user: references an undeclared promptDeclare the prompt or fix the name
C004errorBad template referenceA {{...}} template expression is malformedUse {{vars.X}}, {{input.X}}, {{outputs.node.field}}, {{artifacts.X}}, {{attachments.X}}, {{loop.name.iteration}}, or {{run.id}}
C005errorDuplicate loop definitionMultiple edges share a loop name but disagree on max_iterationsUse the same max_iterations value or use different loop names
C006errorNo workflow foundThe file has no workflow declarationAdd a workflow name: block
C007errorMultiple workflowsMore than one workflow block foundV1 supports one workflow per file — remove extras
C008errorMissing entry nodeThe entry: node name doesn't match any declared nodeFix the entry name or declare the node
C018error/warningMissing model/backend or LLM interaction requirementsAgents/judges without model: or backend: are errors only when no default supervisor model and no auto-detectable runtime credentials are available. mode: llm routers without either value produce a warning and use the built-in runtime default. Human nodes using interaction: llm or interaction: llm_or_human must set model: or interaction_model: and must declare output:.Add model: "...", backend: "...", or configure detectable credentials/defaults for agents/judges; set explicit model/backend for LLM routers when you do not want runtime defaulting; for LLM-backed human nodes add the interaction model and output schema.
C024errorDuplicate MCP serverA mcp_server name is declared more than onceUse unique names for each MCP server
C025errorInvalid MCP server configMCP server misconfigured (e.g., stdio without command, http/sse without url)Match properties to transport type: stdio needs command; http and sse need url and must not set command or args
C039errorCompute node has no expressionsA compute node was declared without any expr: key: "<expression>" entriesAdd at least one expression mapping an output schema field to an expression — or remove the node
C040errorExpression failed to parseAn expression in a compute node or in a quoted when "..." clause isn't validCheck operators, parentheses, namespace prefixes (vars / input / outputs / artifacts / loop / run), and built-in calls (length, concat, unique, contains, join, if)
C041errorDuplicate node idTwo declarations share the same node name across agents/judges/routers/humans/tools/computesRename one — node ids are a single global namespace
C042errorReserved node nameA user node is named done or fail (those are reserved terminal targets)Pick a different node name
C044errorInvalid sandbox modeA node or workflow's sandbox: mode is outside the accepted set ("", none, auto, inline); or inline mode is missing an image/build or sets bothSet sandbox: to auto, none, inline, or omit it. Block-form sandbox config with image:, build:, env:, mounts:, or network: compiles as inline mode unless mode: is specified; inline requires exactly one of image: or build:.
C045errorSandbox auto without configReserved diagnostic code; not currently emitted by compile/validation. Normal CLI/runtime auto mode supplies a default iterion-sandbox-slim:<version> fallback when no .devcontainer/devcontainer.json is presentNo compile-time action. If an embedder disables the default image and runtime reports a missing devcontainer, add .devcontainer/devcontainer.json, provide a default image, or use inline sandbox: with image:/build: (see docs/sandbox.md).
C046errorInvalid budget costbudget.max_cost_usd is negative, NaN, or infinityUse a non-negative finite USD amount, or omit the field to disable the cost cap.

Validation Diagnostics

CodeSeverityDescriptionCauseFix
C009errorSession at convergence pointA node with await: (or multiple incoming sources) uses session: inherit or session: forkChange to session: fresh, session: artifacts_only, or session: persist
C010errorMultiple unconditional edgesA non-router node has more than one unconditional outgoing edge. A loop back-edge (as name(N)) does not count: src -> body as name(N) next to a bare src -> exit is the loop-exhaustion exit — the bare edge fires once the loop has spent its iterations — and is the one allowed shape with two unconditional edges from a nodeKeep only one default edge (plus a loop back-edge and its exhaustion exit), or use a router for fan-out
C011errorAmbiguous conditionsSame condition field appears twice with same polarity from the same sourceRemove the duplicate edge or use different conditions
C012errorMissing fallbackA node has conditional edges but no unconditional fallback and conditions aren't exhaustiveAdd when not X to complement when X, or add an unconditional edge
C013errorCondition field not booleanA when clause references a field that isn't bool in the source output schemaChange the schema field to bool
C014errorCondition field not foundA when clause references a field that doesn't exist in the source output schemaAdd the field to the schema or fix the field name
C015errorElse without conditional siblingAn else edge has no when sibling from the same source — a fallback with nothing to fall back from is just an unconditional edge wearing a misleading keywordAdd the conditional sibling(s), or use a plain src -> dst edge
C016errorUnreachable nodeA declared node cannot be reached from the workflow's entry: nodeAdd edges to reach the node, or remove the unused declaration
C017errorHistory ref not in loop{{outputs.node.history}} is used but the referenced node is not part of any declared loopAdd a loop declaration (as loop_name(N)) to the edge cycle, or remove the .history reference
C019errorUndeclared cycleA cycle (back-edge) exists without any loop declaration on its edgesAdd as loop_name(N) to the back-edge to bound the cycle
C020errorRound-robin too few edgesA round_robin router has fewer than 2 unconditional outgoing edgesAdd at least 2 outgoing edges for alternation
C021errorLLM router too few edgesAn llm router has fewer than 2 outgoing edgesAdd at least 2 outgoing edges for the LLM to choose from
C022errorLLM router edge has conditionAn edge from an llm router has a when clauseRemove the when clause — LLM routers select targets directly
C023errorLLM-only property on non-LLM routerProperties model, backend, system, user, multi, or reasoning_effort are set on a router that isn't mode: llmRemove these properties or change the mode to llm
C026errorInvalid loop iterationsA loop's max_iterations is less than 1Set max_iterations to at least 1
C027errorInvalid reasoning effortreasoning_effort has a value other than low, medium, high, xhigh, max, ultracodeUse one of the six valid values
C028errorDuplicate with-mapping keyThe same with key appears on multiple non-conditional edges to the same targetUse unique keys, or make edges conditional/convergent
C029errorUnknown outputs node referenceA {{outputs.<node>...}} template targets a node not declared anywhere in the fileDeclare the node or fix the typo
C030errorImports not resolvedThe file still carries import "lib/x.bot" lines: it was compiled alone, not as a unit, so its fragments were never merged in — and compiling it anyway would only report what the main happens to referenceValidate or run the file through a path that loads the unit (iterion validate, run, the studio); an inline source cannot import — launch the bot as a bundle
C031erroroutputs ref field not in output schema{{outputs.<node>.<field>}} references a field absent from that node's output: schemaReference an existing field, or add the field to the schema
C032warningoutputs ref on schemaless node{{outputs.<node>.<field>}} targets a node that has no output: schema, so the field cannot be verified. The same warning fires for {{input.<field>}} in an edge with mapping whose source has no output: schema — those mappings resolve from the source output, not from run inputs. An entry router copies the run payload (skipped). A mid-graph router only passes through incoming with keys (plus llm/fan_out_each bindings); a field in none of those warns.Add an output: schema, map the field onto the source, drop the access, or use {{vars.x}} for a launch-time value
C033errorUndeclared variable{{vars.X}} (or vars.X inside an expression) targets a variable not declared in the file-level or workflow-level vars: blockDeclare the variable, or fix the name
C034errorinput ref field not in the validated namespaceIn a prompt, command, or compute expr, {{input.<field>}} is checked against the consuming node's input: schema. In an edge with mapping, it is checked against the source node's output: schema (the payload available when the edge fires). Run-level inputs and vars: are a different namespace.Reference a field in that namespace, or switch to {{outputs.<node>.x}} / {{vars.x}}
C035errorUnknown artifact{{artifacts.X}} targets an artifact never produced via publish:Add publish: <name> on a prior node, or fix the artifact name
C036errorReference to non-reachable node{{outputs.<node>...}} targets a node not reachable from the entry before the consumerReorder the graph or wire an edge so the producer runs first
C037warningNode max_tokens exceeds workflow budgetA node-level max_tokens is greater than the workflow's budget.max_tokensLower the node cap, or raise the workflow budget
C038errorUnsupported MCP auth typemcp_server.auth.type is something other than oauth2 (the only wired type)Drop the auth: block, or change type to oauth2
C043errorInvalid compaction valuescompaction.threshold is outside (0, 1] or compaction.preserve_recent is < 1Use a fraction like 0.85 for threshold and an integer >= 1 for preserve_recent; omit either to inherit the default
C047warningMemory enabled on unsupported backendA node sets memory: enabled: true but its resolved backend doesn't read the field — only the claw backend wires memory tools today. The check is informational; the run still proceedsDrop the memory: block, or switch the node to backend: claw (or another backend that has memory wired).
C048errorMemory missing scopeA node sets memory: enabled: true without a non-empty scope: <name> — the runtime needs the scope to locate ~/.iterion/projects/<key>/memory/<scope>/Add scope: <name> to the node's memory: block (the scope becomes the directory the memory_read / memory_write / memory_list tools operate against).
C049warningArtifact labels without publishA node sets artifact_labels: but has no publish:, so the labels have no artifact to attach toAdd publish: <name>, or remove the artifact_labels: (judge nodes never publish, so their labels are dropped at compile time)
C050errorDuplicate attachmentAn attachment name is declared more than once across file-level and workflow-level attachments: blocksRename the duplicate, or merge the definitions
C051errorAttachment / var name collisionAn attachment name collides with a declared vars: entryRename one of them — attachments and vars share a single template namespace
C052errorInvalid attachment MIMEAn accept_mime: entry is not in type/subtype form (e.g. image/png, application/pdf)Use type/subtype MIME values, optionally with * subtype wildcards
C053errorUnknown attachment reference{{attachments.X}} references an attachment that is not declared in a file-level or workflow-level attachments: blockDeclare the attachment, or fix the name
C054errorUnknown attachment sub-field{{attachments.<name>.<subfield>}} uses a sub-field the runtime does not exposeDrop the sub-field or pick a supported one (path, url, mime, size, sha256)
C055errorBad prompt includeA prompt {{include "..."}} marker could not be resolved: the file is missing, is a directory, exceeds the 256 KiB cap, uses an absolute path, or escapes the .bot directoryPoint the include at an existing file inside the .bot's directory subtree, below the size cap
C060errorPlaywright MCP server requires browser-capable sandbox imageAn MCP server with the Playwright transport is configured but the workflow's sandbox image is not browser-capableUse ghcr.io/socialgouv/iterion-sandbox-browser (or another browser-capable image whose name matches the validator predicate, such as one containing sandbox-browser or sandbox-full-browser), or remove the Playwright MCP server
C070errorPreset references unknown variableA presets: entry sets a key that does not match any name in vars:Add the variable to vars:, or remove/rename the preset key
C071errorPreset value type mismatchA presets: value's type (string/int/bool/list) does not match the declared vars: typeCast the value to the declared type, or change the var's type
C072errorDuplicate preset nameThe same preset name appears more than once in the presets: blockRename or merge the duplicate preset
C080warningUnknown capabilityA capabilities: entry isn't in the built-in registry (currently: board.read, board.create, board.move, board.assign, board.label, board.close, board.comment, watch.subscribe, watch.unsubscribe)Either fix the typo or accept the warning — unknown caps still propagate to the executor (the registry is open for extension)
C081errorMalformed capabilityA capabilities: entry doesn't match the shape domain or domain.action (lowercase letters/digits/underscores)Use the lowercase domain.action form, e.g. board.create
C082warningBoard capability inside sandboxA node grants a board.* capability while running under a sandbox — the stdio __mcp-board transport is unavailable, the runtime falls back to the HTTP transport on the iterion serverNo action required if the iterion HTTP server is reachable from the sandbox; otherwise drop the capability or disable the sandbox for that node
C083warningUnknown cursor referenceAn agent/judge cursors: setting references a cursor name not declared at workflow scopeDeclare it with cursor <name>: or drop the setting — see docs/cursors.md
C084errorInvalid cursor valueA cursor invocation value is not in the enum, falls outside [0, 1], or doesn't match any band. ${VAR} values defer to runtimeUse a declared enum value or a numeric in range; for env-driven values, ensure the substituted result is valid
C085errorMalformed cursor declarationA cursor <name>: block declares neither values: nor bands:, declares both, has overlapping bands, or has a range outside [0, 1]Pick exactly one form (enum or numeric); ensure bands cover disjoint sub-ranges of [0, 1]
C086errorDuplicate cursor nameThe same cursor <name>: declaration appears twice in one sourceRename one of them, or merge their values: / bands: entries
C087warningUnknown providerA provider: chain token is outside the known provider setFix the provider name, or accept the warning for a newly-added provider
C088warningProvider chain ignoredA multi-element provider: chain is set on a backend that ignores the hint (claw/codex)Drop the extra chain elements, or switch to a backend that honours provider hints
C089warningUltracode model gatereasoning_effort: ultracode on a model other than claude-opus-4-8 — the orchestration half is 4.8-only, so it degrades to plain xhighUse model: "claude-opus-4-8" for full ultracode, or accept the xhigh degrade
C090errorDuplicate secretA secret name is declared more than once in the secrets: blockRename or merge the duplicate
C091errorSecret / var name collisionA secret name collides with a declared vars: entryRename one — secrets and vars share a template namespace
C092errorInvalid secret hostA secret's egress host scoping (Layer 2 hosts:) is ill-formedUse valid host entries (hostnames / domains)
C093errorUnknown secret reference{{secrets.X}} references a secret not declared in the secrets: blockDeclare the secret, or fix the name
C094errorMalformed file secretAn as: file secret declaration is malformed (a bad mount_path:, an env: that is not an identifier)Fix the offending property. A bare as: file (optionally optional: true) is complete on its own — it resolves the stored secret by name and mounts it; value:/env:/mount_path: are additions, not requirements
C095errorUnsupported secret sub-field{{secrets.X.<subfield>}} uses a sub-field the runtime does not exposeDrop the sub-field, or reference {{secrets.X}} directly
C097errorUnbounded loop without fuelAn as name(unbounded) loop has no fuel ceiling (neither a per-loop unbounded <N> nor a workflow budget.max_iterations) — the "no silent infinity" invariantAdd a per-loop fuel (as name(unbounded 200)) or a workflow budget.max_iterations
C098warningUnbounded loop without exitAn unbounded loop's body has no edge leaving the loop — only fuel/liveness can stop itAdd a when-exit (convergence condition) so the loop terminates by its own logic
C100errorReview without worktreeinteraction: review without worktree: auto — there is nothing to mergeAdd worktree: auto, or drop the review interaction
C101warningReview URL unknown refreview_url references an output node that does not existFix the node reference, or remove review_url
C102errorInvalid compress valuecompress: is not one of on, off, ultraUse on, off, or ultra
C103errorInvalid policyA Verified Action tool node's policy: is not one of required, recover, best_effort (ADR-044)Use a known policy value
C104errorRecovery without postconditionA tool node configures recovery: (or policy: recover) without a postcondition: — the deterministic truth oracle that makes adaptive recovery safeAdd a postcondition:, or drop the recovery
C105errorRecovery on a gateRecovery rungs are attached to a GATE (a node where recipe == postcondition)Remove the recovery: block — gates stay deterministic; never attach LLM recovery to a gate
C106warningRecovery without recover policyrecovery: bounds are present but policy: is not recover — dead configSet policy: recover, or remove the recovery bounds
C107warningExpression operand type mismatchA comparison inside a compute or quoted when "..." expression has statically-known operands of incompatible type classes (e.g. string[] == int, count < "x")Compare compatible types. Inference is conservative: json (= any) fields, vars, and unresolved refs bail to "no opinion" and are never flagged
C108warningwhen-expression not booleanA quoted when "<expr>" is a bare numeric value (e.g. when "count") rather than a boolean — int/float coerce to truthy, which is rarely the author's intentUse a comparison such as when "count > 0". Bare bool, string[], and string values ride the documented truthy idiom and are not flagged
C109errorVar default type mismatchA vars: entry's default literal type doesn't match its declared type (e.g. count: int = "x")Fix the default to match the type. intfloat widening is allowed (ratio: float = 5); json and string[] accept loose literals and are never flagged
C110errorInvalid permissionpermission: is not one of off, ask, denyUse off, ask, or deny
C111warningPermission rules without gateallow/ask/deny rule lists are declared but the resolved permission mode is ""/off, so they never applySet permission: ask or deny, or remove the rule lists
C112warningTool-node permission inertpermission: on a tool node — parsed but not enforced (a tool node runs a fixed command, not an agent)Remove the permission: from the tool node; gate the agent nodes instead
C113errorfan_out_each without overA fan_out_each router has no over: array sourceAdd over: "{{...array...}}" to the router
C114errorfan_out_each property on non-fan_out_eachover/as/key/depends_on set on a router that isn't mode: fan_out_eachRemove the property, or change the router mode
C115errorfan_out_each edge countA fan_out_each router must have exactly one outgoing template edgeKeep a single template edge from the router
C116errorUse references unknown groupA use ... as statement references a group that is not declaredDeclare the group, or fix the name
C117errorUse param mismatchA use provides an unknown param, or omits a declared oneMatch the group's declared params exactly
C118errorforeach conflicts with loopAn edge combines as foreach with as <loop> (mutually exclusive)Use one iteration form per edge
C119errorsubbot without sourceA subbot node has no source: child .botAdd source: <path>.bot to the subbot
C120warningIndex on scalarA subscript [...] is applied to a statically-scalar value (string/bool/int/float), which is not indexableIndex an array/map, or drop the subscript
C121errorEnum literal never matchesA when "field == 'literal'" / != comparison (or a compute expression) compares an enum-typed field against a literal that is not one of its enum: values — the comparison can never match, so it is almost always a typoUse a declared enum value, or fix the field's enum: set. json fields and unresolved refs are never flagged
C122errorInvalid node timeoutAn agent/judge timeout: is not a positive Go duration (after ${VAR:-default} expansion)Use a positive Go duration string, e.g. timeout: "20m" or "1200s"
C123errorMultiple else edgesA source node has more than one else edge — two fallbacks firing on the same miss is the C010 ambiguity under a new nameKeep exactly one else per source
C124errorElse alongside unconditionalA source node has both an else edge and a bare unconditional edge — two competing defaultselse IS the fallback: remove the bare unconditional edge, or drop the else keyword
C125errorVar enum on non-string typeA vars: entry declares an [enum: ...] constraint on a non-string type (e.g. count: int [enum: "a"]) — enums constrain string values onlyDeclare the var as string, or drop the constraint
C126errorVar default not in enumAn enum-constrained var's default is not one of the declared values (e.g. mode: string [enum: "a", "b"] = "c"). Launch-provided values get the same check at run start: the engine rejects any --var / HTTP / dispatcher / preset value outside the enum setUse one of the enum values as the default, or extend the list
C127warningDuplicate var enum valueThe same value appears more than once in a var's [enum: ...] list — the duplicate is dropped (first occurrence kept, order preserved)Remove the duplicate value
C128warningSandbox opt-outThe workflow (or a node) explicitly declares sandbox: none — every tool and shell command runs directly on the host/runner with its credentials and filesystem, while sandboxing is the engine defaultRemove the sandbox: none block to run sandboxed; keep the opt-out only if the flow genuinely needs the host environment
C129errorfile field outside a human pauseA schema used as the output: of a node that never pauses for an operator declares a file-typed field — any non-human node, or a human node with interaction: llm (auto-answered by a model) or interaction: review (output is the engine-built verdict). Only an operator upload at a real pause produces oneMove the file field to a human node's output: schema with interaction: human (or llm_or_human, which can escalate to a pause), or use string if the node computes a path itself
C130errorReserved answer keyA human node's output: schema declares a key the engine writes on resume (_attachments, which carries the gate's ad-hoc operator uploads) — the operator's answer would be silently replacedRename the field. The reservation applies only to human gates; elsewhere the name is an ordinary field
C131errorInvalid auto_memory valueauto_memory: (workflow or agent/judge node) is not one of on, off — a typo would silently read as "inherit", i.e. offUse on or off, or drop the field to inherit
C132warningauto_memory on an unsupported backendAn agent/judge node explicitly declares auto_memory: on but its backend: does not consume MEMORY.md (claude_code, claw and pi do)Switch to one of those backends, or drop the auto_memory: field
C133errorInvalid loop_budget_guard valueThe workflow's loop_budget_guard: is not one of on, off — a typo would silently read as "inherit", i.e. the default on, keeping a guard the operator meant to liftUse on or off, or drop the field to inherit ITERION_LOOP_BUDGET_GUARD then the default
C134errorInvalid repo_devbox valueThe workflow's repo_devbox: is not one of on, off — a typo would silently read as "inherit", i.e. the default on, and the target repo's whole toolchain would keep installing at every runUse on or off, or drop the field to inherit ITERION_REPO_DEVBOX then the default
C135error on an identifiable mistake, warning otherwiseUnknown tool nameAn agent/judge tools: entry (or a Verified Action's recovery: agent_tools:) names a bare tool the registry cannot resolve, on a backend where the list is a real constraint — claw, whether as the node's own backend or as a fallbacks: route. CLI backends run their own native toolset and ignore the list, so nothing is reported there. It blocks only what the compiler can positively identify: a legacy phantom name (list_files, run_command, git_diff, search_codebase, …), a near-miss typo of a real built-in, or a ${VAR}/{{ref}} entry (tools: is the one node field iterion never expands). Any other unrecognised name warns: a bare name also resolves onto an MCP tool when unique across the connected servers, and the ambient catalog (project .mcp.json, enabled plugins) is merged after compilation. Visible MCP wiring (mcp_server: or an mcp: block on the workflow or node) softens even the identifiable names. Never flagged: qualified MCP references (mcp.<server>.<tool>, the mcp__server__tool alias, wildcards) and iterion's own board/watch tools by their bare name (create_issue, subscribe, …)Use a claw built-in (read_file, write_file, file_edit, glob, grep, bash, web_fetch, …) — the diagnostic names the nearest match; for an MCP tool use its mcp.<server>.<tool> name
C136warningGated backend needs a host-side runTwo shapes on a workflow that has not opted out of the sandbox: (1) a node routes permission: ask|deny to grok or kimi (primary or fallback) — those two enforce the gate through a host-side PreToolUse hook the sandbox cannot reach; (2) a node routes an ask-capable policy (mode ask, or any explicit ask: rule, which outranks mode deny) to claw — sandboxed claw enforces deny-shaped policies in-container (the policy crosses the IPC pre-task), but an Ask decision cannot pause the parent run from inside the container. Either way the node is refused at execution time, and the shipped default sandbox: auto hits this on the common shape (no sandbox: block)Declare sandbox: none on the workflow (or the node), launch with --sandbox none / ITERION_SANDBOX_DEFAULT=none, or (claw shape) drop the ask rules / use deny. A warning rather than an error because those run-time overrides make the workflow legal without it saying anything
C137warningTool command quotes a ref the runtime already quotesA tool command: wraps a {{ref}} in single quotes of its own (BASE_REF='{{vars.base_ref}}', --out '{{input.dir}}/f.json', STD='--flag {{input.x}}'). The runtime shell-escapes every ref by wrapping the value in single quotes, so the author's quote CLOSES it instead of nesting: the value lands as bare shell syntax, and on a forge-controlled var (a fork PR's branch name, a title) that is command execution. A warning rather than an error because the shape is inert for values without shell metacharacters, so a repo full of them keeps running while it is cleaned up. Double quotes are reported too, for a different reason: the runtime's single quotes survive as DATA (X="{{ref}}" with main hands the interpreter 'main') and a value carrying " closes the author's span and injectsRemove the surrounding quotes — the runtime adds them. For an optional flag, build it with ${VAR:+--flag "$VAR"} from a bare VAR={{ref}} assignment
C138errorBuiltin call the evaluator cannot satisfyA compute expression or a quoted when "..." calls a builtin with an argument count outside the range that builtin accepts (length(a, b), slice(a, b), max()). The NAME is caught by C040 at parse; the ARITY is not visible there, so such a call used to compile clean and die mid-run — which on a cloud launch costs a sandbox, a clone and a plan phase before it is discovered. The most common cause is a bot authored against a NEWER engine, whose builtin accepts a shape this one does not (min/max gained their variadic form)Fix the call, or run it on an engine whose evaluator accepts that shape. Accepted counts: length/unique/sort/keys/values/sum/flatten/floor/round take 1; contains/join/tail take 2; if/slice take 3; concat/min/max take 1 or more
C140warningNode references an empty schemaA node's input: or output: names a schema that has no field — a legal declaration (the studio saves a schema the moment it is created), not a contract a model can fill: the node's output would always be an empty objectGive the schema at least one field, or point the node at another schema
C141warningUse of an empty groupA use <group> as <prefix> instantiates a group that declares no node, so the instance is nothing — and nothing else reports it unless a node of the instance is referenced. A bare group g: is a legal declaration (the studio saves it the moment it is created), or a body that landed at the wrong indentation after a blank lineGive the group at least one node, or drop the use
C139errorInvalid workspace_checkpoint valueThe workflow's workspace_checkpoint: is not one of on, off — a typo would silently read as "inherit", i.e. the default on, and the run would keep force-pushing its whole sandbox tree as an iterion/run-<id>-checkpoint branch onto the repository it was pointed at, which is exactly what an author writing this field is stoppingUse on or off, or drop the field to inherit ITERION_WORKSPACE_CHECKPOINT then the default
C142errorInvalid worktree valueThe workflow's worktree: is not one of auto, none — the runtime reads auto and nothing else, so a typo ran the workflow IN PLACE, in the operator's own checkout, with every commit the run made landing there, without a wordUse auto (a fresh git worktree per run, finalised into a branch) or none (run in place), or drop the field for auto
C143warningEscaped quote in a shell stringsandbox.post_create contains a backslash-escaped quote. In a "…" value the lexer keeps every \X verbatim unless the file opts into ## strict-escape: on, so the backslash reaches the shell, which reads \" as a literal quote character: the command runs with quotes inside its arguments instead of around them. Since post_create is best-effort the failure is never read — measured on a bootstrap that died with npm error code EINVALIDPACKAGENAME and had therefore never run once. A warning, because the same value can arrive from a backtick raw string or a | block scalar, where \" is verbatim by design and correct inside a shell double-quoted region; the compiler cannot tell the three apart at this pointIn a "…" value: leave a space-free value unquoted, or use single quotes when it has spaces. In a backtick or block-scalar value: ignore
C144warningProfile 1 assumed, and it mattersThe file has no dsl: header, so it is read as profile 1 — and profile 2 would read it otherwise somewhere: a \ inside a "…" literal (kept verbatim by profile 1, decoded by profile 2), a blank line inside a prompt body (dropped by profile 1, kept by profile 2). Reported by iterion validate only, never at launch; a headerless file with nothing profile 2 reads otherwise draws nothingRun iterion dsl migrate --to 2 <file>: it adds the header, re-spells every literal so its value is unchanged, and names the prompts whose paragraph breaks will now reach the model. Or keep profile 1 knowingly
C170errorInvalid memory visibilitymemory: visibility: has an unknown valueUse a known visibility (bot/project/cross_project/user/org/global)
C171errorMemory visibility conflictmemory: visibility: is combined with the legacy project_root:Use visibility: alone — drop the legacy project_root:
C172warningMalformed provider stepA provider: chain element of the provider:model form has an empty provider or model partProvide both parts, e.g. anthropic:claude-sonnet-4-6
C173errorMalformed fallback routeA fallbacks: route declares neither backend, model nor provider (it would re-issue the identical failing call), duplicates another route's name, or switches backend without pinning its own model: (model specs are not portable across backends)Give the route a distinct target; a route that changes backend: must also set model:
C174warningCommand ignoredA per-node command: CLI-binary override is set on a backend that ignores it (claw/codex) — only claude_code honors itSwitch to backend: "claude_code", or drop the command:
C175warningUnknown fallback triggerA fallbacks: route's on: list names a category the runtime does not classifyUse usage_window, auth, unavailable, transient_exhausted or any
C176errorUnsafe fallback crossingA fallbacks: route would silently change what the node can DO: it runs on a backend that cannot enforce the node's permission: gate, or it crosses the claw⇄CLI boundary on a node with an empty tools: list (empty means NO tools on claw and the FULL native toolset on a CLI backend), or it changes backend: on a node with session: inherit / inherit_if_available / fork / persistRoute to a gate-enforcing backend (claude_code/claw/pi), declare an explicit tools: list, or drop session continuity when crossing backends
C177warningFallback capability driftA fallbacks: route runs on a backend that silently ignores one of the node's settings (e.g. reasoning_effort:)Accept the degradation, or pin the route to a backend that honours the setting
C190warningSupervisor watches non-agentA supervisor watches: a node id that isn't an agent nodeWatch an agent node, or fix the node id
C191warningMalformed supervisorA supervisor declaration is malformed (e.g. a bad cooldown duration)Use a valid Go duration for cooldown
C192errorDuplicate supervisorThe same supervisor <name>: is declared twiceRename or merge
C193warningUnknown supervisor promptA supervisor's system: references an undeclared promptDeclare the prompt, or fix the name
C194errorInvalid resource capacityA resources.<name> capacity is ≤ 0Use a capacity ≥ 1
C195errorUnknown resource in needsA needs: references a resource not declared in the resources: blockDeclare the resource, or fix the name
C196errorEvent node without nameAn emit/wait node has no event: name (ADR-051)Add event: "<name>"
C197errorWait without timeoutA wait node has no (or an invalid/non-positive) timeout: — the mandatory bound, the "no silent infinity" invariant for eventsAdd timeout: "30s" (a positive Go duration)
C198warningDangling eventA wait awaits an event no emit produces (it can only ever time out), or an emit produces an event no wait consumes (dead event)Pair each wait with an emit of the same event name (a wait on an externally-sourced event is expected to warn until external-event support lands)
C199warningInvalid skill referenceA skills: entry (on a node or the workflow) is not a valid skill name — a single path segment of letters/digits/./-/_, not starting with a dot (ADR-059). Existence in the library is resolved at run time, not here, so an unknown-but-well-formed name does not warnFix the name; quote kebab-case names (skills: ["changelog-writer"])
C240errorAsync interaction on human nodeinteraction: async is set on a human node — async questions are posted by agent/judge nodes; a human node IS the blocking questionMove interaction: async to the asking agent/judge and use an await_answers node as the sync point
C241errorawait_answers without timeoutAn await_answers node has no (or an invalid/non-positive) timeout: — the mandatory bound, the "no silent infinity" invariantAdd timeout: "30m" (a positive Go duration)
C242warningawait_answers with dead fromAn await_answers from: names a node that is missing or not an interaction: async agent/judge — no async question can originate there, so the await can only ever time outFix the from: reference, or set interaction: async on the referenced node
C243errorPersist in fan-out bodyA node with session: persist sits in a fan_out_all / fan_out_each / llm multi: true subgraph (before the join)Move persist to the trunk after the join, or use session: fresh on the parallel nodes
C244errorBounded iteration crosses a parallel-branch boundaryA loop/foreach edge has no single branch owner: it leaves the fan-out router, re-enters a body from the collector, crosses sibling branches, or touches a node shared by multiple branch scopes — or a loop/foreach NAME is reused across two scopes (a trunk back-edge and a branch-local one, or two sibling branches), since edges sharing a name fold into one loop and a branch-local counter would shadow the enclosing one. Cycles wholly contained in one fan_out_all, fan_out_each, or llm multi: true branch are valid and receive private durable counters. On an llm multi: true router each branch is bounded by the collector its OWN edge can elect: the model may select that edge alone, and the runtime then elects a collector from the narrower set, so a cycle that only looks branch-local against the full declared set is rejectedKeep every node in the cycle inside one branch and give each scope's loop its own name; otherwise move the loop to the trunk (after the join or wrapping the router), or use a subbot for explicit isolation
C245errorTrunk-only human mode in parallel branchA human node inside a fan_out_all, fan_out_each, or llm multi: true body declares interaction: review or interaction: llm_or_human; their companion/auto-answer orchestration is trunk-onlyUse a plain interaction: human branch gate, or move the review/LLM-assisted gate after the collector
C246warningImplicit fan-out collector execution moved into branchesA node was previously elected as an implicit collector only because a bounded back-edge counted as its second predecessor; bounded iteration is now branch-local, so the node executes once per branchAdd await: wait_all or await: best_effort to the intended collector to preserve one trunk execution, or keep it unmarked when per-branch iteration is intentional
C247errorMalformed fail codeA fail <name>: declaration's code: is not an UPPER_SNAKE identifier (^[A-Z][A-Z0-9_]*$). The value is persisted as the run's failure_code and read by machines — iterion runs list, the studio, the merge-gate notice, the alert sinks — so a lowercase, spaced or digit-leading code would reach all of them. The node still compiles, untyped, so a naming mistake does not cascade into unreachable targetsRename to the shape the engine's own codes use: code: PLAN_BUDGET_EXHAUSTED
C248errorFail code collides with an engine codeA fail <name>: declaration's code: is one of the engine's own store.FailureCode values (BUDGET_EXCEEDED, TIMEOUT, RATE_LIMITED, USAGE_LIMIT_BLOCKED, FAIL_NODE, …). The vocabulary is open-world for READERS but not for writers: the engine reads those codes as control flow — the --auto-resume allow-list and the cloud runner's usage-window retry key on them — so a deliberate refusal wearing one would be auto-retried as a transient provider fault, and since a resumable: true fail re-executes the same guard, every attempt burns for nothing. The reserved set is derived from store.ReservedFailureCodes, never hand-copiedPick a code of the bot's own: code: PLAN_BUDGET_EXHAUSTED
C249warningDuplicate fan-out targetA branch-spawning router — fan_out_all, or an llm router with multi: true — declares more than one edge to the same target node. Both modes spawn one goroutine per outgoing edge and derive the branch identity from the TARGET (branch_<router>_<target>), so those executions share one branch id: they collapse onto one output slot at convergence and onto one durable BranchCheckpoint whose cursor each goroutine overwrites, which lets a resume restart one execution at the other's position. Every outgoing edge counts, conditional or not — fan_out_all takes them all without evaluating any condition. Warned, never refused: the shape has always compiled. fan_out_each is immune (its branch ids are item-indexed, branch_<router>_<i>) and round_robin / single-select llm / condition spawn no branch at all, so none of them warnRemove the duplicate edge; on fan_out_all, use a fan_out_each router when the intent is N executions of the same node
C268errorNamed session slot without persistenceAn agent or judge declares session_slot without session: persist; a shared durable slot has no meaning for fresh/inherit/fork modesSet session: persist, or remove session_slot
C260errorMalformed action idA tool node's action: is not a <connector>.<resource>.<verb> id (lowercase segments, at least three of them). The id is what ADDRESSES the operation in a connector package, so a malformed one resolves to nothing — and it is worth catching at compile time because the alternative is a run that reaches the launch, resolves the catalog, and fails on its first nodeWrite the operation's full id: action: forgejo.issue.comment
C261errorAction without a connectionA tool node declares action: but no connection:, or names it with a {{…}} template. A connector call carries a credential resolved from a named connection; without one there is nothing to authenticate with, and no default is safe to invent — a run must not pick a tenant's credential on an author's behalf. A template is refused for the same reason it is on action:/retry:/timeout:, and one of its own: the alias is not rendered (nothing renders it, and no ref check inspects it), so it would fail mid-run reading as a missing connection — and an alias computed from an output would let an upstream node choose which credential the call carriesAdd connection: <alias>, written as the literal alias iterion connections add --alias stored
C262errorRecovery on a deterministic actionA tool … action: node declares recovery: or policy: recover. ADR-044's ladder ends in an LLM repairing the recipe, which is precisely what an action node promises does not happen: its whole offer is that no model decides the operation, builds the arguments or reads the answer. Refused at compile time rather than left to an operator to avoid, because the promise is what a .bot author is relying onDrop the recovery block; branch on the typed failure with a when edge, or use a command: recipe if a repair ladder is genuinely wanted
C263errorPostcondition on a deterministic actionA tool … action: node declares postcondition:. A postcondition is a SHELL command judging success, and on an action the vendor's own typed answer is the truth — letting an exit code overrule it would let a node report success on a call that failed, or failure on one that workedDrop it; the operation's result and error class are its success oracle
C264errorMalformed action parameterA params: entry has no name, or the same key is declared twice. Silently keeping one of a duplicate pair would send a value the author did not write, with nothing in the run to notice itName every entry, and declare each key once
C265errorMalformed action timeout or retrytimeout: is not a Go duration, or retry: is not a count of extra attempts. A value that merely LOOKS like a bound is worse than none: the call would go out unbounded while the .bot reads as if it were cappedWrite timeout: 30s / timeout: 2m, and retry: 3 — a duration is refused there, the delay between attempts being the vendor's Retry-After to name
C266warningConnector property without an actionconnection:, params:, retry: or timeout: appears on a tool node that declares no action:. The property is INERT there — read only by the connector recipe — and an inert property reads as configured: a node looks bound to a connection it never uses, and command: go test ./... with timeout: 30s runs with no bound at all. A warning, not an error: the node is otherwise perfectly runnableRemove the property, or add the action: it belongs to
C267errorAsync interaction unsupported by backendAn async agent/judge selects codex, kimi, or grok as its primary backend or an explicit fallback; these backends do not expose the async question toolsUse claw, claude_code, or pi with RPC transport. Routes resolved at runtime are checked before dispatch and fail with CAPABILITY_UNSUPPORTED when incapable

Parallel-branch migration note: expression-form guards (when "...") are now evaluated against each branch's private runtime scope. Older runtimes skipped those edges inside fan_out_all, fan_out_each, and llm multi: true bodies and selected the fallback instead. This is a runtime behavior change, not a new diagnostic; validate the guarded and fallback routes when upgrading.

Note on C103C106 (Verified Actions, ADR-044): these four codes are the adaptive-recovery firewall on deterministic ACTION tool nodes. The enum-literal type check that earlier releases emitted as C103 is now C121 — a C103 always means invalid policy. See docs/adr/044-adaptive-recovery-for-deterministic-action-nodes.md.

Bundle Consistency Diagnostics (manifest ↔ workflow)

These C2xx diagnostics come from pkg/bundlelint, emitted when iterion validate runs on a bundle (a .botz archive or a directory with manifest.yaml + main.bot). They cross-check the manifest against the compiled workflow — something neither the manifest parser (pkg/bundle) nor the DSL compiler (pkg/dsl/ir) can do alone, because each only sees one side. They are reported under a separate bundle_diagnostics list in --json output. Manifest/workflow chat-contract violations (C205–C209, C212) and the memory identity mismatch (C230) are errors; warnings are surfaced but do not fail validation.

CodeSeverityDescriptionCauseFix
C200warningdispatch_vars key not a workflow varA manifest.dispatch_vars key names no variable in the workflow vars: blockDeclare the var in main.bot, or fix/remove the manifest key — an undeclared key is silently dropped at dispatch time
C201warningcontext_vars key not a workflow varAn invocations[].context_vars key names no workflow varSame as C200
C202warningschedule.default_vars key not a workflow varAn invocations[].schedule.default_vars key names no workflow varSame as C200
C203warninglaunch_vars key not a workflow varA forge.webhook.launch_vars key names no workflow varSame as C200
C204warningargs_var not a workflow varAn invocations[].args_var names no workflow var, so the trigger's free-text payload is droppedDeclare the var, or fix the name
C205errorchat node not in workflowA chat.nodes key names no compiled workflow nodeFix the node id or add the node to main.bot
C206errorchat human maps to non-human nodeA manifest kind: human entry points at a compiled node whose kind is not humanAlign the manifest kind and workflow node kind
C207errorchat output field missing or wrong typesummary_field, text_field, or approved_field is absent from the node output schema, or is not respectively string/string/booleanDeclare the exact field with the required type, or fix the manifest field name
C208errorchat seed_var missing or not stringchat.seed_var names no workflow var, or that var is not typed stringDeclare a string launch var or fix/remove seed_var
C209errorchat launcher var missing or not stringA chat.launcher_vars[].name names no workflow var, or that var is not typed stringDeclare the string var or fix/remove the launcher entry
C210warningforge secret not declaredThe forge secret the bot expects (forge.secret, default forge_token) has no matching declaration in the main.bot secrets: blockDeclare secrets: { <name>: { as: file, optional: true } }, or point forge.secret at an existing secret. Only checked when the bot is forge-triggerable (has forge.events or a kind: forge invocation)
C212error / warninghost-event gate half-declaredThe manifest's chat.nodes.<id>.host_event_field and the workflow node's interaction: human_or_host disagree. Error when the node declares the mode with no field to receive on — the gate advertises a standby nothing can ever deliver. Warning the other way: the gate works, but reading main.bot gives no hint that anything but the operator can resume itDeclare both halves: interaction: human_or_host on the human node, and host_event_field: <name> in the manifest with a matching json field in the node's output schema
C211warningforge secret not a file mountThe forge secret is declared but not as: file — managed forge tokens are bound as a file mountSet as: file on the secret declaration
C220warningmanifest capability granted by no nodeA manifest.capabilities entry is granted by no workflow-level or node-level capabilities: listAdd it to a node's capabilities:, or drop it from the manifest (it is documentation-only otherwise)
C221warningfrontmatter capabilities override manifestThe main.bot ## --- frontmatter declares capabilities: that differ from and silently override the manifest'sKeep one source of truth — drop the frontmatter list or align the two
C222warningbundle does not openThe document is a bundle's main.bot (an iterion manifest.yaml/.yml or a skills/ beside it) but the bundle does not open — its manifest does not decode — so the studio validated the document alone, without the bundle's prompts/*.md, presets and skills; a reference to a bundle prompt reads as C003 until it opens (iterion validate refuses the same state outright)Fix the manifest named in the message (iterion validate <bundle dir> refuses with the same decode error)
C223warningmanifest beside main.bot not readA file named manifest.yaml/.yml sits beside the main.bot but was not read as this bundle's manifest — it carries top-level keys no iterion manifest has and none only ours have (a typo in the only distinctive key), or the parser cannot read it, or it exceeds 1 MiB — so the file was validated ALONE, without the prompts/, presets and skills beside itFix the manifest the message names (the reason says which keys); a manifest of another tool beside a loose main.bot is the expected case and needs nothing
C230errorper-bot memory name mismatchA node uses memory: visibility: bot, but the manifest name, workflow name, and bundle dir name are not all identical — so the bot's memory tree splits across CLI (workflow name) and dispatcher (bundle name) launchesMake all three identical
C231warningskill has no name:A skills/*.md file has no name: frontmatter, so it is undiscoverable by name once mirrored into .claude/skills/Add name: <kebab-case-id> to the skill frontmatter
C232warningskill has no description:A skills/*.md file has no description: frontmatter, so the router (Nexie) has no signal for when to select itAdd a description: saying what the skill is for and when it applies
C233warningskill description: too terseA skill description: is present but too short to route on (e.g. "Security stuff")Describe what the skill does and the situation it applies to. Routability only — no phrasing template is imposed
C234warningduplicate skill nameTwo skills/*.md files declare the same name:, so one silently clobbers the other when mirroredGive each skill a unique name:
C250errorengine requirement unmetThe manifest declares requires: { iterion: ">= X.Y.Z" } and this build is below it — the bundle uses something the running engine does not haveUpgrade iterion, or lower requires.iterion to a build that carries what the bot uses. The same predicate refuses the bundle at iterion remote admin bots push (409, --force overrides) and at launch on a runner (BOT_REQUIRES_NEWER_ENGINE, terminal)
C251warningengine requirement uncheckedA requires.iterion is declared but this build carries no orderable version (a dev build, a fork's naming scheme), so the comparison could not runValidate with a released build, or one built through task build (which injects the version). Reported rather than passed in silence — an unchecked contract that reads as satisfied is what the declaration exists to prevent
C252warningsyntax profile needs a floorThe bundle's main.bot, or a subbot child it reaches, declares dsl: 2 or carries import lines, and the manifest declares no requires.iterion, or one below the release that reads it (v3.141.0 for profile 2, v3.145.0 for import). A cloud runner receives the main workflow as an AST but re-parses a child as text with its own binary, so a build older than the profile fails at that parse, after admission — a floor declared but lower admits exactly those builds. Emitted for a bundle without a manifest too (one known by its skills/)Declare requires: { iterion: ">= 3.141.0" }iterion dsl migrate writes the migrating build's own version, which is at or above it. The push admission refuses the same bundle with 409 (--force overrides, loudly), through the same predicate (bundle.CheckProfileFloor)
C253warningsubbot child not read for its profileA subbot source: resolves beyond what the profile walk reads — a sibling bundle (../other/main.bot, a child shape the runner resolves within the bundle's collection), an absolute path, a symlink out of the collection — so the profile reported for the bundle does not speak for that childDeclare requires.iterion for the newest profile among those children, or bring the child inside the bundle. iterion validate <bundle> reads a sibling that sits in the same collection; a bot source pushed alone cannot

The engine-contract checks (C250–C251) hold a bundle's declared requires.iterion against the build reading it. The grammar is deliberately total: >= X.Y.Z or a bare version, dotted numeric, optional leading v; every other shape is a manifest parse error, and an unknown key under requires: is refused by the strict manifest decoder. A requirement iterion cannot read is never a requirement iterion ignores.

The skill-authoring checks (C231–C234) guard routability — that a skill can be discovered and chosen by the router — not prose style. They impose no phrasing template and are all warnings, so a skill gap never fails validation.

Quick Troubleshooting

"I get C019 (undeclared cycle)" Every back-edge (edge that creates a cycle) needs as loop_name(N). This is the shape that fails:

iter
schema verdict:
  approved: bool

agent worker:
  model: "anthropic/claude-sonnet-4-6"
  output: verdict

judge evaluator:
  model: "anthropic/claude-sonnet-4-6"
  output: verdict

workflow w:
  entry: worker
  worker -> evaluator
  evaluator -> done when approved
  evaluator -> worker when not approved     # C019: a cycle with no declared loop

And the fix, on the back-edge:

iter
evaluator -> worker when not approved as retry(3) with { feedback: "{{outputs.evaluator.summary}}" }

"I get C009 (session at convergence)" Nodes that receive from multiple branches (via await: or fan-out) cannot use session: inherit or fork. Use session: fresh, session: artifacts_only, or session: persist.

"I get C012 (missing fallback)" If you have when approved, you need either when not approved or an unconditional edge from the same source. Conditions must be exhaustive.

"I get C018 (missing model or backend)" For agents and judges, add model: "..." or backend: "...", set ITERION_DEFAULT_SUPERVISOR_MODEL, or configure detectable backend credentials. For mode: llm routers, either set an explicit model:/backend: or accept the warning and runtime default. For human nodes with interaction: llm or interaction: llm_or_human, add model: or interaction_model: and declare an output: schema.