Workflows are for long-running logic that must survive restart, crash, retry, or a sleeping wait.
Workflow source is normal TypeScript in workflows/.
Step source lives in workflows/steps/.
export default async function aiMove(ctx, input) {
const board = await ctx.step(
"chess.loadBoard",
{ gameId: input.gameId },
{ key: `load:${input.gameId}` }
);
const move = await ctx.step(
"chess.chooseMove",
{ gameId: input.gameId, fen: board.fen },
{ key: `choose:${input.gameId}:${board.ply}` }
);
await ctx.step(
"chess.commitAiMove",
{ gameId: input.gameId, from: move.from, to: move.to },
{ key: `commit:${input.gameId}:${board.ply}` }
);
}
Workflow code is deterministic orchestration. It may branch on
recorded values, call ctx.step, sleep, wait, and
receive signals. Direct database writes, network calls, file
access, provider calls, and random external effects belong in
steps.
Step keys must be stable domain identities. Reusing the same key with a different payload is an idempotency conflict.
Snapshot pinning
Each workflow run records the snapshot it started with.
workflow run -> run_snapshot_id -> .dist/run/<run_snapshot_id>
New builds can activate new snapshots while old runs continue replaying against the code they started with. This avoids downtime and avoids changing workflow semantics halfway through a run.
Durable state
Workflow progress is stored in PgPaw/Postgres:
workflow_runs
workflow_events
workflow_steps
workflow_waits
jobs
stream_chunks
The active JavaScript process is disposable. On restart, the daemon reads durable state, reconstructs the workflow context, replays recorded decisions, and continues from the next missing step.
Determinism boundary
The deterministic workflow context is intentionally small. It coordinates steps, waits, and signals. Side effects happen in steps, and step results are recorded before workflow replay continues.
