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

Iterion .bot grammar reference ​

This is the readable inventory of the syntax accepted by the current parser. The machine-oriented counterpart is grammar/iterion_v1.ebnf. Parsing success is only the first stage: the IR compiler then checks declarations, types, references, graph structure, mode-specific properties, loops, resources, and capabilities.

The property tables on this page are generated from the parser's property registry (pkg/dsl/spec, task dsl:gen), which a conformance test holds to the parser in both directions; the complete per-kind reference, blocks included, is dsl-properties.md.

Notation: {x} means zero or more, [x] is optional, and a | b is an alternative. Indentation is significant; examples use two spaces. # comments (## is the same comment) and blank lines are ignored between constructs — ignored by the GRAMMAR, that is: a comment is kept with the declaration it was written around and written back at its place by every rewrite.

File declarations ​

ebnf
file = [ dsl_header ] { top_level_decl } ;

dsl_header = "dsl" ":" INT NEWLINE ;   (* the syntax profile, on the first significant line; absent = 1 *)

top_level_decl = vars | presets | attachments | secrets | mcp_server
               | prompt | schema | cursor | supervisor
               | agent | judge | router | human | tool | compute
               | emit | wait | await_answers | fail | subbot | group | use | workflow
               | contract ;

At most one top-level vars, presets, attachments, and secrets block is retained. Named declarations may repeat only when their names remain unique after compilation.

Lexical values ​

Identifiers match [A-Za-z_][A-Za-z0-9_]*; quote kebab-case skill names and other values containing punctuation. A DSL string can use:

text
key: "escaped string"
key: `raw string: $SHELL and "quotes" stay literal`
key: |
  block scalar
  with preserved newlines

