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

From a .bot to a running graph ​

A .bot file, a .botz bundle or a bundle directory becomes a running graph through one compile path and one engine, whichever door opened it.

Drawn from the code at commit f564846d3. How this page is kept true: README.

The run's own status machine is drawn once, by data-model-runs.md; the cloud launch exchange and the queue delivery that carries a run to a runner pod are drawn by flow-cloud-run-lifecycle.md. This page draws what neither covers: the compile every door funnels through, the engine's setup order and its per-node pipeline, and the finalisation of the run's worktree.

The compile ​

actoractioncode
usernames an artifact and packs every launch flag into one options structrunCmd, cli.RunOptions, cli.RunRun
enginejudges the modes and overrides up front, and again when the real executor is built, so an injected executor cannot skip the checkautomemory.ValidateMode, ambient.Validate, runtime.ValidateLoopBudgetGuardMode, supervise.ValidateSupervisorsMode, model.ParseModelOverrides, git.ValidateBranchName
enginemints the run id, starts telemetry and seeds the engine optionsstore.GenerateRunID, startRunTelemetry, runtime.WithEventObserver
engineforks on the artifact kind; a bundle entry is promoted to its bundle so its skills/ and prompts/ reach the runresolveWorkflow, openBundleOrFile, runview.CompileWorkflowPath
engineloads the unit, refuses a draft by name, merges the bundle's prompts, compiles, takes the identity, prepares the MCP serversrunview.compileUnit, runview.compileLoadedUnit, runview.workflowIdentity
engineexpands use, compiles prompts, schemas, nodes and edges, then validatesir.Compile, compiler.expandGroups, compiler.validate
engineapplies the CLI budget overrides onto the workflow before the executor snapshots Budget, and records the raw ask on the run docapplyBudgetOverrides, runtime.WithBudgetAsk, runview.RunBudgetOverrides
storeopens the project's managed store, tees run.log into it, then wires a hub per DSL-declared supervisorstore.New, store.TeeRunLog, supervise.DeclaredEnabledOrWarn
enginebuilds the executor first, then health-checks the active MCP servers against that executor — the check takes it as an argument — and wires the subbot runnerbuildRunExecutor, runview.MCPHealthCheck, subbotRunnerForCLI, runtime.WithSubbotRunner
engineresolves the run inputs last, after the executor and its health check — the preset overlay, then --var — and refuses an operator-typed skill that resolves to nothingbuildRunInputs, runtime.MergeBundlePresets, runtime.ResolveExtraSkills

The run ​

actoractioncode
enginebuilds the production executor and refuses a launch override that would un-gate a noderunview.BuildExecutor, runview.ValidateModelOverridePermissions, ir.UngatedCrossingReasonForAskRules
storetakes the exclusive per-run lock; a contended lock blocks but stays interruptiblestore.LockRun
engineloads or creates the run document, then admits the run onceEngine.runResolveDoc, Engine.admitRun, refuseUntrustedWithoutSandbox, RunWillBeSandboxed
enginejudges every [enum:] / [matching:] var on run.Inputs — the one check every launch surface funnels into — and the bundle's engine requirementEngine.validateVarConstraints, Engine.refuseBundleRequiringNewerEngine
enginebounds the worktree pool, then adds the worktree at the anchor checkout's tipEnforceWorktreePoolBound, worktreepool.EnforceBudget, setupWorktree
enginepersists the workspace identity, with the ADR-064 delegation gate on adopting a tree it did not createEngine.runPersistWorkspace, gitlib.FindMainRepoRoot
enginestarts the per-run sandbox and emits run_startedEngine.startSandbox, pickMode, runStartedPayload
enginedrives the loop: cancel, pause, steering, then dispatch — isSpecialDispatch, not the dispatcher's own switch, decides which inline node is bracketedEngine.execLoop, execLoopDispatchSpecial, isSpecialDispatch, Engine.execLLMRouter, Engine.drainOverrides
engineschedules a fan-out: plan the edges, cap concurrency, deep-copy the parent outputs, converge at the downstream await: nodeEngine.execFanOut, planFromEdges, validateWorkspaceSafety, resolveConvergence
enginethe standard pipeline and its budget gatesEngine.execLoopRunNode, checkBudgetBeforeExec, graceOrFailBudget, Engine.acquireResources
providerroutes by node kind, resolves the backend name through its chain, dispatches the fallback chainClawExecutor.executeNode, ClawExecutor.executeBackend, resolveBackendName, Registry.Resolve
engineeverything a node owes after it returnsEngine.execLoopAfterExec, correctAndValidateNodeOutput, persistArtifactIfPublished, snapshotAtNodeBoundary
engineselects the next edge and declines a loop that is not earning its crossingEngine.selectEdgeRS, evaluateEdgesWithLoopsRS, Engine.loopStalled, resolveLoopMaxBase
enginewrites the terminal statuspublishRunFinished, failRunDeliberate, failRunErrWithCheckpoint, handleContextDoneWithCheckpoint

The backend name resolves in this order, first non-empty winning: the launch override, the node's backend:, the workflow's default_backend:, ITERION_DEFAULT_BACKEND, detect.Resolve over the host's credentials, then claw. Seven backends are registered: claw in process, claude_code and codex with their own backends, and kimi, grok, pi and opencode over the shared CLIAgentBackend seam (pi adds an RPC variant beside its print one). Only claude_code and claw are in DefaultPreferenceOrder; the other five are explicit opt-ins.

Finalising the worktree ​

