How a run flows
The real record hierarchy, twelve stage kinds, four stage modes, budgets and failure policy.
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
| Level | What one is | The fields that matter |
|---|---|---|
Run | One brief, from submission to outcome. | status, stages[], budget { limitUsd, spentUsd }, objective, tags[], outcome, error, workspaceId and the resolved workspacePath. |
StageRun | A live instantiation of one StageSpec. | spec (kind, name, roleIds, mode, rounds, maxIterations, optional, whenTags), status, turnIds[], artifactIds[], summary, participantRoleIds[], error. |
TurnRecord | One 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. |
ToolCallRecord | One tool invocation inside a turn. | name, the raw argumentsJson, status (ok | error | denied | running), a truncated resultPreview, durationMs and affectsPaths[]. |
Artifact | What 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
- Task class. Derived from the stage kind, and used to price the turn.
buildiscoding,architectisarchitecture,reportissummarize,integrateisops, and so on. - 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-defaultand 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. - 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.
- 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 arouting.decisionevent, and summarised to the employee aslastRoute. If the chosen provider fails, the next candidate serves the turn and the difference is recorded inservedBywith the failures inattemptedRoutes. - 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.
- Settlement. Usage is added to the run's spend immediately and a
budget.updatedevent 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 toidle(orerror), and aturn.finishedevent closes the turn.
The twelve stage kinds
| Kind | What it is for | Task class | Artifact |
|---|---|---|---|
intake | Turn the raw brief into a structured objective, and emit the TAGS: line that decides which optional stages run. | intake | objective |
plan | Decompose the objective into workstreams and assign owners by role id. | planning | plan |
research | Gather external evidence with sources and explicit confidence levels. | research | research |
debate | Structured argument between specialists, ending in a ruling. | debate | transcript |
workshop | Converge a discussion into a decision record: decision, alternatives rejected and why, interfaces, out of scope. | workshop | decision |
design | Produce the design: flows and states, including empty, loading and error. | design | design |
architect | Produce the technical plan: exact file paths, exported signatures, the command that verifies it. | architecture | spec |
build | Implement in the workspace, with real files written through the tools. | coding | code |
review | Critique what was actually produced, with file paths and line-level reasoning. | review | review |
test | Verify, and separate what was verified from what was not. | testing | test-report |
integrate | Reconcile parallel workstreams. | ops | note |
report | Summarise back to the user. This stage's summary becomes the run's outcome. | summarize | report |
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
| Pipeline | Stages | Shape |
|---|---|---|
product-build | 10 | intake → 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-change | 7 | intake → plan → architect → build (2 developers, parallel) → review (2 reviewers, review-loop, 2 iterations) → test → report. |
quick-answer | 3 | intake → 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.
failedmeans the stage carries an error;emptymeans it produced no turns. Both are recorded with the errorThe stage produced no turns.when there is nothing more specific. Only a non-optional stage halts the run, withStage "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
whenTagsdo not match the tags parsed at intake becomesskipped, with no start time. When the operator cancels, the running stage and every pending stage becomeskipped, and the run iscancelled— distinct fromfailed. - 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, orno 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 keepsstatus: 'done'and emits noerrorevent, so a truncated answer is not flagged the way a crash is. If the tool loop exhausts its 8 round trips, the error isStopped 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,deniedorrunning, with real messages:refused: echo is not granted to ceowhen a role lacks a grant,unknown tool Xwhen the model invents one (the turn continues with an instruction not to use it), andThe 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, scopeengine/stages:Stage "X" has no resolvable participants.A stage that names one unknown role among several logs awarn—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 exitand everything else keeps working. - If the
reportstage never ran or produced nothing, the run'soutcomefalls 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
| Symptom | Where to look | What 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 done | The 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 time | The 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-sentence | The 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 error | route, 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 log | store 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.
Where to go next
- ConceptsSection
The mental model: the office, model routing, and the workspace boundary.
- Known gapsConcept
What is unfinished, what is a deliberate trade, and what the platform will not allow.
- Running it and paying for itFAQ
Which providers and models, how to run locally, what it costs, what stops a runaway bill, and what breaks.
Linked from
Did this page answer your question?