Raw strings have no backtick escape. Under dsl: 2 (the syntax profile, declared on the file's first significant line) a quoted string reads the standard escapes \" \\ \n \t \r \0; under profile 1 (no header) every backslash is kept verbatim unless a # strict-escape: on line (or ## strict-escape: on) sits among the first 31 lines of the file (line 32 counts only when it ends the file), before its first line of code — a profile-1 rule frozen as it is; the directive is refused under profile 2 (E042). One plain bare word is also a string value (backend: claw). Lists are bracketed and comma-separated, or written one - item per line indented under the property; both forms read as the same list in every profile. Depending on the property, elements are identifiers, strings, tool refs (mcp.server.*), or either.

Scalar declaration literals are strings, integers, floats, or booleans. JSON and string[] defaults/preset values therefore use a quoted JSON representation.

Variables, presets, attachments, and secrets ​

Variables and presets ​

ebnf
vars = "vars:" INDENT { IDENT ":" type [ constraints ] [ "=" literal ] } DEDENT ;
constraints = enum [ matching ] | matching [ enum ] ;   (* at most one of each *)
type = "string" | "bool" | "int" | "float" | "json" | "string[]" ;
enum = "[enum:" STRING { "," STRING } "]" ;
matching = "[matching:" STRING "]" ;   (* vars only; a non-empty RE2 pattern *)

presets = "presets:" INDENT
            { IDENT ":" INDENT { IDENT ":" literal } DEDENT }
          DEDENT ;

vars: is valid at top level and inside a workflow; presets: is top-level only. Enum constraints apply only to strings.

Attachments ​

ebnf
attachments = "attachments:" INDENT { attachment } DEDENT ;
attachment = IDENT ":" ( "file" | "image" )
             [ INDENT { attachment_property } DEDENT ] ;
attachment_property = "description:" STRING
                    | "accept_mime:" string_list
                    | "required:" BOOL ;

Attachments are valid at top level and inside a workflow.

Secrets ​

ebnf
secrets = "secrets:" INDENT { secret } DEDENT ;
secret = IDENT ":" [ STRING ] [ INDENT { secret_property } DEDENT ] ;
secret_property = "value:" STRING
                | "as:" ( "value" | "file" )
                | "mount_path:" STRING
                | "env:" string_or_ident
                | "optional:" BOOL
                | "hosts:" string_list
                | "sinks:" string_list
                | "description:" STRING ;

secrets: is top-level only. The short form supplies value; the block form may omit it so the runtime resolves a stored secret by declaration name.

Prompts and schemas ​

ebnf
prompt = "prompt" IDENT ":" [ INDENT { free_text_line } DEDENT ] ;

schema = "schema" IDENT ":" [ INDENT { schema_field } DEDENT ] ;
schema_field = IDENT ":" ( type | "file" ) [ enum ] ;

A prompt, schema, mcp_server, cursor, supervisor, group, contract or workflow header may stand with no body at all — followed by a blank line and another declaration, or by the end of the file — and declares an empty one: the studio saves a declaration the moment it is created, before it has a field or a line (an empty workflow then draws the compiler's own diagnostics, no entry first). The blank line is what tells an empty declaration from a body at the wrong indentation (a group's members are top-level keywords themselves): a header followed directly by an unindented line, or by an indented comment alone, is still the indentation error. Node declarations (agent, tool, …) keep needing a body. A block header inside a declaration or at the top level — vars:, budget:, memory:, mcp:, auth:, cursors:, recovery:, compaction:, resources:, presets:, attachments:, secrets:, sandbox: and its build:/network:, a contract's inputs:/outputs:/criteria:/effects: and a port's file: — may stand bare under the same rule and declares an empty block, kept as the author wrote it rather than dropped: a nested block ends at its parent's dedent or before a blank line and a sibling; a top-level block needs the blank line (or the end of the file), having no dedent. What the empty block means is the compiler's: an empty mcp: wires nothing, a bare sandbox: is the inline block form (C044 until it carries image: or build:). fallbacks: is the exception — a chain with no route is refused by name.

Prompt text may contain runtime {{...}} references and compile-time {{include "relative/file"}} directives. Schema fields accept the six variable types, plus file — an operator-supplied binary valid only on a human node's schema; the compiler rejects it elsewhere (C129).

MCP declarations and activation ​

ebnf
mcp_server = "mcp_server" IDENT ":" [ INDENT { mcp_server_property } DEDENT ] ;
mcp_server_property = "transport:" ( "stdio" | "http" | "sse" )
                    | "command:" STRING
                    | "args:" string_list
                    | "url:" STRING
                    | "auth:" INDENT { auth_property } DEDENT ;
auth_property = "type:" STRING | "auth_url:" STRING | "token_url:" STRING
              | "revoke_url:" STRING | "client_id:" STRING
              | "scopes:" string_list ;

mcp_config = "mcp:" INDENT { mcp_property } DEDENT ;
mcp_property = "autoload_project:" BOOL | "inherit:" BOOL
             | "servers:" ident_list | "disable:" ident_list ;

stdio uses command/args; http and sse use url. Only OAuth2 authorization-code/PKCE auth is wired. mcp: is accepted on workflows, agents, and judges; workflow scope uses autoload_project, while node scope uses inherit.

Cursor and supervisor declarations ​

ebnf
cursor = "cursor" IDENT ":" [ INDENT
           [ "description:" STRING ]
           ( "values:" INDENT { IDENT ":" STRING } DEDENT
           | "bands:" INDENT { STRING ":" STRING } DEDENT )
         DEDENT ] ;

cursors = "cursors:" INDENT
            { IDENT ":" ( IDENT | STRING | NUMBER | BOOL ) }
          DEDENT ;

supervisor = "supervisor" IDENT ":" [ INDENT
               { "watches:" ident_list | "model:" STRING
               | "system:" IDENT | "cooldown:" STRING
               | "max_evals:" INT }
             DEDENT ] ;

cursors: activates declared cursors on an agent/judge; the reserved enabled: key gates the block. A supervisor is concurrent run metadata, not a graph node.

Agents and judges ​

ebnf
agent = "agent" IDENT ":" INDENT { llm_property } DEDENT ;
judge = "judge" IDENT ":" INDENT { llm_property } DEDENT ;

They share the exact property surface (a tool-ref list accepts dotted refs and a trailing .*, and a quoted element is the literal name; reasoning_effort also takes a quoted runtime value):

PropertyValueMeaning
descriptionstringFree-text description shown by the studio and the reports
modelstringModel id the backend serves, e.g. "anthropic/claude-opus-5"; empty takes the backend's default; a {{vars.x}} reference resolves (vars only), then $
backendstringExecution backend: claw, claude_code, codex, pi, kimi, grok or opencode; a {{vars.x}} reference resolves (vars only), then $
providerstringProvider hint for credential resolution, e.g. "anthropic"; a {{vars.x}} reference resolves (vars only), then $
commandstringExecutable that drives a CLI backend, overriding its default binary
inputidentSchema the node's input is validated against
outputidentSchema the node's structured output must match
publishidentArtifact name the output is published under (read back as {{artifacts.<name>}})
artifact_labelstool listLabels stamped on the published artifact; a quoted element is the literal label
deliverablesstring listFiles the node produces that the run collects and serves as downloads: workspace-relative paths, each served under its base name (C317/C318) — the declared twin of the [iterion] attachment= directive
systemprompt name, or its text as a stringThe system prompt: a declared prompt's name, or the text itself as a string (an inline prompt, named after its body)
userprompt name, or its text as a stringThe user message: a declared prompt's name, or the text itself as a string (an inline prompt, named after its body)
sessionone of fresh, inherit, inherit_if_available, fork, artifacts_only, persistHow the node's LLM session relates to the previous node's
session_slotidentNamed durable session slot; requires session: persist; must not be named like another node (C314)
toolstool listTools the node may call; tools: [] declares NO tools, no line at all leaves it undeclared. Restricts claw (C135 on a name it lacks, C159 on a name it holds but cannot serve); on claude_code it becomes --disallowedTools over that CLI's native roster, on codex a sandbox mode; pi/kimi/grok never receive it (C270)
tool_policytool listTool-policy entries applied on top of tools
capabilitiestool listBoard capabilities opened to the node: board.create, board.move, board.read, … (C080/C081). capabilities: [] declares NO capability — it overrides the workflow's list; no line at all inherits it
skillsskill listSkill-library skills mirrored into the run's .claude/skills
tool_max_stepsintUpper bound on tool-call rounds in one execution
max_tokensintOutput-token cap per call
reasoning_effortone of none, low, medium, high, xhigh, max, ultracode, or a quoted ${VAR:-default} stringReasoning effort; none disables reasoning on the models that carry it (GPT-6 Sol/Luna) — elsewhere the behaviour is per-route: claw and claude_code clamp it to the lowest real level (low), pi spells it off, codex's CLI refuses it on a model without it, opencode passes it through; ultracode is xhigh plus multi-agent orchestration, reliable on Opus 4.8 and the Claude 5 family (Opus 5, Fable 5.1) only (C089 warns elsewhere); a quoted string is env-substituted at runtime
timeoutstringDuration the node may run, e.g. "20m"
readonlyboolDeclares the node mutates no workspace file, so it may run beside another branch
full_accessboolGrants the backend its full tool access
imagesstring listImage paths sent with the prompt
interactionone of none, human, llm, llm_or_human, review, async, human_or_hostHow the node asks the operator (ADR-081); human_or_host lets the host application answer in the operator's place, whichever comes first (docs/assistant-dock.md, C212)
interaction_promptidentPrompt the llm interaction mode answers with in the operator's place
interaction_modelstringModel the llm interaction mode uses; a {{vars.x}} reference resolves (vars only), then $
awaitone of wait_all, best_effortConvergence rule when several incoming branches reach the node
compressident — on, ultra, offCommand-output compression: on, ultra or off (C102)
auto_memoryident — on, offThe backend's own auto-memory: on or off (C131/C132)
ambient_contextident — none, workspace, operator, allWhat the node inherits besides its prompt: the repository's instruction files (workspace, the default), the operator's setup (operator), both (all) or nothing (none) — ADR-119 (C184/C185/C187)
permissionident — off, ask, denyTool-permission gate: off, ask or deny (C110–C112)
allowstring listPermission rules always allowed on this node, Tool(pattern) syntax; a non-empty list REPLACES the workflow's allow: (C154 refuses an unreadable rule, C111 warns when nothing gated reads the list)
askstring listPermission rules that pause for approval on this node; a non-empty list REPLACES the workflow's ask: (C154/C111; C136 and C176 screen the node's routes against it)
denystring listPermission rules always blocked on this node; a non-empty list REPLACES the workflow's deny: (C154/C111)
needsident | ident listResource(s) leased from the workflow's resources: block for the node's duration
fallbacksblock → fallbacksOrdered, NAMED alternative routes taken when the primary fails (ADR-087); a chain with no route is refused
mcpblock → mcpMCP servers active for the node
compactionblock → compactionContext-compaction thresholds of the node's session
memoryblock → memoryiterion's shared-memory tools and scopes for the node
sandboxone of none, auto, or a block → sandboxSandbox for this scope: a bare mode (none, auto) or an indented block — the inline form, which needs image: or build: (C044)
cursorsblock → cursorsPrompt-engineering dials activated on the node (docs/cursors.md)

