Durable step execution (claim, lease, fence, receipts)
Generated from
pattern-library/registry.json— do not edit by hand; change the registry and runbun run patterns:build.
Category: Durable Execution · Tier: deep · Maturity: proven (verified 2026-10-10 @ 91f61a76-dirty) · Risk: medium — leases without fences and app-clock leases are the classic failures; both are checked here
Multi-step work that can be killed halfway survives without a workflow engine: a conditional status machine, one claim, a database-clock lease with a reaper, a fence so a stale owner cannot write, and per-step receipts committed with each step's effect.
When to use: Use for short, few-step work that outlives a request or can be interrupted — outbox delivery, replaying an approved AI plan, driving an external long-running operation.
Requires: postgres
Avoid when
- The work is one statement — a single transaction is already all-or-nothing
- The run needs long sleeps, fan-out or hundreds of steps — that is where a durable-execution platform earns its cost
Alternatives
- A durable-execution platform (Vercel Workflow, Temporal, DBOS) — prefer when runs sleep for days, fan out, or exceed a few dozen steps — and its run state may live outside your database
- A queue library with its own worker (pg-boss, Graphile Worker) — prefer when a long-running worker process is available, which serverless Hobby hosting does not give you
Docs
- docs/blueprint/integrity/execution.md#durable-step-execution — The shared contract across outbox, approved plans and branch resets — and where each deviates (GitHub · GitLab)
Code
src/lib/server/notifications/outbox.ts— SKIP LOCKED claim, attempts as fence, DB-clock backoff, reclaimStaleDeliveries (GitHub · GitLab)src/lib/server/db/ai/proposals.ts— Conditional transitions, DB-clock heartbeat lease, lockExecuting fence, markInterruptedIfStale (GitHub · GitLab)src/lib/server/ai/proposals/execute-proposal.ts— Receipt per step in the step's own transaction; fence checked first (GitHub · GitLab)src/lib/server/dbops/operations.ts— Idempotency key, one-in-flight, status-guarded transitions, poll-only external effect (GitHub · GitLab)
Tests
src/lib/server/notifications/outbox.pglite.test.ts(GitHub · GitLab)src/lib/server/db/ai/proposals.pglite.test.ts(GitHub · GitLab)src/lib/server/ai/proposals/execute-proposal.pglite.test.ts— Includes the fence: a healed run commits no further step (GitHub · GitLab)src/lib/server/dbops/operations.pglite.test.ts(GitHub · GitLab)
Invariants
- Every transition is conditional on the state it leaves; matching no row is a lost race, not an error.
- A lease is always paired with a fence — without one a stalled owner commits behind a terminal status.
- A step's receipt commits in the same transaction as its database effect.
- Step identity is stable across retries (a position in a frozen payload or a row id), never a count of calls made.
- A terminal run answers repeats from its receipts and never re-executes.
- External effects are at-least-once unless the API takes an idempotency key — each implementation says which.
Emulation notes
- Take lease time from the database clock (
now()), the same clock the claim predicate reads; an instance's Date.now() is a second, skewable opinion. - Put the fence in the data, not in the platform: Vercel's maxDuration happens to kill a stalled run, a persistent host does not — lock the run row inside each step's transaction and refuse unless it is still yours.
- Never re-issue an external call you cannot key idempotently; poll the operation you started instead (dbops), or accept duplicates explicitly (notifications).
- Heal lazily on read where nothing else needs the slot — a dead run discovered on the next status request needs no extra cron.
Machine-readable record: durable-step-execution in pattern-library/registry.json.