Reference
Build workflow steps that recover from committed boundaries.
Reference the executable workflow slice, recovery guarantees, idempotency limits, durable waits, hosted-agent polling, human input, and declared branches.
Current Executable Slice
Agents with workflow capability can create, run, and schedule workflows today. Schedules use five-field minute cron with optional timezone and fire through the gateway scheduler with one run per scheduled slot. Workflows can also subscribe to supported events by source/type, including configured GitHub and Slack events plus events emitted by custom webhook receivers. A workflow can be a single bounded awaited wasm-js entrypoint, or a persisted graph of committed wasm-js steps. Each run freezes the workflow definition and policy it started with. Graph steps are the durable path: each step commits before the next one starts, and JS steps can pause by returning a durable wait directive instead of sleeping inside JavaScript. Long-running agent work follows the same model: start a hosted agent run, store its run id in wait output, then poll it after the workflow resumes.
Durability Guarantees and Limits
Programming Model
Durable Waits
Do not keep a JavaScript loop alive while waiting for a remote condition. Run a short check, return a wait directive, and let the workflow engine persist the pause. If next is omitted, the workflow resumes the same step. If the returned value includes output, that output becomes the next input when the wait resumes.
export async function main(ctx, workflow, gateway) {
const ready = await remoteCondition(ctx.input);
if (ready) {
return workflow.next("finish", { ready: true });
}
return workflow.sleep("60s", {
output: { attempt: (ctx.input.attempt ?? 0) + 1 }
});
}The raw object form still works and is what the helpers compile to:
export async function main(ctx) {
const ready = await remoteCondition(ctx.input);
if (ready) {
return { output: { ready: true }, next: "finish" };
}
return {
wait: { delayMs: 60_000 },
output: { attempt: (ctx.input.attempt ?? 0) + 1 }
};
}Durable Agent Runs
Workflows should treat hosted agents as long-running jobs. Start the agent once with a stable idempotency key, return a durable wait, then poll the run after resume. This keeps the workflow recoverable and lets the next step branch from the agent result.
export async function main(ctx, workflow, gateway) {
const requestId = String(ctx.input.requestId);
if (!ctx.input.agentRunId) {
const started = await gateway.call("gateway:agent_runs.start", {
idempotencyKey: `workflow-agent-${requestId}`,
title: "Review report",
prompt: `Review ${ctx.input.reportName} and return launch risks.`
});
if (!started.ok) throw new Error(started.error);
return workflow.sleep("60s", {
output: {
...ctx.input,
agentRunId: started.data.run.id,
attempt: 1
}
});
}
const fetched = await gateway.call("gateway:agent_runs.get", {
runId: ctx.input.agentRunId
});
if (!fetched.ok) throw new Error(fetched.error);
const run = fetched.data.run;
if (run.status === "succeeded") {
return workflow.next("decide", {
agentRunId: run.id,
summary: run.result?.message
});
}
if (run.status === "failed" || run.status === "cancelled") {
return workflow.next("handle_failure", {
agentRunId: run.id,
status: run.status,
error: run.error
});
}
return workflow.sleep("60s", {
output: {
...ctx.input,
attempt: (ctx.input.attempt ?? 0) + 1
}
});
}Human Input Pauses
Workflows can also pause when a package-backed action asks for human input. Approval-shaped pauses appear in the existing approvals surfaces and can satisfy approval policy for the selected followup action. Generic requests use the broader user-input request API for values, choices, or explicit resume signals. In both cases, the run keeps the request, answer, and followup context with the workflow history instead of requiring the agent to invent its own out-of-band checkpoint.
Declared Branches
Branching should be visible in the saved graph, not hidden inside JavaScript. Add routes to a step, then return a route label from the JS result. When a step declares routes, it must return one of those route labels; otherwise the run fails instead of silently taking an invisible default path. The Workflows page can then render the real branch structure before a run starts. Prefer workflow.route(label, output) for new graph steps; the raw { route, output } shape remains supported.
{
"id": "review",
"kind": "wasm-js",
"source": "export function main(ctx, workflow, gateway) { return workflow.route(ctx.input.ok ? 'approved' : 'needs_changes', ctx.input); }",
"entrypoint": "main",
"routes": [
{ "label": "approved", "nextStepId": "publish" },
{ "label": "needs_changes", "nextStepId": "revise" }
]
}