Nested blocks:

ebnf
compaction = "compaction:" INDENT
               { "threshold:" NUMBER | "preserve_recent:" INT }
             DEDENT ;

memory = "memory:" INDENT
           { "enabled:" BOOL | "scope:" STRING | "autoload:" string_list
           | "read:" BOOL | "write:" BOOL | "pre_compact_inject:" BOOL
           | "project_root:" BOOL | "visibility:" STRING }
         DEDENT ;

Routers ​

ebnf
router = "router" IDENT ":" INDENT { router_property } DEDENT ;
router_mode = "fan_out_all" | "fan_out_each" | "condition"
            | "round_robin" | "llm" ;
PropertyValueMeaning
descriptionstringFree-text description shown by the studio and the reports
modeone of fan_out_all, fan_out_each, condition, round_robin, llmRouting mode
modelstringllm mode only (C023 otherwise): Model id the backend serves, e.g. "anthropic/claude-opus-5"; empty takes the backend's default; a {{vars.x}} reference resolves (vars only), then $
backendstringllm mode only (C023 otherwise): Execution backend: claw, claude_code, codex, pi, kimi, grok or opencode; a {{vars.x}} reference resolves (vars only), then $
providerstringProvider hint for credential resolution, e.g. "anthropic"; a {{vars.x}} reference resolves (vars only), then $
systemprompt name, or its text as a stringllm mode only (C023 otherwise): The system prompt: a declared prompt's name, or the text itself as a string (an inline prompt, named after its body)
userprompt name, or its text as a stringllm mode only (C023 otherwise): The user message: a declared prompt's name, or the text itself as a string (an inline prompt, named after its body)
multiboolllm mode only (C023 otherwise): the model may select several outgoing edges
reasoning_effortone of none, low, medium, high, xhigh, max, ultracode, or a quoted ${VAR:-default} stringllm mode only (C023 otherwise): Reasoning effort; none disables reasoning on the models that carry it (GPT-6 Sol/Luna) — elsewhere the behaviour is per-route: claw and claude_code clamp it to the lowest real level (low), pi spells it off, codex's CLI refuses it on a model without it, opencode passes it through; ultracode is xhigh plus multi-agent orchestration, reliable on Opus 4.8 and the Claude 5 family (Opus 5, Fable 5.1) only (C089 warns elsewhere); a quoted string is env-substituted at runtime
overstringfan_out_each: expression naming the collection to iterate
asidentfan_out_each: alias each item is bound to ({{each.<as>}})
keyidentfan_out_each: item field that names each branch
depends_onidentfan_out_each: item field naming the branch this one waits for (requires key)
needsident | ident listResource(s) leased from the workflow's resources: block for the node's duration

