Skip to content

How a run flows

The real record hierarchy, twelve stage kinds, four stage modes, budgets and failure policy.

14 min readUpdated 13 Sept 2026Reviewed 12 Sept 2026Published 12 Sept 2026/docs/how-a-run-flows

A run is one brief walked through a pipeline. The engine records exactly four nested things — Run → StageRun → TurnRecord → ToolCallRecord — plus an Artifact per stage. Anything else you have read that describes five or six levels of nesting was describing computations as if they were containers.

The record hierarchy

LevelWhat one isThe fields that matter
RunOne brief, from submission to outcome.status, stages[], budget { limitUsd, spentUsd }, objective, tags[], outcome, error, workspaceId and the resolved workspacePath.
StageRunA live instantiation of one StageSpec.spec (kind, name, roleIds, mode, rounds, maxIterations, optional, whenTags), status, turnIds[], artifactIds[], summary, participantRoleIds[], error.
TurnRecordOne employee, one model call, plus a loop of tool calls.employeeId, roleId, purpose, route (the full routing decision), servedBy and attemptedRoutes when a fallback answered, usage, text, reasoning, toolCalls[], skills[], wroteFiles[], error.
ToolCallRecordOne tool invocation inside a turn.name, the raw argumentsJson, status (ok | error | denied | running), a truncated resultPreview, durationMs and affectsPaths[].
ArtifactWhat a stage produces, so the next stage has something self-contained to read.kind, title, a markdown body, an optional path and the stageId it belongs to.

Two things that are not levels, and this is the correction that matters most: complexity and the prompt. estimateComplexity is a deterministic heuristic computed inside a single turn, and buildTurnMessages assembles that turn's prompt from the brief, the objective, upstream stage summaries, the transcript so far, the participant list and the selected skills. Complexity is not a container that holds a prompt; both are work the engine does before one model call.

Complexity is worth understanding anyway, because the router escalates on it. It starts from a per-stage baseline — intake 0.15, report 0.2, plan 0.4, architect 0.6, for instance — then adds a saturating length term (min(0.15, log2(1 + chars/400) × 0.045), so a huge brief is not proportionally harder), up to 0.22 for named hard signals such as concurren, deadlock, migrat, rollback or cache invalidation, up to 0.12 for revision passes, 0.08 when the turn actually touches files, a little for the number of files already written, and a small shift for seniority. Trivial signals such as typo or lint pull it down, but only when no hard signal is present. The result is clamped to 0..1 and rounded to two decimals, which keeps the routing reason string stable and readable.

Inside a turn

  1. Task class. Derived from the stage kind, and used to price the turn. build is coding, architect is architecture, report is summarize, integrate is ops, and so on.
  2. Skills. Candidates are the intersection of the role's skill ids and the workspace's enabled skills, falling back to every workspace skill if that intersection is empty. The first two candidates are always included as role-default and do not count against the limit; the rest compete on a task-class match (worth 100 plus keyword overlap) or plain keyword overlap against name, description and tags. Sorted by score then id, and the engine takes three.
  3. Tools. The role's granted tools, filtered again by whether the registry actually has them — so a grant naming a tool that no plugin registered simply disappears rather than failing later.
  4. Complexity, then routing. The route is decided per turn, not per role, and the decision is attached to the turn as TurnRecord.route, emitted as a routing.decision event, and summarised to the employee as lastRoute. If the chosen provider fails, the next candidate serves the turn and the difference is recorded in servedBy with the failures in attemptedRoutes.
  5. The tool loop. The model answers; if it asked for tools, every call runs and every result is appended to the conversation before the next model call, so one request can satisfy several calls. The loop is capped at 8 round trips and tool results are truncated at 8,000 characters.
  6. Settlement. Usage is added to the run's spend immediately and a budget.updated event goes out — a turn that costs money must move the budget at once, or the next turn would route as though the run were still free. The employee returns to idle (or error), and a turn.finished event closes the turn.

The twelve stage kinds

