Workflow API Reference
Use this reference while authoring definitions or integrating the workflow SDK programmatically. For a continuous first workflow, start with Custom Workflow Authoring.The workflow() definition
Use workflow(spec) to author a workflow. It validates the schema maps, normalizes or infers the name, and returns a frozen WorkflowDefinition for export, discovery, and ctx.workflow(...) composition.
name
description
autoAttach
true opts interactive top-level named launches through /workflow <name> and the registered workflow tool into opening the graph overlay immediately. Omission and false do not opt in. This option does not affect headless launches, nested ctx.workflow(...) calls, or the existing input-form launch path. Compiled definitions retain this field only as literal true.
heartbeatIntervalMinutes
15-minute default and 0 explicitly disables heartbeats; negative and non-finite values are rejected with a TypeError when the definition is authored. Every compiled definition carries the resolved value, so consumers read a number rather than re-deriving the default.
Cadence is likewise operator-selected. Atomic’s agent guidance assumes the 15-minute default and does not shorten, lengthen, or disable it unless you ask, so a heartbeat you did not configure arrives on that interval. A heartbeat is an alignment checkpoint rather than a failure signal: the agent re-reads the objective, judges whether the run is still on goal, and then continues, steers, replaces, or asks — it is not a cue to intervene in a run that is progressing.
startedAt + n × interval computed from the run’s persisted start time, never from the previous delivery, so a slow delivery, a retry, or a restart cannot drift the cadence.
Each heartbeat arrives in the main chat as a workflows:workflow-heartbeat card naming the workflow, the run id, the cadence, and elapsed run time, with /workflow status <runId> as the inspection hint. It is delivered as a queued steer that waits for the parent’s next protocol-safe boundary, so it never interrupts a response that is already streaming. Only one heartbeat per run is outstanding at a time: an outstanding heartbeat holds its slot until its card is actually consumed into the conversation — not merely until the parent’s turn ends, which can happen while the card is still queued or its queue is paused — so a boundary that falls before that is skipped rather than stacked behind it, and the cadence then resumes at the first future boundary. Pickup resolves the typed workflows:workflow-heartbeat entry and releases only the exact runId + scheduledAt identity, never a rendered-text match, so another custom message cannot free the slot by copying the card text. However long the parent stays busy, and however often its queue is paused, at most one unread heartbeat per run is ever waiting on a host that reports message consumption, which Atomic’s chat host does.
Delivery itself is bounded. Every run in a session shares one heartbeat delivery queue, so a send the host never answers would otherwise hold that queue — and with it every other run’s heartbeats — for the rest of the session. Each in-flight send therefore carries a two-minute watchdog. If the host has not answered by then, that attempt is abandoned and recorded as an undelivered heartbeat: the run’s outstanding slot is released so it re-arms at its next future boundary, and the queue moves on to the next run rather than waiting forever. A late answer from an abandoned attempt is ignored — it cannot settle that heartbeat twice, restart its retries, or disturb whichever heartbeat is in flight by then. A send that fails outright is unaffected and still retries on the existing backoff. The watchdog covers only the send; once a card has been admitted into the parent’s queue it is governed by the consumption rule above, which deliberately has no deadline (#2557).
Paused runs emit nothing and are never backfilled. A resumed or restarted run picks up at the first future boundary on the original cadence rather than bursting the boundaries it missed. Holding to the original cadence across a durable resume takes one stored value: a resume re-dispatches under the original run id but records a fresh start time, so each run writes a single reserved durable anchor record as soon as it has durable progress of its own, before its first boundary comes due. The anchor is only ever read as the earlier of itself and the run’s current start time, so it can restore the original cadence but can never move a boundary or raise one that was missed. A run that reaches a terminal state is re-checked immediately before a heartbeat is queued, again immediately before it is processed, and again before every delivery attempt including retries, so a run that finishes mid-flight stays silent. A card the host has already accepted into the parent’s queue is past all three of those checks, so it is checked once more when the parent reads it — see the terminal-cleanup paragraphs below. When several runs are due at once, they reach the parent in scheduledAt order with the run id as the stable tie-break, and a heartbeat that has to be retried holds its place rather than letting a later one overtake it. Nested workflow runs never heartbeat the parent chat; only top-level runs do. A run keeps the cadence its own definition was authored with: editing, renaming, deleting, or reloading a workflow changes what the next launch uses, and leaves runs already in flight on their launch cadence — including across a durable resume, because the anchor record carries that cadence alongside the start time. A run that launched with heartbeats disabled and is later resumed in a new process is the one exception: a disabled run writes no durable record at all, so nothing preserves that it launched disabled and it adopts whatever the workflow then declares. Cadences below a millisecond are accepted; each raises its next boundary at the finest instant the clock can represent, and the one-outstanding-heartbeat rule still bounds delivery to one card per parent turn. Heartbeat cadences carry a documented representable upper limit. Above roughly 3 × 10^303 minutes, startedAt + interval exceeds the largest finite timestamp a double can hold, so the series has no first boundary: nothing is scheduled, no durable record is written, and ATOMIC_WORKFLOW_DEBUG=1 says so. Such a cadence is still a valid positive interval and is still reported as authored, but it delivers no heartbeat. 0 remains the only value that declares heartbeats off.
When a run reaches a terminal state — completed, failed, blocked, skipped, cancelled, or killed — one cleanup pass drops everything the cadence held for it: its armed wake-up, its next scheduled boundary, its outstanding slot, any heartbeat of its own still waiting in the delivery queue along with the retry timer that belonged to it, and its cadence and durable-anchor memos. Cleanup is idempotent: running it again on the same run creates no state, resurrects no schedule, and reports that there was nothing left to clear. It reacts to the run’s observed state rather than to a transition event, which is what also makes it the recovery pass. At startup, and on every subsequent store change, a run that is already terminal has its stale durable anchor and any leftover queued record discarded rather than replayed, and its anchor is neither read nor rewritten; a run the store no longer holds at all is dropped the same way. Active runs are untouched by another run’s cleanup, and recovery still selects the first future boundary rather than replaying a missed one (#1975).
A recoverable provider or rate-limit block is not the terminal blocked status: the run remains stored as running and resumable. It raises no new heartbeat while blocked, but keeps its cadence state and any card already waiting with the parent; cleanup runs only once the run’s own status becomes terminal.
A heartbeat the host has already accepted into the parent’s queue is beyond that pass, because nothing withdraws a queued message. It is invalidated instead at the moment the parent reads it: the typed card’s exact runId + scheduledAt identity must still be pending for a current nonterminal run. If the run has since reached a terminal state, this process no longer knows that run, or a durable resume has reused the run id with a later pending boundary, the old heartbeat is excluded from the model’s context and cannot steer the parent. That covers all ways a stale card survives — one parked through a long turn while its run finished, one recovered from a previous process at startup, and one admitted before a same-ID durable resume. The card already rendered in your transcript is deliberately left alone: it is a true record that the heartbeat was raised, and rewriting scrollback after the fact would be worse than leaving it. Only the model-facing steer is invalidated.
budget
inputs
ctx.inputs. Atomic validates inputs before the workflow body starts; see Inputs for picker behavior, defaults, and runtime rules.
outputs
{}. TypeScript checks the run return against it at compile time, and Atomic checks it at runtime; see Outputs for declaration, serialization, and child-exposure rules.
worktreeFromInputs
inputBindings.worktree default for stages and tasks.
run(ctx)
ctx.exit(...) for an intentional terminal exit.
Compiled definition fields
TRunInputs describes the validated inputs accepted by run(...) and ctx.workflow(...); it defaults to the workflow’s resolved input values.
workflow({...}) returns a WorkflowDefinition with the resolved name, normalized lookup name, description, runtime defaults, optional budget, schema maps, optional worktree input binding, and run function. Authors provide the fields documented above, and Atomic fills the normalized and defaulted values.
WorkflowContext
Therun function receives ctx: WorkflowRunContext. Prefer its high-level primitives because they create tracked graph nodes and consistent handoffs.
ctx.inputs
inputs schema map. Atomic applies defaults before run starts.
ctx.cwd
ctx.models
models.currentModel is the user-selected session model; leading a stage’s model chain with it (bare, without a :thinking suffix) runs the stage at the session’s model and default thinking level. models.listModels() returns the available catalog. The field is absent when no host catalog exists (for example some detached executions), so definitions should treat it as optional and fall back to their own model configuration.
ctx.task(name, options)
options is required and accepts prompt or its task alias plus the task and stage fields documented below.
ctx.chain(steps, options?)
{task} from chain options; later missing tasks use {previous}.
ctx.parallel(steps, options?)
concurrency and failFast. The call snapshots the current graph frontier at fan-out, so every branch uses the same parent set even when queued or allowed to continue after a sibling failure; downstream stages depend on all settled branches.
ctx.workflow(definition, options?)
inputs when the child has required inputs, while stageName defaults to workflow:<workflow-name>.
workflow({...}). See Workflow Composition for graph flattening, replay, failure, and parent-exit behavior, and WorkflowChildResult for the discriminated result.
ctx.stage(name, options?)
prompt() or complete(). Use it when ctx.task is too coarse and direct session control is required.
ctx.ui
ctx.ui.input(prompt)
ctx.ui.confirm(message)
true or false.
ctx.ui.select(message, options)
ctx.ui.editor(initial?)
initial to seed the editor.
ctx.ui.custom(factory, options?)
done(value). overlay: true is accepted: the attached stage chat mounts it on the same custom-UI slot as an inline widget, which keeps the stage transcript visible and scrollable behind the question, and overlayOptions stays advisory metadata there. label is display-only and defaults to "Custom TUI prompt", while replayIdentity should change when widget semantics change and must not contain secrets.
See Lifecycle Notices and Human Input for replay identity, answer routing, and interactive-only constraints.
ctx.tool(name, args, fn, options?)
name and args. The node is created before fn runs and may appear before, between, after, or without model stages. A completed call replays without rerunning fn, so use this primitive for workflow-owned durable side effects; keep pure computation as ordinary TypeScript.
Cancellation and deadlines. Every callback receives a WorkflowToolContext whose signal aborts when the run is cancelled, when the run is gracefully quit, or when this single node is aborted with workflow({ action: "quit"|"interrupt", runId, stageId: "<tool node id or name>" }). Forward it to fetch, a child process, or any client that accepts an AbortSignal so a stuck call can be stopped:
async () => { ... } still compiles and runs. When timeoutMs is set, a callback that ignores its signal is released after the per-attempt deadline, but any child process or network request it started can keep running until it finishes on its own; forwarding the supplied signal is required for cancellation to stop that underlying work. Without a deadline, quit still abandons an ignored callback after a bounded wait and reports its owning run and node id. A cancelled call writes no replayable checkpoint, so resume re-executes exactly that call at the same ordinal and node id; under failureMode: "return" it also writes one inspection-only tool-failure: record, which is never a replay cache hit.
Options:
failureMode—"throw"keeps the default throw-on-failure behavior;"return"returns a typed success or failure outcome after retries.retriesAllowed— retries failures whentrue; defaultfalse. Retries alone do not bound a callback that hangs because a hung attempt never fails. Callbacks that spawn child processes or perform network I/O need an explicittimeoutMsdeadline and must forward the suppliedsignalto that work.maxAttempts— positive integer maximum when retries are enabled; default3. Invalid enabled retry bounds throw before the callback runs.intervalMs— initial retry interval; default1000.backoffRate— retry interval multiplier; default2.timeoutMs— optional positive finite deadline in milliseconds applied to each callback attempt. Invalid values throw before the callback runs; each retry gets a fresh deadline andAbortSignal, and expiry is handled as an attempt failure.
timeoutMs, each retry receives a fresh signal and deadline. Run cancellation and operator abort remain cancellation rather than timeout, and a callback that completes before its deadline is unchanged. Omitting timeoutMs keeps the existing unbounded callback path.
See ctx.tool — durable cached tool execution for durable failure replay, process-output safety, explicit repair handoffs, and cancellation behavior.
ctx.exit(options?)
status defaults to "completed"; failed exits default to resumable: false, and resumable: true keeps the durable run eligible for a later retry. Supplying resumable with another status records a non-resumable authoring failure. The runtime persists and displays reason, and outputs may provide only declared, schema-valid, serializable output keys.
See Early exit with ctx.exit() for snapshotting, cleanup, replay, and race semantics.
Task and Stage Options
StageOptions and task session fields share the fields below. ctx.task, ctx.chain, and ctx.parallel inherit these options where their signatures use the corresponding option type.
prompt / task
prompt in authored workflow files because it mirrors stage.prompt(...); task remains a supported alias inside authored ctx.task, ctx.chain, and ctx.parallel calls.
previous
previous and {previous} only for compact handoffs. If the prompt has no placeholder, the runtime appends the context, so a large payload can silently bloat the next prompt.
For large handoffs, write artifacts to files, pass their paths with reads, and tell downstream stages to read only the needed sections. Put the instruction in the downstream prompt, for example Read the file at ${artifactPath} and use only the sections needed for this stage. Prefer outputMode: "file-only" when the parent needs only the artifact path.
See Compression and Artifact Handoffs and Filesystem Context for complete patterns.
context / forkFromSessionFile
forkFromSessionFile naming an explicit fork source. Omitting context creates a fresh session unless the runtime is reopening durable state; see Locally Scoped Stage Prompts for choosing fresh reviewer context versus coherent implementation context.
group
"default" runtime group derived from its persistent run identity. Intercom-capable stages inherit that group when group is omitted, including stages in nested workflows. The group stays stable across model fallback, pause/resume, and durable replay, while separate top-level invocations receive different groups.
group is accepted on stage/task options, on ctx.parallel(...) options, and per parallel step. Explicit values override the workflow invocation group; a step-level value also overrides its parallel-set value. A named string becomes an invocation-owned subgroup resolved to workflow:<rootRunId>/<name>, so the same authored name in two concurrent runs never collides. Boolean true auto-generates one shared UUID group per ctx.parallel(...) set (minted once for every item in that set) and is namespaced the same way, while true on a non-parallel stage creates a fresh stage-only subgroup. The trimmed, case-insensitive string sentinels "true" and "auto" have the same automatic behavior and are reserved. group: "default" is the one exception: it opts into the shared default group, is not invocation-owned, and does not receive pending invocation delivery.
The full precedence is: explicit stage/task/parallel group > workflow invocation group > ATOMIC_INTERCOM_GROUP (or legacy PI_INTERCOM_GROUP) > Intercom config > "default". Every workflow model stage receives its workflow invocation group because ordinary Intercom is mandatory. Tool restrictions do not suppress that group; explicit group values retain their existing precedence. Subagents inherit their launching stage’s resolved group by default (see subagents.md). The subagent-only contact_supervisor channel keeps its broker-authorized cross-group route. Ordinary client sends remain group-bound, with one deliberate exception: the workflow invocation group has directional list/send/live-ask control over the subgroups it owns, so an isolated stage stays steerable from the invocation context. That authority does not run in reverse or sideways — a subgroup stage cannot use it to reach a sibling subgroup, and another run cannot use it at all.
Authors do not need to generate or pass a group through ordinary stages, tasks, parallel steps, nested workflows, or delegated subagents. Use an explicit named group or group: true only to create an intentional subgroup, such as isolating one reviewer level from another.
model
fallbackModels / fallbackThinkingLevels
fallbackModels tries the primary first, each fallback in order, and then the current Atomic-selected model when available. It advances for rate limits and quota or usage-limit exhaustion, including messages such as The usage limit has been reached and codes such as usage_limit_reached or insufficient_quota. Auth/provider outages, unavailable models, network timeouts, generic transport errors such as Connection error. or fetch failed, and 5xx responses also advance the chain. A thrown failure that another request to the same candidate can plausibly repair — a rate limit, provider outage, network timeout, or transport error — is retried on that candidate with exponential backoff from settings.retry before the chain advances; retry.enabled: false keeps immediate advancement. A failure the same candidate has already definitively rejected — a rejected credential, an unavailable model, or an incompatible request — skips the same-candidate retry and advances immediately, exactly as in main chat. A same-candidate retry resumes the existing turn when the stage transcript still ends in a message the agent can continue from, and otherwise re-sends the stage prompt; either way the failed provider error is dropped from the live transcript and the prompt is delivered exactly once.
Request/context incompatibility also advances it, including HTTP 400/413/422 bad, unprocessable, or payload-too-large requests; unsupported tools or parameters; context-length or context-window overflow; and too large, invalid_request, or bad_request errors. This lets the chain reach the current selected user model when no configured candidate can serve the request.
A context overflow that the stage session’s compaction has already failed to resolve is terminal for its candidate: it skips the same-candidate retry, because re-sending an identical request cannot fit a context compaction could not shrink, and advances straight to the next candidate.
A schema-backed stage advances the chain for one more reason. Each candidate gets the stage prompt plus up to three corrective follow-ups to produce a valid structured_output call. A candidate that spends that whole budget without one has failed the stage even though every turn returned cleanly, so it fails over exactly like a rate-limited candidate: its attempts are recorded as failures carrying the structured-output error, a [fallback] warning is recorded, its session is disposed, and the next candidate receives the original stage prompt with a fresh correction budget. A capture that already succeeded is never retried on another model. When no candidate remains, the stage still fails with the structured-output contract error and every exhausted candidate’s attempts are recorded as failures.
The chain also covers session creation. A stage session created eagerly — by ctx.__ensureSession(), an eager stage call, or a control attach — retries transient creation failures on its candidate under settings.retry and then walks to the next configured candidate, so a provider that cannot even open a session does not strand the stage. Creation failures that same-candidate retry cannot repair — auth, unavailable model, incompatible request — advance immediately. A creation failure that exhausts the whole chain is not cached: the next call starts a fresh attempt.
That walk runs behind a single creation gate. A concurrent ctx.__ensureSession() or a first ctx.prompt() joins the creation already in flight rather than starting a second walk, so the stage never has two live sessions competing for the same generation.
Controlled pauses are honored throughout. A pause that starts and finishes while a session is still being created keeps its replacement objective, which the next prompt sends exactly once; a pause during a same-candidate continuation is settled as a pause rather than a model failure, so resuming recovers the stage instead of spending a fallback candidate.
Workflow-code errors, tool failures, validation failures, refusals, content-filter or safety blocks, cancellations, and task failures do not advance the chain. A reattached finished stage starts on the model that last succeeded; if that model fails retryably, the full chain restarts from the primary.
thinkingLevel (deprecated)
scopedModels
thinkingLevel field is deprecated.
tools / noTools / excludedTools
tools is an allowlist across built-in and bundled extension tools. excludedTools and noTools: "all" still win for every tool except mandatory ordinary intercom, which remains registered and active.
The bundled subagent tool is available by default on the same terms as main chat. A workflow stage is a top-level session, so it may delegate once; the children it launches may not delegate or control another child. Delegation is exactly one level deep and nothing configures it — there is no config option, agent frontmatter field, or tool parameter for the level. The in-process admission door carries each child’s issued depth in its typed child policy, the executor refuses any launch or interrupt from a session that was itself admitted as a child, and the Rust SubagentControl admission door refuses a child deeper than the single permitted level. That depth is never carried through process environment. Bundled subagent definitions from @bastani/subagents are available to that tool. Explicitly list tools such as subagent, web_search, fetch_content, or intercom when using an allowlist; in-process child sessions load the bundled resources while suppressing the workflow extension lifecycle.
Workflow stages use the same upstream-compatible bash tool as normal Atomic sessions. Enabled commands run through the configured shell with the stage process permissions. There is no command-text allow/deny option: expose or hide shell access with these tool fields, prefer narrow custom tools for repeatable operations, and use a container, VM, or other sandbox for stronger isolation.
customTools
mcp
mcp leaves server access unrestricted by workflow-stage scope.
schema
ctx.stage, ctx.task, ctx.chain, and ctx.parallel items accept a TypeBox schema or a plain JSON Schema descriptor object. The schema may describe an object, array, or primitive, and the captured JSON value becomes the schema-backed stage.prompt(...) result or WorkflowTaskResult.structured; task text remains formatted JSON for handoffs.
A schema-backed StageContext supports one prompt() call, so create another stage for another structured prompt. Missing or invalid structured_output calls receive up to three corrective follow-ups quoting the contract error and reminding the model to call structured_output instead of replying with plain JSON. That budget is per model candidate: a candidate that spends the initial prompt and all three follow-ups without a valid call is treated as a failed candidate and the stage advances to the next entry in fallbackModels, which receives the original stage prompt and its own fresh budget. The recorded attempt error names what the turn actually looked like — no assistant message after the prompt, an assistant message with empty text, or the structured_output validation error — so a repeated external cause is attributable. With no fallback candidate left, the stage fails with the contract error rather than completing. An explicit tool allowlist automatically receives the final-answer tool, while items without schema do not.
When schema and output are both configured, the successful structured_output turn carries two separate results. All ordinary assistant text blocks from that exact message, in order, are written to the artifact; the successful tool arguments become the typed schema-backed workflow value. The runtime snapshots both sides against the exact successful tool-call id rather than searching by tool name, so corrective attempts and later admitted turns cannot replace either result, and it never serializes the tool arguments into the artifact. When the successful message carries no ordinary text — including when a model-fallback session recreation leaves the live session without that message — the artifact falls back to the most recent earlier assistant text that made no structured_output call; if no such text exists the artifact is empty and its receipt includes the standard empty-artifact warning. Stages with schema but no output keep their existing result-text behavior. Builtin pattern workflows that hand structured decisions to later stages (adversarial-verification, generate-and-filter, tournament, loop-until-done) persist those decisions themselves, so their *.json inter-stage artifacts remain machine-readable JSON.
output / outputMode
false. outputMode defaults to inline; file-only keeps the parent result compact by returning an artifact reference instead of full text and requires an output path.
The runner writes the stage’s final assistant message to output after the stage ends, so that path belongs to the runner. For a schema-backed stage, this means the ordinary text in the assistant message that calls structured_output, not the structured tool arguments. A stage that declares output: also automatically gets a full, rendered, line-oriented transcript of its session, and one appended instruction telling the model that its final message becomes the artifact — the workflow definition does not need to describe any of this.
An admitted external turn (for example, a subagent completion) can arrive while the stage is still running and remains visible both to the model and in the companion transcript. The runner does not try to work out which turn was “really” the deliverable: that is an inference about intent, and an earlier revision that scored candidates by byte size got it wrong in both directions. If a late turn displaces the intended content, the transcript still holds it.
The companion transcript is written once under the durable Atomic config root at ~/.atomic/workflows/runs/<runId>/transcripts/ (or the equivalent configured agent root; ATOMIC_WORKFLOW_ARTIFACT_DIR overrides that root). It is never placed inside the repository tree or OS temporary storage: a home-scoped durable location survives both worktree deletion and OS temp purges, and staying outside the repo keeps full tool output — which may contain secrets — from being committed accidentally. Run-scoped artifact directories are pruned only when their durable/live run record is terminal (or the directory is an unowned orphan) and older than the exported WORKFLOW_ARTIFACT_RETENTION_MS policy. Running, paused, quit, blocked, and awaiting-input runs are exempt indefinitely because their artifacts are live resume dependencies. A live continuation transitively protects the original run directory in its resumedFromRunId chain, even when intermediate continuations have different run IDs; merely quoting another run’s artifact path does not protect that unrelated owner or make the quoting run depend on it. A failed run with no live continuation is terminal and does age out: it stays retryable, but the retention window is the grace period it gets, otherwise repeated recoverable failures would accumulate artifacts forever. When a terminal durable owner is aged out, the durable entry is deleted first; if authoritative deletion is unavailable or refuses, the artifact directory is preserved. Goal ledgers, Ralph implementation notes, and QA video paths share that same durable root and retention policy. The receipt names both absolute paths. Search the transcript with rg, then read only the narrow line ranges you need; do not read the whole transcript into a downstream prompt. The transcript is a secondary searchable record; the output artifact remains the curated handoff.
The receipt reports facts only. An empty artifact produces WARNING: the stage artifact is empty; search the companion transcript for this stage's work. A non-empty artifact is never classified, however short and even if it only names its own output path: deciding whether such text is a pointer or a deliverable requires knowing what the author meant, and the regex bank that previously attempted it produced false alarms on genuine short output. The transcript named in every receipt is the recovery path for anything that looks wrong to a reader.
reads
false. Paths are supplied as readonly strings.
reads passes paths, not content. It prepends a [Read from: <paths>] directive to the prompt and the stage reads those files itself with its own read tool, so a stage sees whatever is on disk when it runs — not a snapshot taken when the path was passed. Any stage that rewrites an artifact between producer and consumer changes what the consumer reads. The runtime fails the stage loudly before the model turn when a referenced path is missing, rather than allowing an empty read to look like valid context. Goal preserves that as a reviewer execution failure attributed to the reads contract; it does not misreport the missing file as a malformed reviewer decision. This keeps large artifacts out of the prompt; state the expectation in the prompt too, for example Read the file at ${artifactPath} before continuing.
maxOutput
204800 bytes and 5000 lines.
artifacts
true; explicit output-file artifacts remain available when automatic collection is disabled.
worktree
ctx.task(...). Atomic creates it at <main-root>/.atomic/worktrees/<flattened-name> on branch worktree-<flattened-name>, replacing / in generated names with +. Creation remains anchored at the canonical main root when invoked inside a linked worktree. The base ref resolves as explicit baseBranch, then origin/<default-branch> (fetched when absent), then HEAD. Atomic propagates local settings, configures the main repository’s Husky or populated hooks directory through shared core.hooksPath, symlinks configured worktree.symlinkDirectories, and copies gitignored .worktreeinclude matches without overwriting tracked files. It is mutually exclusive with gitWorktreeDir; cleanup forcibly removes the worktree and deletes its branch even when startup fails before the callback.
gitWorktreeDir / baseBranch
ctx.stage, ctx.task, ctx.chain, and ctx.parallel.
- Creation and validation: A missing path is created with
git worktree add --detach <path> <baseBranch>from the canonical main repository root, where an omitted or blankbaseBranchdefaults toHEAD. Existing paths must be same-repository worktree roots outside the invoking checkout; the checkout itself, nested targets, and missing targets whose symlinked parent resolves inside it are rejected. - Cwd remapping: The default cwd preserves the invoking repository-relative subdirectory inside the worktree. Absolute cwd values inside the invoking repository are remapped, values already inside the worktree are preserved, and relative values resolve from the worktree cwd without lexical or symlink escape.
- Output containment: Runner-managed reusable-worktree relative outputs follow the effective worktree cwd and cannot escape through traversal or symlinks. Temporary-worktree outputs are copied to distinct runner-owned artifact directories before cleanup, including in
file-onlymode. Explicit absolute outputs remain caller-selected. - Caching and diagnostics: Temporary isolation defaults to the runner invocation cwd, and relative task cwd values resolve there. Reusable setup is cached by canonical repository and target identity independently of equivalent path spelling or
baseBranch, revalidates checkout identity before reuse, retries one transient timeout from read-only repository probes, and reports the exact Git command, cwd, timeout, elapsed time, exit status or signal, and spawn error details on failure. - Security boundary: Worktrees isolate checkouts and cwd, not the operating system. Use a container, VM, or another OS-enforced boundary for untrusted code that can race or mutate arbitrary paths.
setupGitWorktree(options) returns the validated and remapped setup result.
sessionDir
atomic --mode json --session-dir <dir> -p '/workflow <name> ...', Atomic writes the main chat transcript and every stage transcript under <dir>; the same inheritance applies when the non-default directory comes from ATOMIC_CODING_AGENT_SESSION_DIR or settings. Without a non-default host directory, stages use Atomic’s global session store.
cwd / agentDir
Host-supplied SDK seams
StageOptions used by embedded integrations, not ordinary workflow-file defaults. The standalone workflow-package authoring declaration intentionally omits most of them and types sessionManager and settingsManager as never, so package-authored workflows should not pass these fields directly.
The runtime strips workflow-owned fields before forwarding session options. Internal durable fields such as resumeFromSessionFile, durableReplayKey, and durableAccumulatedDurationMs are not public authoring options.
name (step items)
chainDir
WorkflowChainOptions.chainDir sets the base directory for relative reads and outputs inside an authored ctx.chain(...). It is an in-workflow primitive option, not a top-level workflow tool argument.
concurrency / failFast
WorkflowParallelOptions uses concurrency to bound active tasks in an authored ctx.parallel(...). When omitted, the runtime uses the workflow’s defaultConcurrency setting, which defaults to 4; parallel execution is fail-fast unless failFast is explicitly false.
Stage prompt options (StagePromptOptions)
stage.prompt(...), not to stage creation. They control prompt expansion, images, streaming/source metadata, preflight reporting, and per-prompt output/session behavior.
Completion options (CompleteStageOpts)
stage.complete(...). fallbackThinkingLevels is the same deprecated compatibility helper used by stage options.
Reasoning levels
Eachmodel and fallbackModels entry accepts a model_name:thinking_effort suffix that sets the reasoning effort for that candidate (off, minimal, low, medium, high, xhigh, max). The selected model’s capability map still governs whether xhigh or max is available. The model string includes the effort, so one fallback chain can mix efforts—for example, a high-effort primary with lower-effort, cheaper fallbacks:
thinkingLevel stage option is deprecated. It still applies as a default to any candidate without a suffix, and when both are present the suffix wins, but new workflows should fold the effort into the model strings:
ctx.task/ctx.chain/ctx.parallel options, ctx.stage options, builtin workflow stage definitions, and workflow parameters. fallbackThinkingLevels is an optional compatibility helper aligned by index to fallbackModels; it applies only to fallback entries that do not already carry a suffix. Each WorkflowModelAttempt reports the resolved model and the effective reasoning effort used for that attempt.
StageContext
ctx.stage(name, options?) returns direct control of a tracked stage session. The executor owns session disposal and wraps stage operations with workflow lifecycle tracking.
stage.name
ctx.stage(...).
stage.prompt(text, options?)
stage.complete(text, options?)
maxTokens.
stage.sendUserMessage(content, options?)
deliverAs: "steer". During controlled pause it joins the raw hold and does not start a turn.
deliverAs: "steer" is consumed after the current assistant response finishes its whole tool batch and before the next model request; deliverAs: "followUp" is consumed only when the agent would otherwise stop. Each queue is FIFO in admission order, and steering keeps priority over an earlier-submitted follow-up.
Native sessions accept strings or text/image content blocks. Non-native fallback adapters accept only strings and reject block arrays; deliverAs affects streaming delivery only, and follow-on turns retain the stage MCP scope.
Externally produced Intercom and subagent notices admitted before the generation closes drain through the same session. When a busy stage owns a foreground subagent, exact-owner detach gets first refusal before Intercom enters this boundary; unclaimed traffic then uses normal stage admission. Traffic arriving after the atomic close boundary cannot reopen the completed stage and is surfaced once through the main-chat path instead.
See Stage follow-on user messages for the full lifecycle and schema-backed example.
stage.steer(text) / stage.followUp(text)
sendUserMessage() to start one when the stage is not paused. A controlled pause holds queued steering and follow-up items without delivering them, and only the existing stage resume action makes them eligible again.
stage.subscribe(listener)
AgentSessionEvent. Call the returned function to stop receiving events.
stage.sessionId / stage.sessionFile
sessionFile is undefined when no file is available.
stage.setModel(model) / stage.setThinkingLevel(level) / stage.cycleModel() / stage.cycleThinkingLevel()
WorkflowModelValue accepts a string or supported SDK model object, and WorkflowThinkingLevel is "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; Atomic’s embedded runtime narrows the model arguments and cycle result to its AgentSession types.
stage.agent / stage.model / stage.thinkingLevel / stage.messages / stage.isStreaming
AgentSession properties.
stage.navigateTree(targetId, options?)
stage.compact() / stage.abortCompaction()
VerbatimCompactionResult.
stage.abort()
Result Types
Workflow primitives return serializable result contracts that carry text, structured values, artifacts, model attempts, child boundaries, and run snapshots. The root authoring declaration directly exportsWorkflowTaskResult, WorkflowChildResult, WorkflowArtifact, WorkflowDetails, RunResult, and StageSnapshot; supporting conditional or union-branch aliases shown below describe the source contract but are not all separately exported by the lean standalone declaration.
WorkflowTaskResult
ctx.task returns this type; ctx.chain and ctx.parallel return arrays of it. structured is present when the item used schema.
model or fallbackModels, each recorded attempt can include usage aggregated from meaningful assistant responses in that attempt. The four token buckets remain separate, cost sums the provider-reported total cost, and turns counts the assistant usage records included in the aggregate. Usage from earlier retained or reattached session history is excluded, while billed error responses removed during a same-model retry remain attributed to that attempt. The usage property is omitted when the provider reports no meaningful token or cost signal. Stages that use only the default model without explicit fallback configuration do not currently create model-attempt records.
WorkflowDetails
WorkflowChildResult
ctx.exit(...), including status: "completed" or status: "failed", exposes only a partial contract and optional exit reason; an unintentional failed or internally cancelled child still rejects the parent call.
WorkflowStageResult
stage.prompt() resolves to the schema’s static value. A stage without schema resolves to text.
WorkflowArtifact
path and kind are always present.
RunResult
run(...) returns this type. exited identifies ctx.exit(...) termination, and stages contains the final stage snapshots.
Programmatic usage
@bastani/atomic/workflows is Atomic’s published workflow SDK. Import workflow from that specifier, import Type from typebox, and export the definition returned by workflow({...}). Workflow helpers may also import the host TypeBox runtime subpaths typebox/compile and typebox/value; the legacy @sinclair/typebox root and matching subpaths remain supported. Keep runtime helpers such as widget factories and shared utilities in a subdirectory outside the top-level discovery scan, such as .atomic/workflows/lib/; see Workflow Locations.
Package authors list both @bastani/atomic and typebox in peerDependencies. The @bastani/atomic package publishes compiled JavaScript and declarations for @bastani/atomic/workflows, @bastani/atomic/workflows/builtin, and each @bastani/atomic/workflows/builtin/* module. TypeScript resolves those exports directly under moduleResolution: NodeNext. When Atomic executes a workflow file or an imported helper, its runtime loader resolves the workflow SDK and supported TypeBox aliases to the same in-memory host modules. It intentionally does not expose extension-only host modules such as the agent core, UI, provider transport, or lockfile implementation.
run and call run(definition, inputs) with an exported definition and validated inputs. Use createRegistry() when an integration needs to register, merge, or look up several definitions before selecting one to run. The extension also registers the /workflow commands and the workflow tool for named execution, discovery, inspection, messaging, run control, and reload.
workflow(spec)
workflow() definition. Export the returned definition or pass it to ctx.workflow(...), run(...), or a registry.
createRegistry(initial?)
register, merge, and remove return registries rather than mutating the current registry.
run(definition, inputs, opts?)
RunOpts
run(...). Every field is optional.
The public authoring declaration intentionally excludes runtime-only executor fields such as defaultSessionDir, gitWorktreeSetupCache, durableBackend, durableScope, and onStageSession.
resolveInputs(schema, provided)
setupGitWorktree(options)
normalizeWorkflowName(name) / workflowNamesEqual(a, b)
GraphFrontierTracker
Execution policies
WorkflowExecutionPolicy.
createStore() / store
createStore() returns an isolated workflow state store. store is the default singleton exported by the SDK authoring surface.
This is the stable core exposed by the standalone authoring declaration. Atomic’s runtime store also has graph, prompt, session, pause/resume, snapshot, and subscription methods used by embedded integrations; those richer runtime controls are not part of the lean workflow-package Store contract shown here.
The embedded runtime’s graphSnapshot() returns one deeply frozen, payload-bounded projection for each store version; repeated reads at the same version return the same object. Runtime code must change graph-visible state through a version-bumping store method before another task can observe it. subscribeInvalidation() reports those changes synchronously without creating a full snapshot. Legacy subscribe(snapshot) consumers still receive a full cloned snapshot; this includes status-file output when statusFile: true, while the default statusFile: false path avoids that payload traversal. Authored stage results remain omitted; a failed author-exit result may retain a bounded JSON output object for status inspection, and oversized output falls back to the existing bounded string fields without adding synthetic output keys.
createCancellationRegistry() / cancellationRegistry
cancellationRegistry is the default singleton. Aborts signal registered controllers and children rather than killing processes.
Static / TSchema
Type builder from typebox.