description, mode and needs apply to every router; the model properties to llm; over, as, key and depends_on to fan_out_each, which requires over and exactly one unconditional outgoing template edge. depends_on requires key. Routers never accept await.

Human nodes ​

ebnf
human = "human" IDENT ":" INDENT { human_property } DEDENT ;

Accepted properties are description: STRING, input/output/publish: IDENT, artifact_labels: tool_ref_list, instructions/system/interaction_prompt: IDENT, interaction: interaction_mode, interaction_model/model/review_url: STRING, min_answers/max_turns: INT, await: await_mode, and the string-or-identifier review fields posture, merge_strategy, and merge_into.

Tool and compute nodes ​

Tool ​

ebnf
tool = "tool" IDENT ":" INDENT { tool_property } DEDENT ;
PropertyValueMeaning
descriptionstringFree-text description shown by the studio and the reports
commandstringShell command, run through bash -c (exclusive with script)
scriptstringInline script run by the interpreter language: names
languageident — js, node, py, python, python3, sh, bashInterpreter for script:
env_expansionident — braced, noneWhat the engine expands in this node's bodies before they run: the braced environment forms (braced, the default) or nothing (none), which hands ${NAME} and ${NAME:-default} to the body's own reader as written — ADR-130; another word, or the property on an action: node that has no body, is refused (C004)
inputidentSchema the node's input is validated against
outputidentSchema the node's structured output must match
publishidentArtifact name the output is published under (read back as {{artifacts.<name>}})
artifact_labelstool listLabels stamped on the published artifact; a quoted element is the literal label
deliverablesstring listFiles the node produces that the run collects and serves as downloads: workspace-relative paths, each served under its base name (C317/C318) — the declared twin of the [iterion] attachment= directive
awaitone of wait_all, best_effortConvergence rule when several incoming branches reach the node
sandboxone of none, auto, or a block → sandboxSandbox for this scope: a bare mode (none, auto) or an indented block — the inline form, which needs image: or build: (C044)
compressident — on, ultra, offCommand-output compression: on, ultra or off (C102)
permissionidentParsed for symmetry but NOT enforced on a tool node (C112 warns): the command runs directly, the gate is an agent's
needsident | ident listResource(s) leased from the workflow's resources: block for the node's duration
parallel_safeboolDeclares the node safe to run beside a mutating branch
goalstringVerified action: what the command is for, in one line
postconditionstringVerified action: command whose exit code is the truth oracle at every rung
policyident — required, recover, best_effortVerified action: required (default), recover or best_effort (C103–C106)
recoveryblock → recoveryVerified action: the self-heal ladder's bounds
actionstring|identConnector operation to call, connector.resource.verb, bare or quoted — exclusive with command:/script: (ADR-098, C260)
connectionstring|identThe connection binding that authenticates the action, bare or quoted (an alias may carry a dash) (C261)
paramsblock → paramsThe action's arguments, by the operation's own parameter keys
retrystring|numberAction: how many EXTRA attempts, e.g. 3; a duration is refused and empty means none (C265). Inert without action: (C266)
timeoutstring|numberAction: bound on one call, e.g. 30s (C265). Inert without action: (C266)

