Use signals
A signal resumes durable work without keeping a step process open. The pausing step records a signal name, correlation ID, optional deadline, resume step, and shared state. A later delivery supplies a JSON object that becomes the resume step’s input.
Declare the pause edge and directive
Section titled “Declare the pause edge and directive”The static pause declaration makes the edge part of the checked agent graph. The returned pauseUntilSignal(...) directive activates that edge at runtime; its signal name and resume target must match the declaration.
import { defineAgent, defineStep, pauseUntilSignal, terminate, type AgentExecutionContext,} from "@sapiom/agent";import { z } from "zod/v4";
interface Shared extends Record<string, unknown> { artifact: { title: string };}
const resumeInput = z.object({ approved: z.boolean(), reviewer: z.string().min(1),});
const requestApproval = defineStep({ name: "requestApproval", next: ["finalize"], pause: { signal: "approval.decision", resumeStep: "finalize" }, async run(_input, ctx: AgentExecutionContext<Shared>) { ctx.shared.set("artifact", { title: "Launch brief" }); return pauseUntilSignal({ signal: "approval.decision", correlationId: ctx.executionId, resumeStep: "finalize", }); },});
const finalize = defineStep({ name: "finalize", next: [], terminal: true, inputSchema: resumeInput, async run(input, ctx: AgentExecutionContext<Shared>) { return terminate({ ...ctx.shared.get("artifact"), approved: input.approved, reviewer: input.reviewer, }); },});
export const agent = defineAgent({ name: "approval-example", entry: "requestApproval", steps: { requestApproval, finalize },});The example uses the execution ID as a per-run correlation ID. When correlationId is omitted from a manual pause directive, the cloud runner fills in the current execution ID before persisting the pause. Set it explicitly when another system needs to store the callback contract, or use a different value that is unique to this waiter—such as an external job ID.
Deliver a human decision
Section titled “Deliver a human decision”Open the paused production run in the Run Inspector. For a manual signal, the Resume run panel prefills the recorded name and correlation ID and builds a payload starting point from the resume step’s input schema.
When the schema has one required boolean decision field, the panel offers Approve and Reject actions. Otherwise, edit the JSON object and select Resume run. A successful delivery resumes the engine; inspect the next step because its schema or author code can still fail.
Call sapiom_dev_agents_signal:
{ "executionId": "<paused-execution-id>", "name": "approval.decision", "correlationId": "<paused-execution-id>", "payload": { "approved": true, "reviewer": "Yash" }}The hosted direct-access server exposes the canonical sapiom_workflow_signal tool with the same four fields. It can deliver a cloud signal but cannot inspect or edit your local agent project. The local @sapiom/mcp server and hosted MCP are distinct surfaces.
The response is { "matched": number }:
1is the expected unique-waiter result;0means no paused run in the tenant currently matches that pair, so a repeated delivery is an idempotent no-op; and- greater than
1means the pair was reused and every matching waiter resumed.
matched reports resume acceptance, not terminal success. Inspect each resumed run for its final result.
Deliver from a webhook handler
Section titled “Deliver from a webhook handler”Keep the Sapiom API key on your server. After verifying the third-party webhook’s own signature, send the normalized object to the workflow signal endpoint:
const executionId = callback.executionId;
const response = await fetch( `https://api.sapiom.ai/v1/workflows/executions/${encodeURIComponent(executionId)}/signals`, { method: "POST", headers: { "content-type": "application/json", "x-api-key": process.env.SAPIOM_API_KEY!, }, body: JSON.stringify({ name: "render.completed", correlationId: callback.jobId, payload: { status: callback.status, assetUrl: callback.assetUrl, }, }), },);
if (!response.ok) { throw new Error(`Signal delivery failed: ${response.status}`);}
const result = (await response.json()) as { matched: number };The payload must be a JSON object. It is parsed through the resume step’s inputSchema before that step runs, so use a stable callback schema and treat an unexpected shape as a failed resumed execution—not as a successful webhook merely because the POST returned matched: 1.
Know which pauses need a person
Section titled “Know which pauses need a person”-
Manual human or webhook signals
Section titled “Manual human or webhook signals”Arbitrary names such as
approval.decisionorrender.completedshow the editable Resume run form. A human, your server, local MCP, or hosted MCP delivers them. -
Automatic capability callbacks
Section titled “Automatic capability callbacks”A handle returned by a dispatched Sapiom capability carries its own result signal and correlation ID. Pass the handle to
pauseUntilSignal(handle, ...); the gateway delivers the callback automatically. Run Inspector shows a read-only Waiting on an automatic callback panel so a person cannot race it with an incomplete payload. -
Timeouts
Section titled “Timeouts”timeoutMsrecords a deadline on the pause. If no signal arrives, the engine terminates the run with a pause-timeout failure; it does not synthesize a fallback payload. Model that fallback as an explicit agent path when the outcome should continue.
Test the boundary honestly
Section titled “Test the boundary honestly”sapiom_dev_agents_run_local auto-resumes every manual pause with {}. It proves the graph and shared-state handoff, but it has no manual-signal payload override. Give local-safe defaults where appropriate, then deploy a test agent and deliver a real cloud signal to verify payload-dependent behavior.
© 2026 Sapiom, Inc.