Agent authoring
This is the deep reference for authoring Sapiom agents — the step model and the patterns you reach for once you’re past the Quickstart. Your scaffolded project also ships an AGENTS.md with the same guidance inline, for your coding agent to read locally.
The step model
Section titled “The step model”An agent is one defineAgent({ name, entry, steps }). Each step is a defineStep({ name, next, run }); run(input, ctx) is ordinary async code that returns a directive telling the engine what to do next.
import { defineAgent, defineStep, goto, terminate } from "@sapiom/agent";
const start = defineStep({ name: "start", next: ["finish"], // the steps this one may hand off to async run(input, ctx) { return goto("finish", { greeting: "hello" }); },});
const finish = defineStep({ name: "finish", next: [], terminal: true, // a terminal step ends the agent async run(input, ctx) { return terminate({ done: true }); },});
export const agent = defineAgent({ name: "my-agent", entry: "start", steps: { start, finish },});defineStep accepts:
| Field | Purpose |
|---|---|
name | the step’s id |
next | step names this step may goto (the graph edges) |
terminal | true if this step ends the agent |
run(input, ctx) | your code; returns a directive |
inputSchema | optional Zod schema validating this step’s input |
timeoutMs | optional per-step timeout |
canFail | set true to allow returning fail() |
pause | declares a pause/resume signal (see below) |
Inside run, ctx gives you ctx.input, ctx.shared (a typed cross-step store), ctx.logger, ctx.attempts (this step’s retry count), ctx.executionId, and ctx.sapiom (the capability client).
Control flow & passing data
Section titled “Control flow & passing data”Advance with goto(targetStep, input); end with terminate(output). The value passed to goto becomes the next step’s input. For data several steps need, use ctx.shared (a typed key/value store). The entry input reaches only the entry step’s input — to use any of it in later steps, write it into ctx.shared from the entry step (as compose does below). Validate a step’s input with inputSchema (Zod):
import { defineAgent, defineStep, goto, terminate, type AgentExecutionContext,} from "@sapiom/agent";import { z } from "zod/v4";
interface Shared extends Record<string, unknown> { salutation: string;}
const inputSchema = z.object({ name: z.string().min(1) });
const compose = defineStep({ name: "compose", next: ["format"], inputSchema, async run(input, ctx: AgentExecutionContext<Shared>) { ctx.shared.set("salutation", `Hello, ${input.name}`); return goto("format", {}); },});
const format = defineStep({ name: "format", next: [], terminal: true, async run(_input, ctx: AgentExecutionContext<Shared>) { return terminate({ greeting: `${ctx.shared.get("salutation")}!` }); },});
export const agent = defineAgent<z.infer<typeof inputSchema>, Shared>({ name: "greeting", entry: "compose", steps: { compose, format },});Failure handling & retries
Section titled “Failure handling & retries”There’s no magic auto-retry — you express failure handling explicitly, which keeps it visible in the graph. The common shape is a bounded loop that escalates to a human: a worker step, an evaluate step that branches, and a counter in ctx.shared that caps the retries before pausing for review. The shape (steps abbreviated):
import { defineStep, goto, type AgentExecutionContext } from "@sapiom/agent";
// evaluate → ship on success, else → reconsiderconst evaluate = defineStep({ name: "evaluate", next: ["ship", "reconsider"], async run(input: { passed: boolean }, ctx) { return input.passed ? goto("ship", {}) : goto("reconsider", {}); },});
// reconsider → loop back to the worker, or escalate once we hit the capconst reconsider = defineStep({ name: "reconsider", next: ["work", "escalate"], async run( _input, ctx: AgentExecutionContext<{ attempt: number; maxAttempts: number }>, ) { const attempt = ctx.shared.get("attempt") ?? 0; const max = ctx.shared.get("maxAttempts") ?? 3; return attempt < max ? goto("work", {}) : goto("escalate", {}); },});When you goto back into a step that declares an inputSchema, the payload you pass must still satisfy that schema.
The SDK also gives you finer-grained directives when a step should handle its own failure:
retry({ delayMs })— re-run this step; bound it withctx.attempts.fail(reason, { output })— end the agent as failed (the step must setcanFail: true).timeoutMson a step caps how long itsrunmay take.
Long-running steps: pause & resume
Section titled “Long-running steps: pause & resume”A step’s run completes in one dispatch and can’t block across processes, so a long-running capability (the coding agent) is launched, and the step pauses until it finishes — then a resume step receives the result as its input.
import { defineStep, pauseUntilSignal, terminate, type AgentExecutionContext,} from "@sapiom/agent";import { CODING_RESULT_SIGNAL } from "@sapiom/tools";
interface Shared extends Record<string, unknown> { codingRunId: string;}
const launch = defineStep({ name: "launch", next: ["collect"], pause: { signal: CODING_RESULT_SIGNAL, resumeStep: "collect" }, async run(input: { task: string }, ctx: AgentExecutionContext<Shared>) { const run = await ctx.sapiom.models.coding.launch({ task: input.task }); ctx.shared.set("codingRunId", run.runId); return pauseUntilSignal(run, { resumeStep: "collect" }); },});
const collect = defineStep({ name: "collect", next: [], terminal: true, // The resumed step's input IS the coding run's result. async run(input: { status: string; summary: string | null }, ctx) { return terminate({ status: input.status, summary: input.summary }); },});Key points:
- Declare
pause: { signal, resumeStep }on the launching step and returnpauseUntilSignal(handle, { resumeStep })— the handle carries the signal and correlation id, so you don’t wire them by hand. - The resumed step’s
inputis the run’s result (for a dispatch pause — a launched capability). It crossed a process boundary, so it carries no live handles — stash anything else the resumed step needs inctx.sharedbefore pausing, and re-attach a sandbox from the result’sexecutionEnvironmentif you need one. - For a manual human-gate pause, the production resume payload must be a JSON object and is whatever fires the signal. Under
run_local, a manual gate always auto-resumes with{}; the local runner does not accept a manual-signal payload override, so type a locally tested happy path defensively and test payload-dependent branches through the cloud signal path. - For a human gate, use the manual form and deliver the signal from Run Inspector, Sapiom MCP, or your server. Matching is tenant-scoped by signal name + correlation ID; the delivery’s execution ID frames ownership rather than choosing the waiter:
return pauseUntilSignal({ signal: "my.approval", resumeStep: "finalize", correlationId: ctx.executionId, // makes the awaited signal unique to this run});The runner uses the current execution ID when correlationId is omitted, but setting it explicitly makes an external callback contract easier to store and inspect. See Use signals for exact delivery requests and matched semantics.
Sub-agents
Section titled “Sub-agents”A step can run another deployed agent and await its result — the same launch-and-pause pattern as the coding agent, via ctx.sapiom.agents.
For a quick child run, run inline:
const result = await ctx.sapiom.agents.run({ definition: "enrich-lead", // the child orchestration's deployed slug input: { domain: "acme.com" },});For a long-running child, launch it and pause until it signals — so the parent step doesn’t time out:
import { defineStep, pauseUntilSignal } from "@sapiom/agent";import { AGENTS_RESULT_SIGNAL } from "@sapiom/tools";
const launchChild = defineStep({ name: "launchChild", next: ["useResult"], pause: { signal: AGENTS_RESULT_SIGNAL, resumeStep: "useResult" }, async run(input: { domain: string }, ctx) { const handle = await ctx.sapiom.agents.launch({ definition: "enrich-lead", input: { domain: input.domain }, }); return pauseUntilSignal(handle, { resumeStep: "useResult" }); },});The resumed step’s input is the child’s AgentRunResultPayload — discriminated on status, so branch on "completed" vs "failed" rather than catching an exception. Import the type from @sapiom/tools.
Determinism
Section titled “Determinism”A step body runs once on the happy path and re-runs only on retry (after a throw or a retry()). Don’t rely on a value being recomputed identically across a pause/resume or a retry — capture non-deterministic values (timestamps, ids) once and pass them forward via goto input or ctx.shared.
Testing what you authored
Section titled “Testing what you authored”The local loop needs no Sapiom account or capability request, and it creates no Sapiom capability spend. Author-written import and step code still has ordinary local side effects:
npm run typecheck— confirms everyctx.sapiom.*call and directive you used actually exists.check— typechecks, bundles and importsindex.ts, derives the manifest, and validates the step graph.run_local— runs your real step code with every Sapiom capability stubbed; a dispatched pause auto-resumes with its stub result, while a manual gate auto-resumes with{}.
run_local needs no stubs to start (capabilities return sensible defaults). Add overrides in .sapiom-dev/stubs.json only when a step branches on a specific result:
{ "version": 1, "steps": { "launch": { "models.coding.run": { "status": "completed" } } },}Stub the method your step actually calls — models.coding.launch if you launched it, models.coding.run if you awaited inline. Capability paths use the plural namespace for namespace calls (repositories.list) and the singular handle for handle methods (repository.pushFromSandbox). run_local reports unusedStubs (a key matched nothing — usually a typo or a singular/plural slip) and stubWarnings (a key matched but the value was the wrong shape) — a green run with either non-empty means a stub silently didn’t apply.
See Test locally for precedence, verbatim response values, branch coverage, and a checked failure stub.
Capabilities inside steps
Section titled “Capabilities inside steps”Every step receives a pre-authenticated, tenant-scoped capability client at ctx.sapiom. Its finite, versioned shape comes from the installed @sapiom/tools declarations. Use editor autocomplete and npm run typecheck; an absent method is not callable from a step.
Audio and browser automation are present on ctx.sapiom as ctx.sapiom.speech and ctx.sapiom.browserAutomation. Use the Capabilities overview to choose an implemented surface, and treat the installed declarations as authoritative.
For example, file storage requires the byte count before it creates a presigned upload:
const bytes = new TextEncoder().encode(JSON.stringify({ result: "done" }));
const { fileId, uploadUrl, requiredHeaders } = await ctx.sapiom.fileStorage.upload({ contentType: "application/json", fileName: "report.json", visibility: "private", fileSize: bytes.byteLength, });
await fetch(uploadUrl, { method: "PUT", headers: requiredHeaders, body: bytes,});
const { downloadUrl } = await ctx.sapiom.fileStorage.getDownloadUrl(fileId);The fetch transfer above is ordinary author code. Under Local Run, the ctx.sapiom.fileStorage.* calls are stubbed but that direct network request is not; supply a deliberate test boundary instead of assuming Local Run is a sandbox.
Common typed namespaces include:
| Namespace | Purpose |
|---|---|
sandboxes | Create, attach to, execute in, and destroy ephemeral compute. |
repositories | Create or attach repositories and push work from a sandbox. |
llm | One routed LLM call (run), or a deferred/session call for queued or repeated calls. |
models | An in-server agentic loop, no filesystem (run/launch). |
models.coding | Run inline or launch a coding task for pause/resume. |
fileStorage | Create uploads and short-lived downloads, list files, and change visibility. |
contentGeneration.images / .video | Create image or video output, optionally into file storage. |
speech | Text-to-speech, sound effects, and voices. |
browserAutomation | Screenshots and browser-session lifecycle; connect your own CDP client for interaction. |
search | Web search, page scraping, and typed email lookup. |
database | Provision, inspect, list, and delete on-demand Postgres. |
agents | Run or launch another deployed agent. |
The installed declarations remain authoritative when this summary and a package version differ. The individual capability pages explain which operations are typed, Cloud-MCP-only, or available on both surfaces. See Choose a call surface for when to reach for llm vs. models.run vs. agents.run.
Sapiom MCP tool reference
Section titled “Sapiom MCP tool reference”Sapiom MCP runs locally from @sapiom/mcp. Register it as sapiom-project in Claude Code or Codex:
claude mcp add sapiom-project -- npx -y @sapiom/mcpcodex mcp add sapiom-project -- npx -y @sapiom/mcpThese are MCP tool names, not shell commands:
| Tool | Purpose | Important boundary |
|---|---|---|
sapiom_status | Report the current environment and authentication state. | Read-only. |
sapiom_authenticate | Open browser sign-in and cache the shared credential. | Required before the first cloud action, not before local authoring. |
sapiom_logout | Clear the cached credential. | Does not delete project or Studio state. |
sapiom_dev_agents_scaffold | Create a bundled starter project. | Writes an npm-install-ready project; install dependencies separately. |
sapiom_dev_agents_clone | Materialize one gallery template, fork, or deployed definition. | Gallery and fork clones are source-only until link/deploy. |
sapiom_dev_agents_check | Typecheck, bundle, import, derive the manifest, and validate the graph. | Import-time author side effects remain real. |
sapiom_dev_agents_run_local | Run authored code locally with ctx.sapiom.* stubs. | No Sapiom capability request or spend; ordinary local effects remain real. |
sapiom_dev_agents_link | Resolve or create the hosted definition by name. | Requires authentication. |
sapiom_dev_agents_deploy | Bundle current local source, including uncommitted source, and start a metered cloud build. | Returns the exact build ID; it does not start an agent run. |
sapiom_dev_agents_run | Start a real execution of the runnable build. | Can create metered capability usage. |
sapiom_dev_agents_inspect | Read a cost-agnostic execution or build audit and optionally wait. | Costs are a separate dashboard read. |
sapiom_dev_agents_signal | Deliver an object payload to a matching manual pause. | Dispatched-capability callbacks resume automatically. |
sapiom_dev_agents_schedule | Create a recurring cron or one-off trigger. | A schedule is independent from delayed child dispatch. |
sapiom_dev_agents_schedule_inspect | List an agent’s schedules or inspect one schedule and recent fires. | A non-null execution ID identifies a started run. |
sapiom_dev_agents_schedule_cancel | Disable future unfired occurrences. | Does not cancel a run that already started. |
sapiom_dev_agents_cron_preview | Validate cron and timezone and project UTC occurrences. | Authenticated but non-persistent. |
sapiom_dev_sandbox_configure | Write a validated sandbox-preview resource to sapiom.json. | Local configuration only. |
sapiom_dev_sandbox_check | Validate configured sandbox previews. | Does not deploy. |
sapiom_dev_sandbox_preview | Upload, build, start, and expose a web-app preview. | A failed result carries build/start logs. |
The usual ownership transition is:
scaffold/clone → install → typecheck → check → run_local ↓ authenticate → link → deploy → run → inspect ↕ signalA bundled scaffold can complete the first line without a Sapiom account. A live gallery clone, link, deploy, production run, inspection, signal, or schedule reads or changes organization-owned cloud state and requires authentication.
AI-readable documentation
Section titled “AI-readable documentation”The browser pages and generated agent-readable outputs share this site’s canonical navigation. Fetch the compact index at https://docs.sapiom.ai/llms.txt, the combined corpus at https://docs.sapiom.ai/llms-full.txt, or append .md to a canonical page URL:
curl https://docs.sapiom.ai/agents/authoring.mdcurl https://docs.sapiom.ai/guides/test-locally.mdScaffolded projects already include AGENTS.md and the version-matched sapiom-agent-authoring skill. Read those local files first when they disagree with a newer web page, because they match the packages installed in that project.
© 2026 Sapiom, Inc.