command and script are mutually exclusive. Recovery accepts max_repair_attempts: INT, max_agent_attempts: INT, model: STRING, and agent_tools: tool_ref_list.

Compute ​

ebnf
compute = "compute" IDENT ":" INDENT { compute_property } DEDENT ;
compute_property = "description:" STRING | "input:" IDENT | "output:" IDENT
                 | "publish:" IDENT | "artifact_labels:" tool_ref_list
                 | "await:" await_mode
                 | "expr:" INDENT { IDENT ":" STRING } DEDENT ;

Each expr string is parsed by the bounded expression language described below.

Event and nested-run nodes ​

ebnf
emit = "emit" IDENT ":" INDENT
         { "description:" STRING | "event:" STRING | with_block }
       DEDENT ;

wait = "wait" IDENT ":" INDENT
         { "description:" STRING | "event:" STRING
         | "timeout:" STRING | "output:" IDENT }
       DEDENT ;

subbot = "subbot" IDENT ":" INDENT
           { "description:" STRING | "source:" STRING | with_block
           | "output:" IDENT | "needs:" needs_value | "isolated:" BOOL }
         DEDENT ;

wait requires a timeout. A subbot launches the child source as a real nested run.

Groups and uses ​

ebnf
group = "group" IDENT [ "(" [ IDENT { "," IDENT } ] ")" ] ":"
        [ INDENT { agent | judge | router | human | tool | compute | edge } DEDENT ] ;

use = "use" IDENT "as" IDENT [ with_block ] ;

Groups are compile-time macros. use bindings substitute {{params.name}}; expanded nodes are addressed as <prefix>.<node>.

Workflows ​

ebnf
workflow = "workflow" IDENT ":" INDENT { workflow_member } DEDENT ;

Workflow members — the properties below and the edges (src -> dst …) — may appear in any order:

