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.Model-tool launch contract
The model-facingworkflow tool is distinct from the workflow(spec) authoring function below. The agent decides whether a workflow fits the request, then launches a registered workflow by name:
inputs are validated with defaults against the named workflow’s input contract; invalid or unknown inputs, or an unknown name, return status: "failed" without launching. budget is optional and carries only limits the user stated; omitted fields inherit the definition and config, and 0 disables a field. A successful run returns its runId. Use that ID for inspection and lifecycle calls.
Runs launched through the model tool belong to the launching session. That ownership survives tool recreation, reload and restart, and inspection or control from another session is rejected, including bulk controls with mixed ownership.
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.
Delivery and unread cards
Each heartbeat arrives in main chat as aworkflows:workflow-heartbeat card. It names the workflow, run id, cadence, and elapsed time, with /workflow status <runId> as the inspection hint. Its queued steer waits for the parent’s next protocol-safe boundary and never interrupts streaming text.
Only one heartbeat per run can be outstanding. Its slot stays occupied until the card is consumed into the conversation, even if the parent’s turn ends or its queue is paused. Boundaries reached while that slot is occupied are skipped, not stacked. Delivery resumes at the first future boundary.
An unresponsive host does not block other runs indefinitely: a heartbeat send times out after two minutes. Once accepted, a card can remain unread without a consumption deadline. Delivery failures retry; missed boundaries do not accumulate.
Pause, resume, and cadence limits
Paused runs emit nothing. Resume and restart pick up at the first future boundary on the original cadence; missed boundaries are never backfilled. Only top-level runs send heartbeats to parent chat. Existing runs and durable resumes retain their launch cadence despite later definition edits, renames, deletion, or reload. There is one exception: a run launched with heartbeats disabled writes no anchor. If resumed in a new process, it adopts the definition’s current cadence. When several runs are due together, older scheduled heartbeats arrive first. Use a practical interval in minutes; submillisecond values cannot exceed the one-outstanding-card limit, and intervals above roughly 3 × 10^303 minutes cannot produce a first heartbeat. Set0 to disable heartbeats explicitly.
Terminal cleanup and stale cards
Terminal runs stop producing heartbeats. A recoverable provider or rate-limit block also suppresses new heartbeats but retains the cadence for resume. An already-visible card remains in the transcript. Its stale instruction is excluded from model context if the run has ended or the card no longer belongs to the current run state.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.
On resume, a cached stage declaration stays synchronous. Its replay metadata is saved before the next checkpoint, prompt, task, tool, or workflow completion. Await those operations and handle their rejection normally; a storage failure is not treated as a successful replay.
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"|"pause", 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. Targeted node aborts, and all cancellations under failureMode: "return", retain an inspection-only tool-failure: record, never a replay cache hit. An uncaught targeted abort records failedToolNodeId while the tool node stays cancelled; failedStageId remains model-stage-only. Resume without a stage override retries the proven unfinished tool at its original ordinal and node id, replaying completed checkpoints. Missing or ambiguous frontier evidence returns insufficient_state; see tool-abort recovery.
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 / forkFromSessionId
forkFromSessionId naming an explicit session ID in the selected session store. Use the earlier task’s sessionId, not its transcript file path. IDs are preserved verbatim, including an empty stored ID. 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
model: "auto" for prompt-based selection of an execution model and supported effort before session admission. Omission and concrete models keep prior behavior. Shared chain/parallel defaults accept auto; each stage receives one decision. The router sees the final supplied prompt, including interpolated inputs and supplied task context, rather than only a stage name.
modelConstraints?: ModelConstraints applies hard restrictions to auto selection and execution/compaction fallbacks. Fields are allowedModels?: string[], allowedProviders?: string[], excludedProviders?: string[], allowedEfforts?: (string | null)[], maxInputCost?: number, maxOutputCost?: number, minContextWindow?: number, and requiredInputs?: ("text" | "image")[]. Model IDs are exact provider/model IDs. Costs are catalog USD per million tokens including pricing tiers, not task budgets. Efforts are off, minimal, low, medium, high, xhigh, max, or null for nonreasoning models. Empty allowlists admit nothing. Inherited and stage restrictions all hold; a task override cannot widen shared restrictions. Use allowedEfforts to restrict automatic reasoning selection. The user’s modelRouting.allowedModels and excludedModels settings also apply to every auto stage, including builtin stages that omit a model; modelConstraints cannot widen them, and a stage whose filters leave no eligible model fails before launch.
An auto stage has no session until it receives prompt text. Model-dependent operations such as compact() or cycleModel() before that point report an error; call prompt() first or deliberately select a concrete model with setModel(). Session operations after admission keep their normal behavior.
fallbackModels
fallbackModels tries the primary and each declared fallback in order. If all fail, the stage fails; the current Atomic-selected model is not appended implicitly. To use it as a final fallback, include it explicitly. The chain advances for:
- Rate limits and quota or usage-limit exhaustion, including
The usage limit has been reached,usage_limit_reached, andinsufficient_quota. - Auth/provider outages, unavailable models, network timeouts, generic transport errors such as
Connection error.orfetch failed, and 5xx responses.
settings.retry. Set retry.enabled: false for immediate advancement.
Rejected credentials, unavailable models, and incompatible requests advance immediately, as in main chat. Retrying the same candidate cannot repair these failures.
A stage that sets model or fallbackModels uses only the chain it declares. The fallbackModels list in settings.json and the chain of the session that launched the workflow never apply to it, so a provider failure cannot move the stage to a model or reasoning level outside the chain you declared. A stage that sets neither runs on the session’s current model and keeps that session’s fallback behavior.
Explicit stage models can use numbered account providers such as openai-1/gpt-5-mini:low with credentials saved under openai-1 in auth.json. They use the source provider’s catalog and endpoint, but the account’s own credentials; no alias entry in models.json or provider-registration extension is needed. The source provider and requested model must exist. modelRouting (allowedProviders, excludedProviders, allowedModels, excludedModels) and routerModel apply to model: "auto", not to these pinned primary or fallback models.
A same-candidate retry resumes the existing turn when the transcript ends in a message the agent can continue from. Otherwise, it re-sends the stage prompt. In either case, Atomic removes the failed provider error from the live transcript and delivers the prompt 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.
If extension initialization fails and its session cannot be cleaned up, the stage stops without trying another model. Later prompts or attachment attempts on that stage also fail. Inspect both the initialization and cleanup errors and resolve the extension failure before starting a new run; changing fallback models does not fix a cleanup failure.
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 candidate gets the original prompt plus up to three corrective follow-ups to produce valid structured_output. Exhaustion records a failed attempt and advances to the next candidate with a fresh correction budget. Successful capture is never retried. If the chain runs out, the stage fails with its structured-output contract error.
Corrective follow-ups are reserved for turns that actually answered without a valid structured_output call. A schema-backed prompt that resolves without any assistant message is treated as a transient provider failure instead: it spends the same-candidate retry budget from settings.retry, then advances to the next candidate, and its attempt records carry Model turn ended without an assistant message after the prompt (empty completion) rather than the structured-output contract error. If every candidate ends that way, the run blocks as a recoverable provider_unavailable failure that can be resumed once the provider recovers. Cancellation during such a turn stops both the retries and the chain.
Session creation uses the same retry and fallback policy. Transient failures retry; rejected credentials, unavailable models, and incompatible requests advance immediately. If all candidates fail, a later call may try again. Concurrent callers share creation rather than starting duplicate sessions.
Pause remains effective during creation or retry. Resume continues the paused objective without consuming a fallback candidate solely because of the pause.
Resume sends a replacement objective exactly once, including when pause occurred during session creation.
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.
scopedModels
tools / noTools / excludedTools
tools is an allowlist across coding and bundled extension tools, including Intercom. excludedTools and noTools: "all" win for every tool. Include intercom explicitly when a restricted stage needs peer coordination.
The bundled subagent tool is available by default, subject to the same restrictions as other tools. Workflow stages may delegate one level; their children cannot delegate or control another child. This depth is not configurable. Explicitly include subagent, web_search, fetch_content, or intercom when a restricted stage needs them. Child sessions do not expose the workflow extension’s tool.
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
structured_output call for that item. ctx.stage, ctx.task, ctx.chain, and ctx.parallel items accept a TypeBox schema or a plain JSON Schema descriptor object. The calling model supplies instructions and a nonempty named state, plus optional exact model and fallbackModels IDs. The tool infers a value matching the schema; that inferred value becomes the schema-backed stage.prompt(...) result or WorkflowTaskResult.structured. The calling model’s arguments are not the result. The inferred result has no fixed size limit beyond the selected model’s own output-token limit. Omit model to use the current stage chat model. After the primary and explicit fallbacks, the current chat model is tried if it was not already used. Atomic resolves each ID through the stage model registry. A registered classifier is used only when the schema is a finite Choice; otherwise that candidate is skipped and the skip is recorded. A classifier runtime failure advances to the next model unless the request was cancelled or refused for safety.
A schema-backed StageContext supports one prompt() call, so create another stage for another structured prompt. A missing call, or an inference failure that exhausts the tool’s own model chain, receives up to three corrective follow-ups quoting the contract error. That budget is per stage model candidate. A candidate that spends the initial prompt and all three follow-ups without a valid inferred result fails, and the stage advances to the next entry in fallbackModels, which receives the original stage prompt and its own fresh budget. With no stage fallback left, the stage fails with the contract error rather than completing. An explicit tool allowlist automatically receives structured_output, while items without schema do not.
With both schema and output, ordinary text from the successful structured_output message goes to the artifact. The inferred tool result is the typed value. Corrective attempts and later turns cannot replace that pair. If the successful message has no ordinary text, the artifact uses the most recent earlier assistant text without a structured_output call, or stays empty with a warning. The pattern builtins save their inter-stage decisions as JSON separately.
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 owns output. For an ordinary stage, it saves the completed assistant answers from the current prompt generation in order. The first answer is unchanged; later answers are appended after ## Supplement 1, ## Supplement 2, and so on. Text blocks retain their original text, including whitespace and repeated answers. Tool-call progress, reasoning, user input, tool results and earlier session history remain transcript-only. A single answer therefore keeps its existing artifact format. A schema-backed stage retains its separate contract: the ordinary text paired with the successful structured_output call owns the artifact, and the tool’s inferred value is the structured result.
Follow-ups accepted before the stage closes append numbered supplements. Corrections must say what they correct; Atomic does not infer replacement intent. Later retained-session chat cannot overwrite a completed artifact. Use a new tracked stage and output path for replacement. Compaction, branch navigation, and model fallback do not discard already accepted answers; historical branch text is not appended as new output. Stages without output retain their last-response behavior.
A stage declaring output also gets a rendered companion transcript. Describe the deliverable in the prompt rather than asking the model to write the runner-owned artifact.
The companion transcript lives 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 refreshes when newly admitted answers update the artifact before close.
The transcript never lives in the repository or OS temporary storage. Its home-scoped location survives worktree deletion and temp purges, and keeps full tool output, which may contain secrets, out of accidental commits.
Retention follows the exported WORKFLOW_ARTIFACT_RETENTION_MS policy:
- Run-scoped directories are pruned only when their durable/live run record is terminal, or they are unowned orphans, and they exceed the retention age.
- Running, paused, quit, blocked, and awaiting-input runs are exempt indefinitely because their artifacts are resume dependencies.
- A live continuation protects every original run directory in its
resumedFromRunIdchain, even across different continuation IDs. Merely quoting another run’s artifact path does not protect that unrelated owner or create a dependency. - A failed run without a live continuation ages out. It remains retryable during the retention grace period; failures do not preserve artifacts forever.
- Atomic deletes an aged-out terminal owner’s durable entry first. If authoritative deletion is unavailable or refused, it preserves the artifact directory.
rg, then read only the needed line ranges. Do not load the whole transcript into a downstream prompt. It is a secondary searchable record; the output artifact remains the curated handoff.
An empty artifact produces WARNING: the stage artifact is empty; search the companion transcript for this stage's work. Non-empty artifacts are not quality-checked automatically. Inspect the deliverable and search its companion transcript when something appears missing.
reads
false. Paths are supplied as readonly strings.
reads passes paths, not content. It prepends a [Read from: <paths>] directive; the stage uses its own read tool to inspect those files. It sees the file when it runs, not a snapshot from when you supplied the path. Rewriting an artifact between producer and consumer changes what the consumer reads.
A missing referenced path fails the stage before the model turn. Goal reports this as a reviewer execution failure attributed to reads, not a malformed reviewer decision.
This keeps large artifacts out of the prompt. State the reading requirement 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
cwd, stages use the workflow invocation directory. For workflows launched from an SDK session, this is the owning session’s cwd, even when the host process runs elsewhere. A cwd configured through createAgentSessionAdapter() takes precedence over that default.
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 resumeFromSessionId, 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 3; explicit configuration and per-call concurrency remain honored. Parallel execution is fail-fast unless failFast is explicitly false.
For a dynamic parallel step array that cannot be scanned, set possibleStageNames: ["review-*"] in the call’s options. Supply a literal array covering every possible step name; * matches a varying name component. This metadata only controls advance stage discovery, not execution or concurrency. Named helpers can forward options.possibleStageNames directly to their parallel calls when every direct caller supplies a literal array, including through named relative imports. Use plain data properties in those caller options; options methods/getters, mutation, and side-effecting parameter defaults are not supported for discovery. Opaque forwarding and unannotated dynamic calls still produce discovery warnings.
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(...).
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:
ctx.task/ctx.chain/ctx.parallel options, ctx.stage options, builtin workflow stage definitions, and workflow parameters. Each WorkflowModelAttempt reports the resolved model and effective reasoning effort for that attempt. Use suffixes rather than separate reasoning fields.
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 session delivery is literal by default. Set expandPromptTemplates: true to opt into the session’s existing skill/prompt-template expansion and registered extension-command dispatch; a command may be handled without creating a user turn. Stage admission still applies before delivery. The attached stage composer opts into this path for /skill: invocations only; ordinary programmatic messages keep the default. Custom adapters must implement the option to provide equivalent behavior.
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. Closing the atomic boundary cancels still-running stage-owned children and suppresses their later findings and completion notices. Ordinary traffic not owned by that stage arriving afterward cannot reopen the completed stage and retains the existing single main-chat route.
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
sessionId to identify a session in its selected store. sessionFile is display metadata and is undefined when no file is available; it cannot be used as a fork or resume identity.
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.
Each stage snapshot carries its graph position: parentIds lists the stages it depends on, and executionOrder is its admission order among the run’s stages and tool nodes. model is the effective model after fallback resolution; routerSelection is the automatic routing choice: model and effort, ranked fallbacks, and candidates, the full IDs of every model the router could choose from after modelRouting and modelConstraints filtering. Selections recorded before candidates existed omit it. Timestamps are epoch milliseconds and are absent until the stage starts or ends. The runtime may replace parentIds before a stage starts, so read it from the latest snapshot rather than caching it.
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 typebox/compile and typebox/value. 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 supplies the workflow SDK and current TypeBox modules.
The loader also shares the host’s modules for the exact specifiers @bastani/atomic, @bastani/pi-ai, and @bastani/pi-ai/providers/all. These exceptions expose Atomic’s public root API and the AI compatibility API and provider transports. The host’s versions take precedence over project-local copies, including in standalone-binary installs without project packages. Workflow files and relative imports still refresh on reload. Other deep subpaths resolve from disk. The workflow loader does not share the extension loader’s remaining host aliases, including the separate agent-core and UI packages, other AI subpaths, and proper-lockfile. Older or alternative hosts without getWorkflowHostModules() retain filesystem resolution for the three specifiers.
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?)
Stage sessions and adapters
Whenopts.adapters supplies neither agentSession nor prompt, run() uses createAgentSessionAdapter({ cwd: opts.cwd }). Each stage then runs its own in-process Atomic createAgentSession session with persistent session storage by default. Use the stage’s sessionDir to choose its session directory, or supply an explicitly selected in-memory session manager through a custom adapter for ephemeral stages. A persistent storage failure fails stage creation; it never falls back to memory. Stage sessions resolve models, credentials, settings, and resources the same way createAgentSession does. A prompt adapter on its own answers stage prompts itself, so run() adds no default session adapter beside it.
createAgentSessionAdapter(baseOptions) yourself to apply the same createAgentSession options, such as agentDir, modelRuntime, or settingsManager, to every stage. Options set on an individual stage override baseOptions. baseOptions cannot set sessionManager, because every stage needs its own. A cwd in baseOptions applies to every stage that doesn’t set its own, and session files saved through sessionDir record that directory.
A custom adapter’s create(options, meta) receives the stage’s session options, including any session manager Atomic opened for sessionDir, resume, or fork. It must resolve to { session } or to the session itself, where session implements StageSessionRuntime. To change options per stage while keeping the stage policy that createAgentSessionAdapter applies, such as hiding the workflow tool from stage sessions, delegate to it:
ask_user_question waits for an answer until the run is aborted through opts.signal. For unattended runs, pass executionMode: "non_interactive"; stages then run without ask_user_question.
RunOpts
run(...). Every field is optional.
onStageSessionEvent streams every agent-session event from each stage (for example message_update, tool_execution_start, and tool_execution_end), tagged with the stage’s runId and stageId. AgentSessionEvent is exported by @bastani/atomic. The stream continues when a fallback model replaces a stage’s session. Stages in nested ctx.workflow(...) runs report the child run’s runId. Exceptions thrown by the listener, and rejections from an async listener, are ignored and do not affect the stage.
The public authoring declaration intentionally excludes runtime-only executor fields such as defaultSessionDir, gitWorktreeSetupCache, durableBackend, durableScope, and onStageSession.
Choosing the durable backend
run() uses the local SQLite journal at ~/.atomic/workflows/journal.sqlite by default, shared with sessions under separate wf_ and session_ table prefixes. It releases the run’s lease when execution settles. Select a journal with RunOpts.journal:
{ path }opens that SQLite file.{ database, prefix? }uses a caller-owned asyncSqliteDatabasefacade. Atomic initializes the journal schema on it but does not close the database.{ memory: true }explicitly selectsMemoryRunJournal. Records last only for the process and cannot recover runs after exit.{ adapter, options?, secrets?, prefix? }loads your installed adapter module. See Storage adapters.- A
RunJournalinstance is used as supplied.SqlRunJournal,openSqlRunJournal,MemoryRunJournal, journal error classes, andregisterRunJournalConformanceare exported from@bastani/atomic/workflows.
await openSqlRunJournal(path) to open a file directly, or omit path for the default journal. It also accepts a caller-owned node:sqlite DatabaseSync directly and wraps the connection itself. Pass an async SqliteDatabase facade to SqlRunJournal.open or RunOpts.journal.database instead. await resolveRunJournal(options) applies the selection rules below.
To attach your own SQLite-compatible driver, implement the async SQLite storage API and pass its facade to SqlRunJournal.open:
SqlRunJournal.open leaves ownership of the facade with you. Journal tables use wf_ names, optionally preceded by your prefix; session tables use a separate namespace in the same database. A new prefix selects an empty journal, not a migration. Bare journal table names from earlier unreleased builds are not read. SQL event and lease timestamps, including expiry checks, use the database clock. Journal storage failures expose StorageUnavailableError.adapter, .location, and .cause; use .location rather than .path.
Custom journals implement this full async interface. The named types are exported from @bastani/atomic/workflows:
getRun returns the current summary or undefined. leaseState returns the current lease with its renewedAt timestamp and ageMs, or undefined when unheld. ageMs is the journal-clock time since renewal, computed in the same read and clamped to a non-negative safe integer. Use it for expiry decisions instead of subtracting renewedAt from the process clock. takeoverExpired measures a duration on the journal’s clock and atomically refuses a foreign lease unless its renewal is strictly older than staleAfterMs, returning the held outcome while fresh. read loads bounded pages only as consumed, in sequence order starting at the inclusive fromSeq, which defaults to 1. Stopping iteration must stop further page reads.
append checks lease fencing first, then the idempotency key before expectSeq. A committed keyed retry returns { status: "exists", entry } with the original entry even when the retry’s sequence expectation is stale. A new key with a stale expectation rejects with JournalCasConflictError without writing.
SqlRunJournal.append serializes its payload once at invocation, before asynchronous lease and key checks, so later caller mutation cannot change the event. This invokes toJSON once even for a keyed duplicate, which still returns the original entry without writing. For StatusChanged, text validation and the run summary’s status use the serialized payload, not the original object’s status. Header and payload serialization failures retain their original cause in StorageUnavailableError; append reports such a failure only after the lease, key, and sequence checks above. A payload that cannot serialize has no status snapshot to validate, even if its original status contains invalid text.
deleteOwnedRun atomically deletes a run only while the lease’s runId, holderId, and epoch still match; otherwise it rejects with StaleLeaseError and preserves the run and its events. An optional expectSeq must equal the latest sequence or the operation rejects with JournalCasConflictError without deleting anything. Deletion keeps the last epoch so an old lease cannot delete or write a recreated run. deleteRun is an unconditional administrative operation. Inputs and returned records must be detached. close() is idempotent, and every later operation rejects.
Custom journals need only the async contract; no synchronous journal methods are required. Use registerRunJournalConformance(runner, name, open, observeRead) to check a fresh journal per case. Supply a read observer that reports actual page sizes and entries loaded or copied, with restore() and reset() methods, so the suite can verify lazy reads without consuming the whole history.
Precedence is RunOpts.journal (createAgentSession({ journal }) for a session’s workflow tool), non-empty ATOMIC_WORKFLOW_JOURNAL_PATH, workflows.journal, shared top-level storage, then the default ~/.atomic/workflows/journal.sqlite. Nearer settings selections replace whole objects, never merge options or secrets. Relative settings paths resolve against Atomic home. Invalid selected values raise StorageConfigError; storage failures reject the call without selecting memory. Keep the same journal and working directory when resuming stored runs.
Durable writes
Await asynchronous workflow operations before using their results. Atomic commits a successful write before publishing its new state, and shutdown drains accepted journal writes before releasing ownership. A write failure rejects with a typed storage error, not an in-memory fallback. For direct durable-backend use, openJournalDurableBackend with await JournalDurableBackend.open(journal, { ownsJournal: false }), not a constructor. Omit ownsJournal: false to let the backend close the journal. Await commitWorkflowRegistration, commitWorkflowStatus, commitWorkflowStatusTransition, commitPendingPromptAdjustment, commitReset, and commitCheckpoint; use commitAdditiveCheckpointBestEffort for additive writes.
ctx.stage() remains synchronous. For a cached stage, Atomic commits its replay metadata before the next async execution boundary; callers do not need an extra checkpoint or flush. See Storage errors for configuration, driver, and ownership failures.
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.
Every RunSnapshot passed to recordRunStart() must include toolNodes. Use toolNodes: [] when the run has no tool nodes.
snapshot() returns a JSON copy of every run and notice; version increases on every store change. graphSnapshot() returns a cached, size-bounded copy for graph rendering that stays the same object until the store changes. subscribe(listener) calls listener with a fresh snapshot() after each change and returns an unsubscribe function.
Atomic’s runtime store also has prompt, session, and pause/resume methods used by embedded integrations; those controls are not part of the published Store contract shown here.
createCancellationRegistry() / cancellationRegistry
cancellationRegistry is the default singleton. Aborts signal registered controllers and children rather than killing processes.
Static / TSchema
Type builder from typebox.