KindWhat it is forTask classArtifact
intakeTurn the raw brief into a structured objective, and emit the TAGS: line that decides which optional stages run.intakeobjective
planDecompose the objective into workstreams and assign owners by role id.planningplan
researchGather external evidence with sources and explicit confidence levels.researchresearch
debateStructured argument between specialists, ending in a ruling.debatetranscript
workshopConverge a discussion into a decision record: decision, alternatives rejected and why, interfaces, out of scope.workshopdecision
designProduce the design: flows and states, including empty, loading and error.designdesign
architectProduce the technical plan: exact file paths, exported signatures, the command that verifies it.architecturespec
buildImplement in the workspace, with real files written through the tools.codingcode
reviewCritique what was actually produced, with file paths and line-level reasoning.reviewreview
testVerify, and separate what was verified from what was not.testingtest-report
integrateReconcile parallel workstreams.opsnote
reportSummarise back to the user. This stage's summary becomes the run's outcome.summarizereport
The Work page: approvals waiting on a human, a list of runs with their cost and current stage, and the open transcript of one run showing its objective, constraints and turn-by-turn record.
The transcript of a run in progress. Everything on this page comes from the four record levels above — the stages, the turns inside them, the tool calls inside those, and the artifacts each stage emitted.

The four stage modes

A stage's mode decides how its people are scheduled. All four are dispatched from one switch, and executeStage never throws — a stage that fails leaves its turns recorded and lets the run engine decide what that means. Whichever mode runs, the stage then emits exactly one artifact of its kind (an objective, a plan, a transcript, a spec, a test report) plus one artifact per file the stage actually wrote, each carrying the path and titled with it. The stage's summary — the clipped text the next stage will read — becomes that first artifact's body, which is why a stage must produce something self-contained rather than a pointer to its own turns.

Slot roles[0] is not a formality in any mode: in single it is the only speaker, in parallel it is the first branch merged, in debate it facilitates and rules, and in review-loop it chairs. Listing a role first in a stage spec is a decision about who holds the gavel, not about who is most senior.

single

Exactly one turn, by roles[0]. The stage summary is that turn's text clipped to 6,000 characters. The source comment describes this mode as "one employee, one turn (or a short internal loop)"; the internal loop does not exist. Use parallel if you want more than one voice.

parallel

Every listed role works independently and at the same time, up to maxConcurrency (default 4, clamped to 1–16). Each branch gets its own structuredClone of the run's accumulated knowledge, so two builders cannot interleave writes into one shared list. Results are merged afterwards in role order, not in completion order, which is what keeps the stage summary stable between runs. The summary is each turn's text under an employeeId — purpose heading, clipped to 6,000 characters.

debate

rounds = max(1, spec.rounds ?? 2). In each round every role speaks once, in list order, and each speaker sees the transcript so far — round one is "open your position", later rounds are "rebut what you have heard". Each speech is emitted as a speech event of kind debate with the other participants as recipients. After the last round, roles[0] acts as facilitator and rules: it emits one final speech of kind report, and that verdict becomes the stage summary the next stage receives. This mode is used by both debate and workshop stages; the only difference is that a workshop's facilitator is told to converge into a decision record rather than to rule on a disagreement.

review-loop

maxIterations = max(1, spec.maxIterations ?? 2). This is the mode where a literal reading of the type would lead you astray, and the source documents the deviation deliberately: roles[0] is the review chair, not the producer. In product-build the review stage is ['backend-lead', 'frontend-lead', 'qa-lead'] — three reviewers, none of whom built anything, so treating the first as the producer would have it revise its own review.

Each iteration runs the reviewers in parallel at maxConcurrency, then the chair reads all of it and synthesises a verdict. The loop stops when the verdict either approves or raises no objection at all. Three regular expressions decide that, and they are deliberately conservative:

  • An objection is matched by objection, must fix, blocking, reject, not acceptable, does not work, or the ❌/⚠ marks.
  • An approval is matched by approv, sign-off, ship it, looks good, lgtm or good enough — and then immediately vetoed if any objection phrase is also present.
  • Phrases that deny an objection — "no objections", "zero blockers", "without concerns" — are stripped first, because a heuristic that cannot tell "no objections" from an objection would send every clean review back for another round.