PropertyValueMeaning
entrydotted identNode the run starts at; a dotted name addresses a group instance's node
contractidentThe bot's public contract (a top-level contract declaration), bound to the program (C300–C304)
varsblock → varsWorkflow-scoped vars (merged with the file's)
attachmentsblock → attachmentsWorkflow-scoped attachments
budgetblock → budgetRun caps, each overridable by the matching run flag
resourcesblock → resourcesNamed semaphores and pools nodes lease with needs:
mcpblock → mcpMCP servers active for the run
compactionblock → compactionDefault compaction thresholds
sandboxone of none, auto, or a block → sandboxSandbox for this scope: a bare mode (none, auto) or an indented block — the inline form, which needs image: or build: (C044)
worktreeident — auto, noneauto runs the workflow in a fresh git worktree, finalised into a branch; none runs in place
default_backendstringBackend for nodes that name none; a {{vars.x}} reference resolves (vars only), then $
compressident — on, ultra, offCommand-output compression: on, ultra or off (C102)
auto_memoryident — on, offThe backend's own auto-memory: on or off (C131/C132)
ambient_contextident — none, workspace, operator, allWhat the node inherits besides its prompt: the repository's instruction files (workspace, the default), the operator's setup (operator), both (all) or nothing (none) — ADR-119 (C184/C185/C187)
loop_budget_guardident — on, offDecline a loop's back-edge the budget cannot fund: on (default) or off (C133)
repo_devboxident — on, offLoad the target repo's devbox.json toolchain: on (default) or off (C134)
workspace_checkpointident — on, offMid-run preservation of a copy-based sandbox's workspace as a checkpoint branch pushed to the run's own remote: on (default) or off (C139)
permissionident — off, ask, denyTool-permission gate: off, ask or deny (C110–C112)
allowstring listPermission rules always allowed, Tool(pattern) syntax
askstring listPermission rules that pause for approval
denystring listPermission rules always blocked
tool_policytool listRun-wide tool-policy entries
capabilitiestool listRun-wide board capabilities; a node's own capabilities: replaces them — capabilities: [] on the node holds none
skillsskill listRun-wide skill-library skills
interactionone of none, human, llm, llm_or_human, review, async, human_or_hostDefault interaction mode for the run's nodes that set none

Budget fields are max_parallel_branches: INT, max_duration: STRING, max_cost_usd: NUMBER, max_tokens: INT, warn_tokens: INT (advisory-only — crossing it emits a budget_warning), and max_iterations: INT.

Resources are either counting semaphores or named-member pools:

iter
resources:
  browser: 2
  worktree: ["slot-a", "slot-b"]

Nodes acquire them with needs: browser or needs: [browser, worktree].

Public contract ​

ebnf
contract = "contract" IDENT ":" NEWLINE [ INDENT { contract_prop } DEDENT ] ;
contract_prop = "display_name" ":" str_value NEWLINE
              | "responsibility" ":" str_value NEWLINE
              | "version" ":" INT_LIT NEWLINE
              | ( "inputs" | "outputs" ) ":" NEWLINE [ INDENT { contract_port } DEDENT ]
              | "criteria" ":" NEWLINE [ INDENT { contract_criterion } DEDENT ]
              | "effects" ":" NEWLINE [ INDENT { contract_effect } DEDENT ] ;
contract_port = IDENT ":" port_type NEWLINE [ INDENT { contract_port_prop } DEDENT ] ;
port_type = ( IDENT | "string[]" ) { "[" "]" } ;
json_value = "null" | bool_value | INT_LIT | FLOAT_LIT | STRING_LIT
           | "[" [ json_value { "," json_value } ] "]"
           | "{" [ json_member { "," json_member } ] "}" ;

A contract is the bot's public face — inputs, outputs, delivered files, deterministic criteria, visible effects — declared once at top level, named by the workflow's contract:, and checked against the program: an input is a declared var, required exactly when the var has no default (C300), an output names the node and field that produce it — from: build.pr_url, or from: build for a file (C301) — and a criterion names a port in the singular (input.goal, output.pr_url) and a registered kind (C302; an unregistered kind is declared, not evaluated, C303). default: and params: take one JSON value on one line — "text", 12, true, null, [...] or {key: value} — with no bare word (unlike every string property), no signed number and no exponent, which the .bot text cannot write. The full productions are in iterion_v1.ebnf; the decision and its bounds in ADR-099. A bare contract header, or a bare inputs: / outputs: / criteria: / effects: header and a port's bare file:, declares an empty one under the rule of every empty declaration and block: a blank line, the parent's dedent or the end of the file must follow the bare header. A comment survives a studio save where it was written — inside a contract as inside any declaration — carried by the declaration it was written around.

iter
vars:
  goal: string

schema report:
  summary: string
  pr_url: string

prompt build_user:
  Implement {{vars.goal}} and report the pull request URL.

agent build:
  model: "anthropic/claude-sonnet-4-6"
  user: build_user
  output: report

contract feature:
  display_name: "Feature dev"
  responsibility: "Implements a feature and opens a pull request"
  version: 1
  inputs:
    goal: string
      description: "What to build"
  outputs:
    pr_url: string
      from: build.pr_url
  criteria:
    goal_is_not_empty:
      kind: min_length
      port: input.goal
      params: {min: 1}
  effects:
    opens_pr:
      description: "Opens a pull request on the repository"

workflow feature_dev:
  contract: feature
  entry: build
  build -> done
PropertyValueMeaning
display_namestringExplicit human-readable name
responsibilitystringThe single responsibility this bot fulfils
versionintPublic contract version, 1 or more (C300); defaults to 1
inputsblock → contract.portsNamed typed values and files the bot takes; each one is a declared var (C300)
outputsblock → contract.portsNamed typed values and files the bot produces on success; each one names the node and field that produce it (C301)
criteriablock → contract.criteriaDeterministic registered checks on a port; prose is not executable
effectsblock → contract.effectsVisible effects, including paid operations

A port (an inputs: / outputs: entry, name: type):

PropertyValueMeaning
descriptionstringMeaning of the value
requiredboolMandatory port (default true). An input mirrors its var — required exactly when the var has no default — and a written value that disagrees is refused (C300)
nullableboolPermit an explicit null value (default false); on an input whose var has no default, the one way to be optional — omitted, the var is null (C300)
defaultjson valueTyped default of an optional input: the var's default, read as the launch reads a value of its type (a string[] or json var's text as a list or an object) — written, it must be the var's (C300); omitted, the var's is the port's. One JSON value on one line — "text", 12, true, null, [...], {key: value} — with no signed number and no exponent, which the text cannot write and the compiler refuses from a document (C302)
min_itemsintMinimum array cardinality (C300 on an input, C301 on an output)
max_itemsintMaximum array cardinality (C300 on an input, C301 on an output)
fromdotted identProducer of an output: node.field for a value, node for a file (C301); refused on an input (C300)
fileblock → contract.fileProperties of a delivered or consumed file; existence and provenance are the runtime's checks

