Every Weft workflow receives a ctx object as the first argument to its run function:
import { defineWorkflow, z } from "@techery/weft-sdk";
export default defineWorkflow(
{
id: "example",
description: "Demonstrate workflow context",
input: z.object({}),
output: z.object({ ok: z.boolean() }),
},
async (ctx) => {
ctx.phase("Work");
ctx.log(`run ${ctx.run.id}`);
return { ok: true };
},
);ctx is the workflow's boundary to agents, humans, the filesystem, processes, HTTP, Git, durable tasks,
time, randomness, and child workflows. This document describes every public method, the engine logic behind
it, and the patterns to use when composing workflows.
The TypeScript contract is defined by Ctx and
UiApi. The runtime behavior is implemented in
buildCtx.
- Mental model
- Surface map
- Agents
- Fan-out and flow control
- Workflow and reusable composition
- Humans and custom UI
- Files, commands, environment, and HTTP
- Git
- Checks, patches, and notes
- Durable workflow tasks
- Durable waits and journaled values
- Structure, logging, and run metadata
- Recommended patterns
Methods that observe or affect the outside world are journaled. On resume, Weft executes the workflow function again from the top and reuses a completed step when its recorded identity still matches. Agent calls, human requests, filesystem reads, commands, HTTP requests, Git operations, checks, task operations, waits, time, randomness, and UUID generation all cross this durable boundary.
Orchestration helpers such as parallel, pipeline, sequence, successes, all, scope, and recipe
do not hide their nested effects inside one opaque step. Each nested ctx call remains independently
journaled and visible in the run tree.
A journaled step's identity includes its kind, inputs, schema, and optional key. Content identity is enough
for a unique one-off call, but a stable key is strongly recommended for repeated, reordered, or similar
calls. If replay cannot unambiguously match a keyless step, Weft reruns it instead of guessing.
Use keys that identify the logical operation, not its current position:
await ctx.agent(`Review ${file}`, {
key: `review:${file}`,
schema: FindingList,
});Human requests, commands, fetches, checks, task mutations, UI presentations, and child workflows also expose keys where call sites benefit from explicit replay identity.
Agent results, human answers, workflow input/output, recipe input/output, signals, and optionally command or HTTP results are validated by their Standard Schema. A TypeScript type alone is not a runtime guarantee. Provider-facing JSON Schema may be a compatible projection, but the original Standard Schema remains the authority when Weft accepts a value.
Workflow source should not call ambient nondeterministic APIs such as Date.now(), Math.random(),
crypto.randomUUID(), raw fetch, process execution, or direct filesystem modules. The gate rejects or
replaces those paths. Use ctx.now(), ctx.random(), ctx.uuid(), ctx.fetch(), ctx.exec()/ctx.bash(),
and ctx.fs.*() so the observed result can be journaled and replayed.
Duration arguments accept milliseconds as a non-negative number or strings such as "250ms", "30s",
"10m", "2h", and "1d".
Paths are normally repository-relative and resolve from ctx.run.cwd. Write-scope globs must be non-empty
POSIX repository-relative patterns: no absolute paths, backslashes, or .. segments.
| Area | Methods and fields |
|---|---|
| Agents | agent, agent.detailed |
| Fan-out and flow | parallel, sequence, pipeline, successes, all |
| Composition | workflow, scope, recipe, step (deprecated) |
| Humans and UI | gate, human.ask, human.approve, human.review, human.review.detailed, ui.render |
| Files and processes | fs.read, fs.glob, fs.stat, exec, bash, env.get, secret |
| HTTP | fetch |
| Git reads | git.status, head, branches, mergeBase, changedSince, diff, log, show, blame, fileAt, snapshot |
| Git writes | git.add, commit, checkout, fetch, pull, push, reset, apply, tag, branch.create, branch.delete, stash.push, stash.pop, stash.drop, clean |
| Verification and patches | check, check.exec, check.fn, check.trust, check.skip, integrate, discard, note |
| Durable tasks | tasks.observe, tasks.upsert, tasks.update, tasks.note, tasks.setCriterion |
| Durable waits | signal, sleep |
| Journaled values | now, random, uuid |
| Structure and metadata | phase, log, budget, run |
const result = await ctx.agent("Find correctness bugs in auth.ts.", {
key: "review:auth",
schema: z.object({
findings: z.array(z.object({ line: z.number(), claim: z.string() })),
}),
provider: "codex",
effort: "high",
timeout: "10m",
});Runs one provider-backed agent step and returns only its schema-validated value.
Important logic:
schemais required. Invalid structured output is sent back to the same session for schema repair; the default repair allowance comes from engine configuration and can be overridden withrepair.- Routing precedence is step options, then workflow defaults, then engine defaults.
provider,model, andeffortcan be selected per step. providerOptionsis namespaced by provider. Only the selected provider's entry is validated and sent.providerRequirementsfails before a paid turn when the provider cannot satisfy required structured-output, permission-hook, or session-resume capabilities.maxTurns,timeout,retry,repair, andonMaxTurnscontrol execution. Retry attempts are separate step attempts; schema repairs happen inside the provider session and all usage is accumulated.onError: "throw"is the default.onError: "null"changes the return type toT | null, records the dropped failure, and durably replays the same suppression instead of retrying it on resume. Cancellation and settlement failures are never suppressed.labelis presentation only.keyis replay identity.
An agent is read-only unless write is declared. A write step runs in its own Git worktree and returns a patch;
it does not mutate the integration tree.
const fixed = await ctx.agent.detailed("Fix the retry loop and nothing else.", {
key: "fix:retry",
schema: z.object({ summary: z.string() }),
write: {
paths: ["src/retry.ts"],
also: ["pnpm-lock.yaml"],
mode: "strict",
},
});write.mode: "warn" is the default: out-of-scope edits are reported but the patch remains eligible for
integration. "strict" quarantines a patch with out-of-scope edits. Use ctx.integrate() to land an accepted
patch or ctx.discard() to close it without landing it. A run cannot complete with unresolved patches.
isolation: "worktree" also isolates a read step. Declaring write implies worktree isolation.
Task context is available only when the workflow declares a task contract and the host has a task tracker.
Use tasks: false to omit it, or pass a selector and an explicit mode. A step may narrow the workflow's
meta.tasks.agentAccess, but cannot exceed it; write mode therefore requires workflow-level write authority:
await ctx.agent("Reassess open parser work.", {
key: "reassess:parser",
schema: Assessment,
tasks: {
mode: "read",
statuses: ["todo", "in_progress", "blocked"],
tags: ["parser"],
limit: 20,
},
});The engine injects a bounded snapshot and, in write mode, accepts validated task operations in the structured agent result. Providers do not receive direct task-store or CLI authority. Operations settle idempotently only after the agent step succeeds.
Agents created by defineAgent() fix a prompt template, output schema, and routing defaults:
const result = await ctx.agent(fileReviewer, { path: "src/auth.ts" }, {
key: "review:auth",
effort: "xhigh",
});The prompt input is validated when the reusable prompt declares an input schema. Invocation options override definition defaults. A reusable agent cannot bake in a call-site key or silently default to nullable output.
Accepts the same direct-prompt and reusable-agent forms, but returns operational metadata:
type DetailedAgentResult<T> = {
value: T;
usage: { input: number; output: number; cacheRead?: number; usd?: number; samples?: number };
files: string[];
patch?: { ref: string; key: string; files: string[]; quarantined?: boolean; outOfScope?: string[] };
attempts: number;
sessionId?: string;
};Use detailed when the workflow needs usage, files, session metadata, or especially a patch for
ctx.integrate()/ctx.discard(). Use plain ctx.agent() when only the typed value matters.
Runs independent lanes and returns Settled<T>[] in input order:
const settled = await ctx.parallel(
files,
(file) =>
ctx.agent(`Review ${file}`, {
key: `review:${file}`,
schema: Review,
}),
{ concurrency: 4, errors: "settle" },
);Each result is { ok: true, value } or { ok: false, error: StepError }.
errors: "settle"is the default and lets the caller choose how to handle lane failures.errors: "throw"waits for every lane to settle, then throws the first failure.concurrencyworks with the item/mapper form or an array of thunks. Eager or mixed task arrays cannot accept a concurrency option in TypeScript; the runtime also rejects casted or JavaScript callers rather than pretend to limit promises that have already started.- The engine enforces its configured fan-out cap and global concurrent-step limit.
- Cancellation propagates instead of becoming an ordinary settled failure.
Prefer the mapper form for most fan-out. Use thunks when each lane has distinct code:
await ctx.parallel(
[
() => ctx.agent("Check API", { key: "check:api", schema: Result }),
() => ctx.agent("Check UI", { key: "check:ui", schema: Result }),
],
{ concurrency: 2 },
);Returns successful values, records every failed lane as a deliberate drop, and preserves the relative order of successes.
const reviews = ctx.successes(await ctx.parallel(files, reviewFile));Use this only when partial results are an intentional workflow policy. It makes tolerance visible in the run report instead of silently filtering errors.
Returns all values in input order or throws the first failure.
const reviews = ctx.all(
await ctx.parallel(files, reviewFile, { concurrency: 4 }),
);Because parallel has already settled all lanes, all does not abandon work in flight. Use it when every lane
is required. errors: "throw" on parallel and a later ctx.all() express similar failure policy; normally
choose one place to enforce it.
Builds an immutable per-item pipeline. Every item passes through its stages independently, so a fast lane can enter stage two before a slow lane finishes stage one.
const confirmed = ctx.all(
await ctx
.pipeline(findings)
.step((finding) => ctx.agent(`Verify ${finding.claim}`, { key: `verify:${finding.id}`, schema: Verdict }))
.filter((verdict) => verdict.real)
.map((_verdict, originalFinding) => originalFinding)
.run({ concurrency: 4, errors: "throw" }),
);Pipeline methods:
.step(fn)awaits a stage and carries its result to the next stage..filter(fn)removes a lane when the verdict is falsy. Filtered lanes do not appear in.run()results..map(fn)is a typed transformation stage; it may be synchronous or asynchronous..run(options?)returns orderedSettledresults for the lanes that were not filtered. It supports the sameconcurrencyanderrorspolicy asparallel.
Every callback also receives the original item and its input index. Builder branches are immutable, so two pipelines derived from a common prefix do not share later stages.
Traverses items sequentially and gives each item a stable phase-scoped context and key builder:
const outputs = await ctx.sequence(
services,
{
key: "deploy-service",
keyOf: (service) => service.id,
phase: (service) => `Deploy ${service.name}`,
},
async (service, item) => {
return item.ctx.workflow(deployService, service, {
key: item.key("deploy"),
});
},
);key is the stable call-site namespace and must be a non-empty string without :. keyOf must return a
unique, non-empty string without : for every item, and local keys have the same restriction.
item.key("deploy") produces deploy-service:<itemKey>:deploy. The default phase is the item key. Separate
sequence calls must use separate key values because replay keys are unique across the whole run.
Use sequence for ordered traversal where stable per-item structure matters. It stops on the first thrown
failure and returns plain Result[], not settled results.
Runs a real child workflow with an independent run, journal, suspension lifecycle, and optionally a delegated budget:
const verdict = await ctx.workflow(verifyFinding, { finding }, {
key: `verify:${finding.id}`,
label: `Verify ${finding.file}:${finding.line}`,
budget: { fraction: 0.2 },
});budget accepts fraction, absolute tokens, absolute usd, or a compatible combination. Child spending
rolls up to the parent. The engine enforces its configured maximum child depth.
An inline child definition needs meta.id or meta.name for stable replay identity. A registry name can be
passed as a string, but passing the imported definition preserves input/output typing. On resume Weft re-enters
the child rather than blindly serving the old outer result; the child's own journal decides which internal
steps remain reusable after code edits.
Use ctx.workflow() when the child needs a durable boundary. Use ctx.recipe() for transparent in-process
composition whose individual effects should remain directly in the parent run.
Returns an immutable context that applies the supported agent, tasks, and parallel defaults. The execution
scope also binds nested namespaces such as human.ask, git.branch.create, and agent.detailed, so an outer
phase remains attached even when the method is reached through a nested property:
const deepReview = ctx.scope({
agent: { provider: "codex", effort: "xhigh", timeout: "20m" },
tasks: { mode: "read", tags: ["security"] },
parallel: { concurrency: 2, errors: "throw" },
});
const results = await deepReview.parallel(files, (file) =>
deepReview.agent(`Review ${file}`, { key: `security:${file}`, schema: Review }),
);Scope defaults merge with a parent scope. Explicit call options win. Agent scope options intentionally exclude
call-specific schema, key, label, onError, and tasks; task authority has its own scope field.
Because the returned handle is immutable and execution-scoped, separate scoped contexts can run concurrently without changing one another.
Runs a defineRecipe() definition transparently:
const summary = await ctx.recipe(summarizeFiles, { files });The recipe input is validated before its function runs and its output is validated afterward. Nested effects use the current phase/scope and remain ordinary journal entries. The recipe call itself is not an independent durability, budget, or suspension boundary.
Runs a legacy defineStep() transparent composition helper. It validates that the value is a step definition
but has no schema-backed input/output boundary. Use defineRecipe() and ctx.recipe() for new code.
Requests approval for an action with a risk tier:
const decision = await ctx.gate({
key: "deploy:payments:production",
action: "Deploy payments to production",
risk: "high",
detail: "Image sha256:abc123; migrations: none",
});
if (!decision.approved) return { deployed: false };The host approval policy first matches action globs, then risk-tier overrides. By default, low is
auto-approved and medium, high, and irreversible ask a person. Irreversible approvals require a generated
confirmation token unless policy auto-approves them. Policy approvals are still journaled.
The result is { approved, note?, answeredBy }, where answeredBy is "human", "policy", or "timeout".
Use gate for risk policy, not for arbitrary typed input; use human.ask for typed questions. Give repeated or
reorderable gates distinct key values so their answers remain replayable after source edits.
Always creates a durable human request and validates the answer:
const lane = await ctx.human.ask({
key: "release:lane",
question: "Choose a release lane",
detail: "Canary receives 5% of traffic.",
schema: z.object({ lane: z.enum(["canary", "full"]) }),
timeout: "2h",
onTimeout: { default: { lane: "canary" } },
});Timeout policies are:
"deny"(also the default): the ask fails with a human-timeout error."escalate": records that the deadline passed and keeps waiting.{ default: rawInput }: validates the supplied raw value through the same schema and returns its output.
A human answer that fails the authoritative schema is rejected and the request reopens for a replacement.
Use a stable key when similar questions may occur more than once or move during edits.
An optional custom input view can stage an answer:
const answer = await ctx.human.ask({
key: "release:decision",
question: "Approve the release plan",
schema: ReleaseDecision,
ui: { view: ReleaseDecisionView, props: { plan, checks } },
});View props and proposed answers must be JSON-compatible. The browser component can only propose a candidate; host-owned controls validate and submit the durable answer.
Asks a person for { approved: boolean, note?: string } without applying risk-tier auto-approval:
const approval = await ctx.human.approve({
key: "publish:report",
action: "Publish the report?",
detail: "Target: internal engineering portal",
timeout: "1d",
onTimeout: "deny",
});Unlike ctx.gate, this method is an explicit human checkpoint. A denied or timed-out approval is returned as
{ approved: false, note? }; workflow code decides what to do next. Approval timeouts may deny, escalate and
keep waiting, or provide an explicit { default: { approved: false, note? } }. The type and runtime reject an
approval-producing timeout default because approval requires a human answer.
Presents a primary artifact or file plus optional immutable artifact attachments, waits for a schema-validated answer, and returns the answer only.
Artifact form:
const answer = await ctx.human.review({
key: "review:plan",
subject: { kind: "artifact", content: plan, mediaType: "text/markdown", label: "Plan" },
attachments: [{ kind: "artifact", content: evidence, label: "Evidence" }],
question: "Review the plan",
schema: z.object({ approved: z.boolean(), note: z.string().optional() }),
});The legacy artifact: string shorthand remains supported. Artifact bytes are stored as blobs and the journal
holds references.
File form:
const answer = await ctx.human.review({
key: "review:release-notes",
subject: { kind: "file", path: "docs/release.md", mode: "edit" },
schema: z.object({ approved: z.boolean() }),
});File paths are repository-relative. mode: "view" is read-only. mode: "edit" lets the operator submit a
draft; Weft applies it only while the file still matches the content hash that was opened, preventing a stale
review from overwriting later edits. Review supports the same timeout and custom-UI options as human.ask.
Performs the same review but returns { answer, subject }. Artifact metadata includes blob ref, SHA-256, size,
media type, and label. File metadata includes path, mode, before/after SHA-256, resulting blob ref and size, and
whether an edit was applied. Use it when later workflow logic needs durable subject provenance.
Publishes a durable, read-only custom presentation:
await ctx.ui.render({
key: "present:release-plan",
slot: "release-plan",
view: ReleasePlanView,
props: { plan, checks },
});view must be a display view created with defineResultView(). props must be JSON-compatible and the
protocol caps serialized props at 512 KiB. key is required replay identity. slot is an optional projection
identity that lets a UI region select its latest presentation.
The method returns void; presentation data is journaled separately from the ordinary step output, which is
null on the wire. Rendering happens only in the browser and cannot mutate workflow values.
Reads a file and returns { content, sha256, size }. The result is journaled, so a resume sees the same bytes
when the step is reused. Prefer repository-relative paths.
Expands one pattern or an array of patterns and returns { paths } sorted lexicographically. node_modules
and .git are ignored. options.cwd changes the glob root and resolves relative to the workflow cwd.
const { paths } = await ctx.fs.glob(["packages/*/src/**/*.ts"], { cwd: "." });Returns { exists, size?, mtimeMs?, isFile?, isDirectory? }. Missing paths and a missing parent are normal
{ exists: false } values. Other failures such as permission errors fail the step rather than masquerading as
absence.
ctx.fs intentionally has no write methods. Agent writes are patch-producing steps; reviewed file edits,
patch integration, Git operations, and explicitly gated commands are the controlled mutation paths.
Executes a program directly without a shell:
const result = await ctx.exec("pnpm", ["test", "--filter", "@acme/api"], {
key: "test:api",
cwd: ".",
timeout: "10m",
});Without a schema it returns { exitCode, stdout, stderr }. A non-zero exit is data, not automatically a thrown
error; inspect exitCode or use ctx.check when pass/fail semantics are the goal. Spawn failures, cancellation,
and timeouts fail the step.
With schema, stdout must be JSON and validate against the schema; the typed value is returned:
const manifest = await ctx.exec("node", ["scripts/manifest.mjs"], {
key: "manifest",
schema: Manifest,
});risk routes the command through ctx.gate semantics before execution. env accepts ordinary strings and
ctx.secret() handles.
Runs a command through /bin/bash. It has the same raw/schema return forms, cwd, timeout, env, risk, and key
behavior as ctx.exec.
Prefer exec for fixed argv because it avoids shell interpolation. Use bash for pipelines, redirection, or
shell syntax that is intentionally part of the operation.
Returns a journaled string | undefined environment value. Because the value is stored in the journal, do not
use this for credentials or sensitive material.
Returns an opaque SecretHandle immediately; it does not reveal the value to workflow code. The engine resolves
the named environment variable only on the live execution path of exec, bash, or fetch, and journals
<redacted> instead.
const token = ctx.secret("SERVICE_TOKEN");
const response = await ctx.fetch("https://api.example.com/v1/jobs", {
headers: { authorization: token },
});Keep the handle opaque. Do not stringify it into command text or HTTP bodies; pass it through supported env
or headers fields.
Performs a journaled HTTP request. Options include method, headers, string body, timeout, and key.
Without a schema it returns { status, headers, body } for any HTTP status. With a schema, Weft requires a 2xx
response, parses the body as JSON, validates it, and returns the typed value.
const job = await ctx.fetch("https://api.example.com/v1/jobs/42", {
key: "job:42",
timeout: "30s",
headers: { authorization: ctx.secret("SERVICE_TOKEN") },
schema: Job,
});If fetchAllow is configured, every redirect hop must match the hostname allow-list. Weft follows at most five
redirects in its policy-aware path. Credential headers and all secret-backed headers are stripped on a
cross-origin redirect. Secrets and allow-list checks occur only during live execution; a replayed response does
not resolve credentials or contact the network.
All Git methods run in ctx.run.cwd and are journaled. Read methods do not ask for approval. Write methods have
a fixed minimum risk tier and pass through the approval policy. A call's risk option may raise that tier but
never lower it.
Returns { branch, clean, staged, unstaged, untracked }.
Returns { sha } for HEAD.
Returns { current, all }.
Returns { sha } for the merge base of two refs.
Returns { files: Array<{ path, status }> }, where status is A, M, D, or R. The comparison is based on
the merge base with HEAD and includes working-tree and untracked changes.
Returns { patch, stats, ref? }. range accepts from, to, and paths; omitted bounds compare HEAD to the
working tree.
Returns { commits } with SHA, author, date, subject, and body. Options accept from, to, paths, and max.
Returns { content } for a Git object/ref.
Returns { lines } with line number, SHA, author, and content. options.lines limits the result to a
[start, end] tuple.
Returns { content } for one path at a ref.
Records the current tracked working-tree state as a reusable commit-like ref without moving HEAD or the
index. A clean tree returns the current HEAD ref. This is a read-classified capture, not a checkout.
| Minimum risk | Operations |
|---|---|
low |
add, commit, branch.create, ordinary checkout, fetch, stash.push, stash.pop, tag |
medium |
pull, soft/mixed reset, apply, stash.drop, discard checkout |
high |
ordinary push |
irreversible |
force push, hard reset, branch delete, clean |
Stages the listed paths.
Commits either the selected paths or the staged state and returns { sha }. On resume, a served completion is
accepted only while the commit still exists in HEAD history.
Checks out a ref. discard: true is a higher-risk path-restoration operation. An ordinary checkout is
re-established on resume when the working tree has moved to a different branch/ref.
Fetches a remote, defaulting to origin.
Pulls the selected/default upstream.
Pushes the selected/default branch. force: true raises the fixed tier to irreversible.
Resets to a ref. Mode defaults to mixed; hard is irreversible.
Applies patch text, optionally with Git's three-way mode. The journal identity stores a hash of the patch rather than duplicating its contents.
Creates a tag and returns the commit SHA it points to. Resume verifies the exact tag namespace and target; it does not allow a same-named branch to stand in for a missing tag.
Creates a branch, optionally checking it out. Resume verifies both branch existence and, when requested, the active checkout.
Deletes a branch. The operation is classified as irreversible even without force.
Pushes a stash entry.
Pops the latest stash entry.
Drops the latest stash entry. This is at least medium risk because the saved state is removed.
Cleans untracked files according to the Git adapter's operation. It is always classified as irreversible.
For agent-produced patches, prefer ctx.integrate() over reconstructing patch landing with low-level Git
methods. integrate understands Weft's patch journal, quarantine state, replay verification, and conflicts.
Runs a reusable check created by defineCheck():
const result = await ctx.check(typecheck, { package: "api" }, {
key: "check:typecheck:api",
policy: "required",
timeout: "10m",
});A check result has status: "pass" | "fail", optional summary/evidence/details, and a disposition of
"executed", "trusted", or "waived".
Reusable checks may run a callback or construct an argv command. Input schemas are validated before execution.
Definitions declare advisory or required; an invocation can strengthen advisory to required but cannot
weaken required. A failed required check may be returned so the workflow can inspect it, but it is also recorded
and prevents a successful run completion.
Invocation policy:
trust: { run, reason }reuses evidence only for a revisioned definition when the named completed run contains a compatible executed pass with the same definition name, revision, and validated-input hash.waive: { reason, issue?, expiresAt? }produces a passing result withdisposition: "waived"and preserves the stated rationale.
Passing a defineCheckSuite() definition runs its members concurrently and returns
{ passed, results: Record<name, CheckResult> }. A parameterized suite validates its input, resolves named
member definitions and inputs, and returns results under those member names. Suite options can set
keyPrefix, strengthen all members to required, override timeouts, and override concurrency.
These remain public for compatibility but reusable definitions are preferred:
await ctx.check("typecheck", { exec: ["pnpm", "typecheck"], required: true });
await ctx.check.exec("typecheck", ["pnpm", "typecheck"], { required: true });
await ctx.check.fn("manifest", async (signal) => validateManifest(signal));
await ctx.check.trust("typecheck", { run: "r-previous", reason: "same artifact" });
await ctx.check.skip("platform-test", "requires macOS");The direct ctx.check(name, options) form requires exactly one source: exec, fn, trustPrior, or skip.
Command checks pass on exit code zero. Callback checks accept a boolean or a structured check result and receive
an abort signal for timeout handling. Legacy callback checks rerun on resume because their closed-over inputs
are not visible in step identity. The exec, fn, trust, and skip properties are deprecated convenience
forms; use defineCheck() plus invocation trust/waive for new workflows.
Lands patches returned by ctx.agent.detailed() or passed as PatchRef values, in array order:
const ledger = await ctx.integrate(fixes, {
order: "sequential",
onConflict: "fail",
});order currently supports only "sequential". The returned ledger contains merged, conflicts,
quarantined, and skipped patch keys.
- Results without a patch are skipped.
- Strict-scope violations are quarantined and never applied.
onConflict: "fail"restores the pre-apply files and throws.onConflict: "ask"lets a person skip the patch, keep conflict markers, or abort.onConflict: "agent"runs a strict in-place resolver limited to the conflicted files and independently verifies that conflict markers are gone.
Integration snapshots support rollback, and resume verifies that previously merged patch content still exists before serving the recorded completion. This is the standard way to land agent changes.
Marks the supplied agent patches or patch refs as deliberately discarded. It does not modify the integration tree. Passing results with no patches is a no-op.
Every produced patch should end in integrate or discard; dangling patches fail run settlement.
Adds a journaled semantic note to the report:
await ctx.note({
kind: "decision",
text: "Kept the compatibility path for one release.",
evidence: "tests/compat.test.ts",
});kind is "decision", "claim", or "risk". Use notes for conclusions worth preserving independently of
logs. evidence is optional prose or a reference meaningful to the reader.
ctx.tasks operates on workflow-scoped durable records. The workflow must declare a stable meta.id and task
contract, and the host must provide a task tracker. All operations are journaled and idempotent.
Task selectors are conjunctive across fields and alternative within a field. They support ids, dedupeKeys,
statuses, tags, relatedFiles, and limit (default 50, host-capped).
Returns a bounded snapshot:
const snapshot = await ctx.tasks.observe(
{ statuses: ["todo", "in_progress"], tags: ["security"], limit: 25 },
{ key: "tasks:security:open" },
);The result is { total, truncated, tasks }. Each task is a bounded summary containing identity, revision,
title, description, lifecycle, priority, tags, dependencies, related files, acceptance criteria, latest note,
typed extensions, and update time.
An observation is replay-stable: a new run reads fresh state, while a resume reuses what that step originally saw. This prevents task changes made during a suspension from silently changing already-decided control flow.
Converges one recurring logical task by workflow-scoped dedupeKey:
await ctx.tasks.upsert({
key: `task:upsert:${finding.fingerprint}`,
dedupeKey: `finding:${finding.fingerprint}`,
set: {
title: finding.claim,
description: finding.evidence,
status: "todo",
priority: "high",
tags: ["code-review"],
relatedFiles: [finding.file],
acceptanceCriteria: ["Regression test added", "Fix merged"],
extensions: { firstSeenSha: sourceSha },
},
note: `Observed in run ${ctx.run.id}`,
});For a new task, omitted optional fields receive create defaults. For an existing task, only supplied fields are
updated; the optional note is appended atomically. key identifies the journal step, while dedupeKey
identifies the persistent logical task.
The legacy overload upsert(dedupeKey, { create, update?, note? }, { key }) remains available when create and
update policy intentionally differ.
Updates selected task fields. acceptanceCriteria replaces the criteria list; matching criteria keep their
state unless resetAcceptance is true. ifRevision enables optimistic concurrency.
Appends a task note, optionally guarded by task revision.
Marks one stable acceptance-criterion ID met or unmet, optionally guarded by revision. Use IDs returned by
observe; criterion text is not the mutation identity.
Task mutations are validated against the workflow's current task contract and applied after the journaled step settles. Typed extension input may differ from stored extension output when the Standard Schema transforms it.
Waits for an externally delivered named signal and returns its schema-validated payload:
const deployment = await ctx.signal(
"deployment.finished",
z.object({ deploymentId: z.string(), status: z.enum(["succeeded", "failed"]) }),
{ timeout: "2h" },
);The incomplete step is reused across resume, so a timeout continues from its original deadline instead of starting over. An invalid delivery is durably rejected, allowing a corrected later signal to proceed. A valid payload is journaled and replayed.
Creates a durable timer. Resume waits only the remaining time from the original schedule; it does not restart the full duration. The engine records the timer firing before the step completes.
Returns the host clock as journaled epoch milliseconds. Replays return the recorded value.
Returns a journaled Math.random() draw in [0, 1). Replays return the same draw.
Returns a journaled random UUID. Replays return the same UUID.
These methods are asynchronous because crossing the journal is the feature. Do not replace them with ambient
globals merely to avoid await.
Announces a phase and returns an immutable context bound to it:
const review = ctx.phase("Review");
await review.agent("Review the parser", { key: "review:parser", schema: Review });Nested phase handles produce paths such as Review / Verify. The name must be non-empty.
Ignoring the return value preserves the legacy statement-style pattern:
ctx.phase("Review");
await ctx.agent(/* ... */);Prefer the returned handle when phases may overlap or run concurrently. Immutable handles prevent one lane's phase from changing a sibling lane's journal grouping.
Emits a workflow log message for live observability and the run record. It returns void and is not a typed
data dependency. Use ctx.note() when a decision, claim, risk, or evidence should be promoted into the semantic
report.
A live read-only view, not a method:
ctx.budget.spent; // { tokens, usd }
ctx.budget.remaining; // { tokens: number | null, usd: number | null }null remaining means that axis is unlimited. The getter reflects current spend, including completed child
workflow usage. Budget checks occur at agent admission and charge boundaries; this view is for workflow policy
and observability, not a reservation.
Read-only metadata for the current run:
ctx.run.id; // durable run ID
ctx.run.cwd; // integration working directory
ctx.run.baseRef; // optional base ref supplied by the host
ctx.run.depth; // 0 for root, increasing in child workflowsUse run.id for provenance and task occurrence notes. Use stable workflow IDs and step keys—not the run ID—when
identifying logic that should recur across runs.
const results = ctx.all(
await ctx.parallel(items, runItem, { concurrency: 4 }),
);const results = ctx.successes(
await ctx.parallel(items, runOptionalItem, { concurrency: 4 }),
);const fixes = ctx.successes(
await ctx.parallel(files, (file) =>
ctx.agent.detailed(`Fix ${file}`, {
key: `fix:${file}`,
schema: FixSummary,
write: { paths: [file], mode: "strict" },
}),
),
);
const accepted = fixes.filter((fix) => shouldLand(fix.value));
const rejected = fixes.filter((fix) => !shouldLand(fix.value));
await ctx.discard(rejected);
const ledger = await ctx.integrate(accepted, { onConflict: "fail" });Use ctx.recipe() for a reusable typed function whose nested steps belong directly to the current run. Use
ctx.workflow() when the unit needs an independent budget, durable contract, run ID, or suspension lifecycle.
Use ctx.human.ask() for typed business input, ctx.human.approve() for an unconditional human checkpoint,
and ctx.gate() when the host's action/risk approval policy should decide whether a person must be asked.
Use ctx.env.get() only for non-sensitive configuration. Use ctx.secret() and pass the handle directly in
exec/bash environment maps or fetch headers for credentials. Never interpolate a secret handle into text.
Give repeated calls stable logical keys. For sequential collections, prefer ctx.sequence() and
item.key(local). For concurrent collections, derive keys from stable item identity rather than array index.