When an objection survives, the work goes back to the people who actually wrote files during this run, excluding anyone already in the stage. That list is derived from the run's own record rather than declared, because it is the only reliable source of truth for "who built this". If nobody wrote anything, or the iteration cap has been reached, the loop ends with the last verdict as the summary.

The three shipped pipelines

PipelineStagesShape
product-build10intake → plan → research (optional, single) → debate (4 roles, 2 rounds) → workshop (3 roles, 1 round) → architect → build (4 developers, parallel) → review (3 reviewers, review-loop, 2 iterations) → test → report.
code-change7intake → plan → architect → build (2 developers, parallel) → review (2 reviewers, review-loop, 2 iterations) → test → report.
quick-answer3intake → research → report. No build stages at all.

Two details are easy to miss and matter in practice. product-build's research stage is the only shipped stage marked optional: true — if it fails, the failure is recorded and the run continues. And research is not tag-gated in the shipped pipelines: whenTags exists on every StageSpec and the engine honours it, but no shipped stage uses it. The optional flag is what lets the plan say "skip research" without the engine failing on it.

Which pipeline you get is decided from the brief by a small deterministic rule, not by a model: a question-shaped brief under 240 characters that ends in ? or begins with what, why, how, is, should, explain and similar goes to quick-answer; a brief under 600 characters containing a change verb such as fix, bug, refactor, rename, upgrade or remove goes to code-change; everything else goes to product-build. A submission can name a pipeline explicitly.

Budgets

There are two gates, and neither behaves the way the old documentation claimed.

The hard ceiling — checked before each stage, and it fails the whole run

Before a stage starts, the engine compares spentUsd >= limitUsd. If the ceiling is reached, the run is marked failed immediately, with the error text:

Run budget of $5.00 was exhausted before stage "Build".

An error event goes out and the run settles. This is a real halt rather than a warning, but note where the check sits: between stages. A second mechanism exists for the middle of a stage — the stage gets an abortReason() callback returning The operator cancelled this run. or Run budget of $5.00 has been exhausted. — but only debate and review-loop ever consult it, and only between turns. single and parallel never ask, so a stage can and will overshoot the ceiling. That is the honest answer to "what if one stage is expensive": it finishes, and the run stops before the next one.

The soft gate — once per run, between stages, and a refusal cancels

The soft threshold (DEV3D_SOFT_SPEND_APPROVAL_USD, default 1.50; 0 disables it) is checked in the same place, before a stage. Once the run's spend has crossed it, the engine asks a human exactly once per run:

Spend $1.62 of the $5.00 budget and continue?
The run has spent $1.62 so far. Remaining: $3.38. The next stage is "Build" (build).

The approval has kind spend, turnId: null, and the employee credit goes to the owner of the first stage. If it is refused, the run is cancelled — not paused — with the error The operator declined to continue spending on this run. A set of run ids that have already cleared the gate guarantees it cannot fire twice, so a long run asks one question rather than one per stage. A timeout counts as a refusal; the default wait is 600,000 ms, floored at 1,000.

One vocabulary detail that catches people reading the types: RunStatus declares queued, running, awaiting-approval, paused, done, failed and cancelled, but no code path ever sets paused. The soft gate cancels, and the only thing that produces awaiting-approval is a tool waiting on a human.

The variable you would expect to be the budget is not the budget. DEV3D_RUN_BUDGET_USD (default 5) is read into config.runBudgetUsd and then never referenced anywhere else in the source, even though .env.example calls it a hard ceiling the engine enforces. A run's real limit is input.budgetUsd ?? project.budget.defaultRunUsd, where defaultRunUsd itself defaults to 5. Set the limit per project, or pass budgetUsd on submission.

When something fails

