Midwess

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.