Example Templates
Each template below is a complete, runnable defineOrchestration definition written against the published @sapiom/orchestration and @sapiom/tools packages. Scaffold a project, drop in the code, run check and run_local for free, then deploy when you’re ready.
1. Greeting
Section titled “1. Greeting”What it shows: Two-step linear workflow — input validation with inputSchema (Zod), cross-step state via ctx.shared, and the goto → terminate control flow. No capability calls, no spend. The natural starting point for understanding the step model.
Capabilities used: None.
import { type OrchestrationExecutionContext, defineOrchestration, defineStep, goto, terminate,} from "@sapiom/orchestration";import { z } from "zod/v4";
// Typed cross-step store (type alias, not interface — satisfies// Record<string, unknown> implicitly, which the SDK requires).interface GreetingShared extends Record<string, unknown> { salutation: string;}
const inputSchema = z .object({ name: z.string().min(1).describe("Who to greet."), excited: z.boolean().optional().describe("Append an exclamation mark."), }) .meta({ examples: [{ name: "Ada", excited: true }] });
type GreetingInput = z.infer<typeof inputSchema>;
interface ComposeOutput { readonly salutation: string; readonly excited: boolean;}
// Step 1 — validate input, build the salutation, stash it in shared.const compose = defineStep({ name: "compose", next: ["format"] as const, inputSchema, async run(input, ctx: OrchestrationExecutionContext<GreetingShared>) { const salutation = `Hello, ${input.name}`; ctx.shared.set("salutation", salutation); ctx.logger.info("compose: built salutation", { name: input.name }); return goto("format", { salutation, excited: input.excited ?? false, } satisfies ComposeOutput); },});
// Step 2 — apply the excitement flag and finish.const format = defineStep({ name: "format", next: [] as const, terminal: true, async run(input: ComposeOutput) { const greeting = input.excited ? `${input.salutation}!` : `${input.salutation}.`; return terminate({ greeting }); },});
export const orchestration = defineOrchestration<GreetingInput, GreetingShared>({ name: "greeting", entry: "compose", steps: { compose, format },});Scaffold and run:
# 1. Replace index.ts with the template above, then:npm run typecheck # confirm typessapiom_dev_orchestrations_check # validate step graphsapiom_dev_orchestrations_run_local # run end to end, free
# Supply input — e.g. in the run_local payload:# { "name": "Ada", "excited": true }2. Compute Spend
Section titled “2. Compute Spend”What it shows: Single capability call — ctx.sapiom.sandboxes.create() + sandbox.exec() + teardown in a finally block. This is the minimal “one real spend line” workflow: the sandbox is a real compute instance, the exec runs your command in it, and the cost is attributed to this execution. Shows the create → exec → destroy lifecycle and the sandbox name convention for uniqueness across retries.
Capabilities used: ctx.sapiom.sandboxes (billed per execution).
import { type OrchestrationExecutionContext, defineOrchestration, defineStep, terminate,} from "@sapiom/orchestration";import { z } from "zod/v4";
const inputSchema = z .object({ command: z .string() .min(1) .default('echo "hello from a real Sapiom sandbox" && uname -a') .describe("Shell command to run inside the provisioned sandbox."), }) .meta({ examples: [{ command: 'echo "hello from a real Sapiom sandbox" && uname -a' }], });
type ProvisionInput = z.infer<typeof inputSchema>;
// Sandbox names must be lowercase alphanumeric + hyphens, 2–63 characters.// Append the attempt count so retries don't collide with a prior attempt's sandbox.function sandboxName(executionId: string, attempt: number): string { const id = executionId.toLowerCase().replace(/[^a-z0-9-]/g, "-"); return `compute-spend-${id}-a${attempt}`.slice(0, 63).replace(/-+$/, "");}
const provision = defineStep({ name: "provision", next: [] as const, terminal: true, inputSchema, async run(input: ProvisionInput, ctx: OrchestrationExecutionContext) { const name = sandboxName(ctx.executionId, ctx.attempts); ctx.logger.info("provision: creating sandbox", { name });
// Real, metered compute. xs tier + short TTL — the TTL is a backstop reaper. const sandbox = await ctx.sapiom.sandboxes.create({ name, tier: "xs", ttl: "5m", }); ctx.logger.info("provision: sandbox created", { name: sandbox.name, workspaceRoot: sandbox.workspaceRoot, });
try { const result = await sandbox.exec(input.command); ctx.logger.info("provision: command finished", { exitCode: result.exitCode }); return terminate({ sandboxName: sandbox.name, command: input.command, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, note: "Real compute via ctx.sapiom.sandboxes — cost is attributed to this run.", }); } finally { // Best-effort teardown. The TTL reaps it regardless — never fail the run // on teardown. try { await sandbox.destroy(); ctx.logger.info("provision: sandbox destroyed", { name: sandbox.name }); } catch (err) { ctx.logger.warn("provision: sandbox teardown failed (non-fatal; TTL reaps)", { err }); } } },});
export const orchestration = defineOrchestration<ProvisionInput>({ name: "compute-spend", entry: "provision", steps: { provision },});Scaffold and run:
sapiom_dev_orchestrations_checksapiom_dev_orchestrations_run_local # free — sandbox stubbed locally
# Deploy + run to see real spend:sapiom_dev_orchestrations_linksapiom_dev_orchestrations_deploysapiom_dev_orchestrations_run # billed — real sandboxsapiom_dev_orchestrations_inspect # watch the spend line3. Approval Workflow
Section titled “3. Approval Workflow”What it shows: Human-in-the-loop (HITL) approval gate using pauseUntilSignal. The workflow pauses after generating a summary and resumes only when an approver fires a named signal — approve or reject. Demonstrates how ctx.shared state survives across the pause boundary, how the resume step validates its signal payload with inputSchema, and the fail() directive for a rejected outcome.
Capabilities used: None (the summary step uses a deterministic placeholder — the LLM capability is not yet on ctx.sapiom).
import { type OrchestrationExecutionContext, defineOrchestration, defineStep, fail, goto, pauseUntilSignal, terminate,} from "@sapiom/orchestration";import { z } from "zod/v4";
// type alias (not interface) so the SDK's Record<string, unknown> constraint is met.type ApprovalShared = { topic: string; summary: string;};
// ---------------------------------------------------------------------------// Schemas// ---------------------------------------------------------------------------const prepInputSchema = z .object({ topic: z.string().min(1).describe("What the workflow should summarize."), }) .meta({ examples: [{ topic: "the Sapiom workflows engine" }] });
type PrepInput = z.infer<typeof prepInputSchema>;
const finalizeInputSchema = z .object({ approved: z.boolean().describe("Whether the summary was approved."), note: z.string().optional().describe("Optional reviewer note."), }) .meta({ examples: [{ approved: true }] });
type FinalizeInput = z.infer<typeof finalizeInputSchema>;
const APPROVAL_SIGNAL = "demo.approval";
// ---------------------------------------------------------------------------// Steps// ---------------------------------------------------------------------------
// Step 1 — validate input, stash topic in shared, advance to enrich.const prep = defineStep({ name: "prep", next: ["enrich"] as const, inputSchema: prepInputSchema, async run(input, ctx: OrchestrationExecutionContext<ApprovalShared>) { ctx.shared.set("topic", input.topic); ctx.logger.info("prep: ready", { topic: input.topic }); return goto("enrich", { topic: input.topic }); },});
// Step 2 — build a deterministic summary (swap in an LLM call once available).const enrich = defineStep({ name: "enrich", next: ["approval"] as const, async run(input: { topic: string }, ctx: OrchestrationExecutionContext<ApprovalShared>) { const summary = `${input.topic.charAt(0).toUpperCase()}${input.topic.slice(1)}: a deterministic summary placeholder.`; ctx.shared.set("summary", summary); ctx.logger.info("enrich: generated summary", { topic: input.topic }); return goto("approval", { topic: input.topic, summary }); },});
// Step 3 — HITL gate: pause until a human fires APPROVAL_SIGNAL.// correlationId = ctx.executionId makes this signal unique to this run.const approval = defineStep({ name: "approval", next: ["finalize"] as const, pause: { signal: APPROVAL_SIGNAL, resumeStep: "finalize" }, async run( input: { topic: string; summary: string }, ctx: OrchestrationExecutionContext<ApprovalShared>, ) { const correlationId = ctx.executionId; ctx.logger.info("approval: pausing for signal", { correlationId }); return pauseUntilSignal({ signal: APPROVAL_SIGNAL, resumeStep: "finalize", correlationId, output: { awaitingApprovalFor: input.topic, signal: APPROVAL_SIGNAL, correlationId, }, }); },});
// Step 4 — resume target. Its inputSchema validates the signal payload.// Reads topic + summary back from shared (they survived the pause).const finalize = defineStep({ name: "finalize", next: [] as const, terminal: true, canFail: true, inputSchema: finalizeInputSchema, async run(input: FinalizeInput, ctx: OrchestrationExecutionContext<ApprovalShared>) { const topic = ctx.shared.get("topic"); const summary = ctx.shared.get("summary");
if (!topic || !summary) { return fail("shared state missing after resume", { output: { topic: topic ?? "", summary: summary ?? "", approved: input.approved }, }); }
const output = { topic, summary, approved: input.approved, note: input.note ?? null };
if (!input.approved) { ctx.logger.info("finalize: summary rejected", { topic }); return fail("summary rejected by approver", { output }); }
ctx.logger.info("finalize: summary approved", { topic }); return terminate(output); },});
export const orchestration = defineOrchestration<PrepInput, ApprovalShared>({ name: "approval-workflow", entry: "prep", steps: { prep, enrich, approval, finalize },});Scaffold and run:
sapiom_dev_orchestrations_checksapiom_dev_orchestrations_run_local # free — pause auto-resumes with {} locally
# Deploy + run the HITL loop:sapiom_dev_orchestrations_linksapiom_dev_orchestrations_deploysapiom_dev_orchestrations_run --input '{ "topic": "the Sapiom workflows engine" }'# execution pauses at `approval` — fire the signal to resume:sapiom_dev_orchestrations_signal \ --name "demo.approval" \ --correlationId "<executionId>" \ --payload '{ "approved": true, "note": "Looks good." }'sapiom_dev_orchestrations_inspect4. Code-Fix Loop
Section titled “4. Code-Fix Loop”What it shows: The flagship composition — all four primitives in one workflow: (1) a real sandbox capability call, (2) a branch on the result, (3) a bounded retry loop guarded by a counter in ctx.shared, and (4) a HITL escalation gate when the loop exhausts. Every attempt runs real metered compute attributed to the execution; the pass/fail verdict is a stand-in for a real test suite (swap in your actual npm test command). Condense or extend as needed.
Capabilities used: ctx.sapiom.sandboxes (billed per attempt, each attempt creates and destroys one sandbox).
import { type OrchestrationExecutionContext, defineOrchestration, defineStep, fail, goto, pauseUntilSignal, terminate,} from "@sapiom/orchestration";import { z } from "zod/v4";
const REVIEW_SIGNAL = "code-fix.human-review";
interface CodeFixShared extends Record<string, unknown> { task: string; attempt: number; maxAttempts: number; /** Tests "pass" once attempt >= this — stand-in for the real test verdict. */ passOnAttempt: number; lastOutput: string | null; /** Sandbox names provisioned across all attempts — audit trail. */ sandboxes: string[];}
const prepInputSchema = z .object({ task: z .string() .min(1) .default("Fix the failing test in the billing module") .describe("What to fix."), maxAttempts: z .number() .int() .min(1) .max(10) .default(3) .describe("Max fix attempts before escalating to a human."), passOnAttempt: z .number() .int() .min(1) .default(2) .describe( "Tests pass on this attempt (set > maxAttempts to force human escalation).", ), }) .meta({ examples: [{ task: "Fix the failing billing test", maxAttempts: 3, passOnAttempt: 2 }], });
type PrepInput = z.infer<typeof prepInputSchema>;
const reviewDecisionSchema = z.object({ approved: z .boolean() .describe("Approve the work so far (true) or reject it (false)."), note: z.string().optional().describe("Reviewer note."),});type ReviewDecision = z.infer<typeof reviewDecisionSchema>;
// Sandbox name: lowercase alphanumeric + hyphens, bounded to 2–63 chars.// Append attempt count so retries don't collide.function sandboxName(executionId: string, attempt: number): string { const id = executionId.toLowerCase().replace(/[^a-z0-9-]/g, "-"); return `code-fix-${id}-a${attempt}`.slice(0, 63).replace(/-+$/, "");}
// ---------------------------------------------------------------------------// prep — validate input, seed shared state.// ---------------------------------------------------------------------------const prep = defineStep({ name: "prep", next: ["attempt-fix"] as const, inputSchema: prepInputSchema, async run(input: PrepInput, ctx: OrchestrationExecutionContext<CodeFixShared>) { ctx.shared.set("task", input.task); ctx.shared.set("attempt", 0); ctx.shared.set("maxAttempts", input.maxAttempts); ctx.shared.set("passOnAttempt", input.passOnAttempt); ctx.shared.set("lastOutput", null); ctx.shared.set("sandboxes", []); ctx.logger.info("prep: starting code-fix loop", { task: input.task, maxAttempts: input.maxAttempts, }); return goto("attempt-fix", {}); },});
// ---------------------------------------------------------------------------// attempt-fix — real sandbox runs the work + test command.// Replace the echo command with your actual test runner (e.g. `npm test`).// ---------------------------------------------------------------------------const attemptFix = defineStep({ name: "attempt-fix", next: ["evaluate"] as const, timeoutMs: 120_000, async run(_input: unknown, ctx: OrchestrationExecutionContext<CodeFixShared>) { const attempt = (ctx.shared.get("attempt") ?? 0) + 1; ctx.shared.set("attempt", attempt); const passOnAttempt = ctx.shared.get("passOnAttempt") ?? 2;
const name = sandboxName(ctx.executionId, attempt); ctx.logger.info("attempt-fix: provisioning sandbox", { name, attempt }); const sandbox = await ctx.sapiom.sandboxes.create({ name, tier: "xs", ttl: "5m", }); ctx.shared.set("sandboxes", [ ...(ctx.shared.get("sandboxes") ?? []), sandbox.name, ]);
try { // Replace with your real fix + test command: const result = await sandbox.exec( `echo "attempt ${attempt}: running checks" && uname -s`, ); // Stand-in verdict: tests "pass" once attempt >= passOnAttempt. // In production: check result.exitCode (0 = pass, non-zero = fail). const passed = attempt >= passOnAttempt; ctx.shared.set("lastOutput", result.stdout.trim()); ctx.logger.info("attempt-fix: checks finished", { attempt, exitCode: result.exitCode, passed, }); return goto("evaluate", { passed, output: result.stdout.trim(), attempt }); } finally { try { await sandbox.destroy(); } catch (err) { ctx.logger.warn("attempt-fix: sandbox teardown failed (non-fatal; TTL reaps)", { err }); } } },});
// ---------------------------------------------------------------------------// evaluate — branch on the real test result.// ---------------------------------------------------------------------------const evaluate = defineStep({ name: "evaluate", next: ["ship", "reconsider"] as const, async run( input: { passed: boolean; output: string; attempt: number }, ctx: OrchestrationExecutionContext<CodeFixShared>, ) { if (input.passed) { ctx.logger.info("evaluate: tests green → ship", { attempt: input.attempt }); return goto("ship", { attempt: input.attempt }); } ctx.logger.info("evaluate: tests red → reconsider", { attempt: input.attempt }); return goto("reconsider", { output: input.output }); },});
// ---------------------------------------------------------------------------// reconsider — bounded loop guard: retry, or escalate once the cap is hit.// ---------------------------------------------------------------------------const reconsider = defineStep({ name: "reconsider", next: ["attempt-fix", "escalate"] as const, async run( _input: { output?: string }, ctx: OrchestrationExecutionContext<CodeFixShared>, ) { const attempt = ctx.shared.get("attempt") ?? 0; const maxAttempts = ctx.shared.get("maxAttempts") ?? 3; if (attempt < maxAttempts) { ctx.logger.info("reconsider: looping to retry", { attempt, maxAttempts }); return goto("attempt-fix", {}); } ctx.logger.info("reconsider: max attempts reached → escalate", { attempt, maxAttempts, }); return goto("escalate", {}); },});
// ---------------------------------------------------------------------------// escalate — HITL: pause until a human reviews. Survives restarts.// ---------------------------------------------------------------------------const escalate = defineStep({ name: "escalate", next: ["resolve"] as const, pause: { signal: REVIEW_SIGNAL, resumeStep: "resolve" }, async run(_input: unknown, ctx: OrchestrationExecutionContext<CodeFixShared>) { const attempt = ctx.shared.get("attempt") ?? 0; ctx.logger.info("escalate: pausing for human review", { attempt }); return pauseUntilSignal({ signal: REVIEW_SIGNAL, resumeStep: "resolve", correlationId: ctx.executionId, output: { awaitingReview: true, attempts: attempt, lastOutput: ctx.shared.get("lastOutput"), signal: REVIEW_SIGNAL, correlationId: ctx.executionId, }, }); },});
// ---------------------------------------------------------------------------// ship — terminal: tests passed.// ---------------------------------------------------------------------------const ship = defineStep({ name: "ship", next: [] as const, terminal: true, async run( input: { attempt: number }, ctx: OrchestrationExecutionContext<CodeFixShared>, ) { ctx.logger.info("ship: done", { attempt: input.attempt }); return terminate({ status: "shipped", attempts: input.attempt, sandboxes: ctx.shared.get("sandboxes"), note: "Tests green. Each attempt ran real metered compute attributed to this run.", }); },});
// ---------------------------------------------------------------------------// resolve — terminal: a human approved or rejected the escalated run.// ---------------------------------------------------------------------------const resolve = defineStep({ name: "resolve", next: [] as const, terminal: true, canFail: true, inputSchema: reviewDecisionSchema, async run( input: ReviewDecision, ctx: OrchestrationExecutionContext<CodeFixShared>, ) { const attempts = ctx.shared.get("attempt") ?? 0; if (!input.approved) { ctx.logger.warn("resolve: human rejected", { attempts, note: input.note }); return fail("rejected by reviewer after max fix attempts", { output: { status: "rejected", attempts, note: input.note ?? null }, }); } ctx.logger.info("resolve: human approved", { attempts }); return terminate({ status: "approved-after-review", attempts, sandboxes: ctx.shared.get("sandboxes"), note: input.note ?? null, }); },});
export const orchestration = defineOrchestration<PrepInput, CodeFixShared>({ name: "code-fix-loop", entry: "prep", steps: { prep, "attempt-fix": attemptFix, evaluate, reconsider, escalate, ship, resolve, },});Scaffold and run:
sapiom_dev_orchestrations_checksapiom_dev_orchestrations_run_local # free — sandboxes stubbed; the happy path (passOnAttempt <= maxAttempts) runs to completion
# Adjust passOnAttempt to control how many sandbox charges you incur:sapiom_dev_orchestrations_run --input '{ "task": "Fix billing test", "maxAttempts": 3, "passOnAttempt": 1 }'sapiom_dev_orchestrations_inspect
# If escalated (passOnAttempt > maxAttempts), fire the resolve signal:sapiom_dev_orchestrations_signal \ --name "code-fix.human-review" \ --correlationId "<executionId>" \ --payload '{ "approved": true }'