A port's file: block:

PropertyValueMeaning
media_typestringExpected media type
min_bytesintMinimum file size
schemaidentSchema of structured file contents

A criterion (a criteria: entry):

PropertyValueMeaning
kinddotted identRegistered deterministic validator (see the criteria table; a plugin's may be dotted); an unregistered kind is declared but not evaluated (C303)
portdotted identChecked port, singular: input.<name> or output.<name> (C302)
paramsjson valueParameters validated against the criterion's parameter declaration (C302): one JSON object on one line, e.g. {min: 2}

An effect (an effects: entry):

PropertyValueMeaning
descriptionstringExternally visible operation
paidboolThe operation may incur a charge; unknown cost remains unknown

Sandbox block ​

Short form:

iter
sandbox: auto   # or none / inline

Block form accepts:

ebnf
sandbox = "sandbox:" INDENT
            { "mode:" ( "auto" | "none" | "inline" )
            | "image:" STRING | build | "user:" STRING
            | "workspace_folder:" STRING | "host_state:" IDENT
            | "post_create:" STRING | "env:" string_map
            | "mounts:" string_or_ident_list | network }
          DEDENT ;

build = "build:" INDENT
          { "dockerfile:" STRING | "context:" STRING | "args:" string_map }
        DEDENT ;

network = "network:" INDENT
            { "mode:" IDENT | "preset:" string_or_ident
            | "inherit:" IDENT | "rules:" string_or_ident_list }
          DEDENT ;

Block form without mode implies inline. image and build are mutually exclusive. Sandbox blocks are accepted on workflows, agents, judges, and tools.

Edges ​

ebnf
edge = node_ref "->" node_ref { "->" node_ref } { when_or_else | iteration | with_block } ;
node_ref = IDENT { "." IDENT } | "done" | "fail" ;

when_or_else = "when" ( [ "not" ] IDENT | STRING ) | "else" ;
iteration = "as" IDENT "(" ( INT | STRING | "unbounded" [ INT ] ) ")" [ "guard" ":" ( "on" | "off" ) ] [ "sized_by" STRING ]
          | "as foreach" IDENT "(" IDENT "in" STRING ")" ;
with_block = "with" "{" { IDENT ":" ( STRING | INT | FLOAT | BOOL ) [ "," ] } "}" ;

A chain a -> b -> c is one edge per arrow, in order; the clauses belong to the last edge (a clause before a further arrow is E032). A non-string with literal is read as its text (n: 3 maps "3"). Clauses may occur in any order, but each kind may occur at most once. when and else are mutually exclusive. A quoted when is parsed as an expression. Every graph cycle must be declared by a named loop or finite foreach; unbounded loops require a fuel source and an exit edge. A loop's guard and sized_by suffixes accept either order, each at most once — sized_by names the work signal the loop budget guard prorates with (dsl.md).

Expression language ​

compute.expr values and quoted when clauses use this precedence grammar:

ebnf
expr = or ;
or = and { ( "||" | "or" ) and } ;
and = not { ( "&&" | "and" ) not } ;
not = ( "!" | "not" ) not | comparison ;
comparison = add [ ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add ] ;
add = multiply { ( "+" | "-" ) multiply } ;
multiply = unary { ( "*" | "/" | "%" ) unary } ;
unary = "-" unary | postfix ;
postfix = primary { "[" expr "]" } ;
primary = number | string | bool | list | object | path | call | lambda_call | "(" expr ")" ;
list = "[" [ expr { "," expr } ] "]" ;
object = "{" [ pair { "," pair } ] "}" ;
pair = ( IDENT | string ) ":" expr ;
path = IDENT { "." IDENT } ;
call = IDENT "(" [ expr { "," expr } ] ")" ;
lambda_call = ( "map" | "filter" ) "(" expr "," lambda ")"
            | "reduce" "(" expr "," expr "," lambda ")" ;
lambda = ( IDENT | "(" IDENT { "," IDENT } ")" ) "=>" expr ;

Standard namespaces are vars, input, outputs, artifacts, loop, and run. Built-ins are length, concat, unique, contains, join, tail, if, sort, keys, values, slice, sum, min, max, flatten, floor, round, map, filter, and reduce. min/max accept either one array or two or more values (arguments are flattened one level). A list literal evaluates to an array (an all-string one conforms to string[]), an object to a JSON object; a repeated object key is refused at parse, and neither form accepts a trailing comma. Lambdas are confined to finite combinators, expression depth is capped, and one evaluation may visit at most 100,000 elements.

Template references ​

Normal runtime references have at least namespace.path and may continue through dotted fields:

ebnf
template = "{{" [ "!" ] namespace "." IDENT { "." IDENT } "}}" ;
namespace = "vars" | "input" | "outputs" | "artifacts" | "attachments"
          | "secrets" | "loop" | "each" | "run" ;

{{!input.command}} requests raw substitution only while rendering a tool shell command; it disables shell escaping and must be restricted to trusted input. Group expansion separately consumes {{params.name}} before normal reference parsing.

Namespace-specific shapes and constraints are detailed in the DSL guide. Unknown or malformed references are diagnostics, not silently empty templates.

Semantic validation ​

Syntax-valid files can still fail compilation for duplicate ids, unknown schemas/prompts/nodes, invalid templates, unreachable nodes, non-exhaustive routing, undeclared cycles, router-mode property misuse, unsafe fan-out, bad resource references, capability mismatches, and invalid sandbox/secret/cursor configuration.

Use iterion validate file.bot. The authoritative sparse code ranges are DSL C001–C199 (plus the async-interaction band C240–C242) and bundle checks C200–C234; see diagnostics.