Skip to content
Go To Dashboard

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.

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.

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.

The response is { "matched": number }:

  • 1 is the expected unique-waiter result;
  • 0 means no paused run in the tenant currently matches that pair, so a repeated delivery is an idempotent no-op; and
  • greater than 1 means the pair was reused and every matching waiter resumed.

matched reports resume acceptance, not terminal success. Inspect each resumed run for its final result.

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.

  1. Arbitrary names such as approval.decision or render.completed show the editable Resume run form. A human, your server, local MCP, or hosted MCP delivers them.

  2. 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.

  3. timeoutMs records 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.

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.