There is no stage-level "degraded" status. StageStatus is exactly pending, running, awaiting-approval, done, failed and skipped. (The word degraded does appear in the source, but it means something else entirely: the provenance of a provider's model list, discovered | degraded | seed.) Real degradation happens through five mechanisms instead.

  • A stage failure is two distinct facts. failed means the stage carries an error; empty means it produced no turns. Both are recorded with the error The stage produced no turns. when there is nothing more specific. Only a non-optional stage halts the run, with Stage "X" failed; the run stopped there. An optional stage's failure is recorded and the run continues. Exceeding the budget always halts.
  • Skipping is normal. A stage whose whenTags do not match the tags parsed at intake becomes skipped, with no start time. When the operator cancels, the running stage and every pending stage become skipped, and the run is cancelled — distinct from failed.
  • Router capability relaxation. When capability filtering leaves nothing, the router widens the search rather than failing the turn, and says so in the route reason in one of three literal forms: no model met the capability requirements; dropped tool/vision requirements, no model met capability or context requirements; dropped both, or no model satisfied the request; fell back to the full catalog. The turn runs on something less well matched than requested, and the reason is on the record.
  • Turn-level partial failure. Two cases are recorded on the turn rather than raised as errors. If the model stops because it hit its output limit, the turn's error is The model hit its output limit mid-turn; the reported work product is incomplete. — and, importantly, the turn keeps status: 'done' and emits no error event, so a truncated answer is not flagged the way a crash is. If the tool loop exhausts its 8 round trips, the error is Stopped after 8 tool round trips without a final answer. Only genuinely failed turns emit <employee> failed: <error>.
  • Tool-level status. Every tool call carries ok, error, denied or running, with real messages: refused: echo is not granted to ceo when a role lacks a grant, unknown tool X when the model invents one (the turn continues with an instruction not to use it), and The tool "X" crashed: … when a tool throws. A tool is supposed to return a failure rather than throw, and the turn survives either way.

Three smaller behaviours are worth knowing because they look like bugs otherwise:

  • A stage with no resolvable participants logs at level error, scope engine/stages: Stage "X" has no resolvable participants. A stage that names one unknown role among several logs a warn — Stage references role "X", which is not in the org chart. Skipping it. — and runs with the rest.
  • If persistence is unavailable, the store logs running without persistence (…); history will be lost on exit and everything else keeps working.
  • If the report stage never ran or produced nothing, the run's outcome falls back to the last stage's summary rather than being left null.

One piece of the failure story is simply unfinished, and it is fair to say so: the retry prompt that renders **Correction required — your previous attempt failed:** … is declared and rendered, but no caller ever passes the note, so the turn-retry path is unreachable. It is listed in Known gaps.

What to check when a run stops early

SymptomWhere to lookWhat it usually is
Run status failed with "budget … exhausted before stage"The run.error and the budget counter.The hard ceiling working as designed. Raise the project's defaultRunUsd, or pass budgetUsd on submission.
Run status cancelled with "declined to continue spending"The approvals list.The soft gate fired and was refused, or timed out. The run does not resume; resubmit if you want it to continue.
Run status cancelled with "Cancelled by the operator."Whoever sent the cancel command.Cancellation is a WebSocket command, not an HTTP route. Stages that had not run are skipped.
A stage is failed but the run is still doneThe stage's optional flag.An optional stage failed and was recorded; the run continued past it. Only product-build's research stage ships optional.
A stage is skipped with no start timeThe run's tags[] and the stage's whenTags.Tag gating, or a cancellation. If the intake stage emitted no TAGS: line, the run falls back to a single feature tag.
A turn is done but its text stops mid-sentenceThe turn's error field, not the run's.The output limit was hit. The turn keeps done and emits no error event, so the work product is flagged only on the record.
A turn is failed and the employee shows an errorroute, servedBy and attemptedRoutes on the turn.Every candidate provider failed. attemptedRoutes lists what was tried, in order, as providerId/modelId: error.
An error log says a stage "has no resolvable participants"The stage's roleIds against the org chart.Roles were fired, or a plugin-contributed pipeline names roles this floor does not have.
Persistence warnings in the logstore at /api/health.The database could not be opened; the run is fine and its history will not survive the process.

Related reading: Model routing for how each turn picked its model, the wire protocol for the 27 events and 31 commands the console sees, the office for what the floor shows while this is happening, and Architecture for how the pieces fit together.

Linked from

Did this page answer your question?