Skip to content
Go To Dashboard

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.


What it shows: Two-step linear workflow — input validation with inputSchema (Zod), cross-step state via ctx.shared, and the gototerminate 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:

Terminal window
# 1. Replace index.ts with the template above, then:
npm run typecheck # confirm types
sapiom_dev_orchestrations_check # validate step graph
sapiom_dev_orchestrations_run_local # run end to end, free
# Supply input — e.g. in the run_local payload:
# { "name": "Ada", "excited": true }

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:

Terminal window
sapiom_dev_orchestrations_check
sapiom_dev_orchestrations_run_local # free — sandbox stubbed locally
# Deploy + run to see real spend:
sapiom_dev_orchestrations_link
sapiom_dev_orchestrations_deploy
sapiom_dev_orchestrations_run # billed — real sandbox
sapiom_dev_orchestrations_inspect # watch the spend line

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:

Terminal window
sapiom_dev_orchestrations_check
sapiom_dev_orchestrations_run_local # free — pause auto-resumes with {} locally
# Deploy + run the HITL loop:
sapiom_dev_orchestrations_link
sapiom_dev_orchestrations_deploy
sapiom_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_inspect

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:

Terminal window
sapiom_dev_orchestrations_check
sapiom_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 }'