statewhat it meanscode
unsetfinalisation has written nothing: a refusal, an unchanged HEAD, or the run is still live. The storage branch iterion/run/<run id> is created on every promoted HEAD, with a numeric suffix on collision — it is the GC guard. A failed wip bank is not one of these refusals: stageRunWork, wipSetAside and gitCommitMessage set PreserveWorktree and fall through, so a run that committed anything itself still gets its branch and, WipBanked having stayed false, still reaches the merge dispatch — the never-merge-a-banked-HEAD rule does not cover itfinalizeWorktree, createBranchSafely, readHEAD, stageRunWork
pendingthe branch exists and the merge is deferred to an operator action — the studio's default, the CLI's opt-outfinalizeOptions.autoMerge, Service.PerformMergeCtx
mergingone worker holds the right to merge; every exit goes through the conditional write, so a claimant that lost its claim cannot overwrite the winnerstore.ClaimMerge, store.UpdateRunMergeIf, RunMergeUpdate.ExpectClaimedAt
conflictedgit merge --squash left UU paths on disk and the operator resolves or abortstrySquashMerge, MergeConflictError, Service.ResolveMergeConflictFile
mergedthe target advanced; MergedCommit equals FinalCommit under merge, differs under squash. The only final status: it is the one value ClaimMerge refusestryFastForward, trySquashMerge, persistMergeSuccess
skipped · failedthe storage branch is the result for now and the run's own status is unaffected either way — but neither is a sink: ClaimMerge is claimable from "", pending, failed, skipped and conflicted, so an operator action re-enters merging from any of themfinalizeWorktree, resolveMergeTarget, store.ClaimMerge, mergeClaimable

An error exit takes none of this: the worktree is preserved for inspection, or released outright when the refusal was early and produced nothing. A review gate that already finalised this run owns it — unless the recorded FinalCommit is stale, in which case finalisation re-runs at the true tip.

Guards that refuse ​

  • UserInputError — a malformed --auto-memory, --ambient-context, --loop-budget-guard, --supervisors, --branch-name or budget flag: nothing durable exists yet.
  • bundle.AuthorDocumentError / the stable code author_document — a draft that documents an author never becomes a program, at every door that names one.
  • ErrInlineImport and C030 — a source whose import lines were never merged in is refused whole, rather than compiled with pieces missing.
  • C176, through ir.UngatedCrossingReasonForAskRules — a route or a launch override that would strip a node's permission: gate, refused at compile time and again before the executor is built.
  • untrusted_without_sandbox — run.Trust not trusted and the resolved sandbox is no container: LAUNCH_FAILED, persisted as run.Admission.
  • context_mismatch — the resolved execution context disagrees with the run record under ContextPolicyEnforce; report and legacy record the violations and allow.
  • Var constraints and requires.iterion — both fire before a worktree or a sandbox is spun up for a doomed run. On resume they part ways: the parent's stored values are never re-judged (only the keys a fork --new-inputs recorded, once, at the child's first resume — judgeForkSuppliedInputs, then the record is consumed), while refuseBundleRequiringNewerEngine runs on every resume, before the claim, leaving the run the resumable status it had.
  • NODE_NOT_FOUND — a checkpoint pointing at a node the edited source no longer declares.
  • cost_cap_daily and budget_cap_run — paused_operator, resumable; BUDGET_EXCEEDED when no pause policy is declared and the exit-path grace is spent.
  • liveness_stall, loop_cap, loop_out_of_fuel, loop_budget_guard — a declined back-edge, never a killed run: the run leaves by its other edges, or dies of LOOP_EXHAUSTED when none matches.
  • NO_OUTGOING_EDGE — the run has nowhere to go; a budget overrun measured after the same node outranks it.
  • Refuse-at-the-root and proveOwnWorktree — finalisation writes nothing when the worktree path is the repository root or the tree hangs off another repository; guardMergeTarget refuses the merge when the target is not the currently-checked-out branch, the branch changed since the run started, or the main working tree is dirty (a failed git status reads as dirty, never as clean).
  • The worktree pool bound is not a refusal: it reclaims oldest-first and returns a warning report.

Collections touched ​

collectionwritten bywhat
runsenginethe document itself and every field above: status, Admission, BudgetOverrides, WorkDir / Worktree / RepoRoot / BaseCommit, the checkpoint, artifact_index, the steering and permission patches, await_answers_waits.<token> (a field on the run, not its own collection), and the finalisation block
eventsenginerun_started, node_started / node_finished, budget_exceeded, budget_exit_grace, run_steered, artifact_written, worktree_branch_failed, run_paused, run_failed, run_finished
run_seqenginethe per-run monotonic sequence AppendEvent allocates from
run_turnsproviderone document per LLM turn a node's model call produces, written by the store event hooks BuildExecutor wires — OnLLMTurnCapture → WriteTurn, keyed on run, node, loop iteration and turn index
run_plansproviderone snapshot per plan-tool write, from the same hooks — capturePlan → AppendPlanSnapshot, secret-redacted, deduplicated against the previous snapshot
interactionsenginea human pause's questions, and the async ask_user_async ones
user_messagesenginethe operator-message inbox the generation engine drains between the agent's tool iterations, through model.StoreInboxBinder over store.DrainPending and store.MarkConsumed
"S3: artifacts/<run>/<node>/<version>.json"engineWriteArtifact PUTs the body, appends the event and $max-es run.artifact_index.<node>; artifacts are not a collection
"S3: tools/<run>/<tool-use-id>/<kind>"providera tool-call body over the inline threshold, redacted then set aside by the same hooks: one object per input and per output, through WriteToolBlob; under the threshold, or when the write fails, it rides the event capped inline
"FS: <store-dir>/runs/<run-id>"enginethe local lane runs the same engine code against the filesystem store: run.json, events.jsonl, run.log, turns/<node>/<iter>/<turn>.json, plans/, the interaction files — no Mongo collection is touched
"FS: <store-dir>/worktrees/<run-id>"enginethe per-run worktree, and the per-node snapshot refs under refs/iterion/runs/ in the parent repository