Compiling Workflows into Databases: Durable Execution Without an Orchestrator

2026-07-2815 min read

Most teams reach for an external orchestrator the moment a job needs to survive a crash. Jeremy Edberg and Qian Li argue that this instinct is backwards: the coordinator you add to increase reliability is itself a new thing that can fail, a new hop of latency, and a new piece of infrastructure to operate. Their thesis is that workflows are data, and databases have been very good at handling data for fifty years. If you checkpoint workflow state into the database you already run, you get durable execution as a library — no separate worker pool, no separate orchestration service.

Edberg is CEO of DBOS and was the founding reliability engineer at Netflix; Li co-founded DBOS and started the project as a Stanford grad student. They presented this 49-minute talk at QCon San Francisco, and InfoQ published the recording and transcript on July 23, 2026. These notes report what the speakers presented; where I add context or push back, the text says so explicitly.

What You Will Learn

  • What durable execution means and why AI workloads made it urgent.
  • How checkpointing workflows and steps into two ordinary tables produces exactly-once execution and crash recovery.
  • How to implement the core run_workflow and run_step wrappers yourself.
  • How to build durable queues on the same table using FOR UPDATE SKIP LOCKED.
  • How to run cron schedules across many workers with no leader election.
  • What "workflow fork" is and why it makes production bug fixes cheaper.
  • The real limits of the library approach: per-language implementations, database write throughput, and the determinism contract it imposes on you.

The Problem: Systems Built for Two Latency Regimes

Edberg opens with a familiar architecture. To ingest millions of documents so an AI can query them, you build a downloader service, a queue in RabbitMQ or Kafka, a processing worker, and a coordinator to tie them together. Then you ask what happens when it breaks — and every one of those pieces breaks. Models return wrong answers or none, downloads from the internet fail routinely, coordinators crash, and a user may ask a follow-up question three weeks later.

His framing of why this hurts more now is worth sitting with. For roughly thirty years we built tooling for two regimes: interactive requests returning in under a second, and batch jobs nobody waits on. AI inference sits awkwardly in between — responses take seconds, a human is waiting, and the workflow may need to stay alive for hours or days. Failures are also newly expensive, since inference costs money per call and restarting from the beginning burns budget as well as time. Meanwhile orchestration ends up scattered across microservices, coordinators, and databases, and the most expensive operation in a distributed system is moving data around.

The visibility complaint is equally practical. With a typical queue-based solution it is hard to see what is enqueued, what failed, and what retried; you either write a lot of bespoke bookkeeping code or fly blind. Edberg's tally against the external-coordinator pattern is longer still: extra latency on every hop, more infrastructure to run, application logic rewritten to fit the framework's worker and producer roles, API-call overhead, and vendor lock-in.

Durable Execution in Two Primitives

Li's reduction of the problem is the core of the talk. Strip away the variety of durable execution engines on the market and only two mechanisms remain:

  1. Checkpointing — persist workflow and step state to a durable store.
  2. Exactly-once execution — on recovery, resume from the last completed step rather than repeating work.

Her analogy is a video game autosave. If someone pulls the power cord, you restart from the last save point instead of the opening level. The "magic" is just a database; DBOS builds on Postgres but Li presents the idea as universal across databases.

The user-facing API surface is deliberately tiny. You call register_workflow to mark a function as an orchestration workflow, and register_step to mark functions as steps (where you can also configure retries and exponential backoff). Your workflows and steps stay ordinary functions in ordinary code. The library then swaps in wrapped versions that do the checkpointing.

The Two Tables

State lives in two tables in the reference implementation:

Table Holds
workflow_status Workflow ID, workflow name, status, inputs, outputs
step_outputs Per-step output checkpoints, keyed back to the workflow ID

step_outputs carries a foreign key to workflow_status.workflow_id. That is the entire persistence model.

The Three Wrappers

Li walks through the code in four-step chunks. The workflow wrapper generates a workflow ID (think of it as an idempotency key), writes that ID plus the inputs to workflow_status with status pending, invokes the real function as a normal call in your language, then captures the result or the thrown error and checkpoints it back with a terminal status.

The step wrapper retrieves the workflow invocation ID and its own step ID from context, then does the load-bearing check: if a checkpoint already exists for this step, return it instead of executing. Otherwise it runs the function and checkpoints the output.

Recovery lists all pending workflows from the database, retrieves each workflow's recorded inputs, and looks up the function pointer in a map populated at registration time. It then re-runs the workflow with its original ID. The step wrapper's checkpoint check does the rest: completed steps return their stored results immediately, and execution effectively resumes at the first incomplete step.

Architecture And Data Flow

flowchart TD
    A[Client calls registered workflow] --> B[Workflow wrapper generates workflow ID]
    B --> C[(workflow_status: insert inputs, status = pending)]
    C --> D[Invoke workflow function in-process]
    D --> E{Step wrapper: checkpoint exists?}
    E -- yes --> F[Return stored output, skip execution]
    E -- no --> G[Execute step function]
    G --> H[(step_outputs: insert step result)]
    F --> I{More steps?}
    H --> I
    I -- yes --> E
    I -- no --> J[(workflow_status: write output, mark complete)]
    K[Process crash or OOM] -.-> L[Recovery scans pending workflows]
    L --> M[Reload inputs and function pointer]
    M --> D

The cost model falls out of this diagram: two database writes per workflow and one per step. Because the library runs in-process, and you typically co-locate application servers with the database, each of those writes is a few milliseconds. The speakers contrast this with an external coordinator, where every step incurs round trips to the orchestrator and back.

They report a comparison against AWS Step Functions: workflows of increasing step count, each run 1,000 times, plotting average latency. Step Functions latency grows substantially with step count because each step adds hundreds of milliseconds even when the step body takes a few milliseconds, while the library approach adds only checkpoint overhead. Treat this as a vendor's benchmark of its own product — the mechanism is credible, but the numbers are theirs.

Queues Without a Queue System

Running workflows synchronously is not enough; teams want to enqueue thousands of tasks and drain them in the background under rate limits. Rather than add a queueing system, Li extends workflow_status with queue_name, created_at, started_at, and priority columns. Queues, in her framing, are just a way to group and order workflows — which is exactly what a relational table with an ORDER BY does.

This buys three things for free. Durability is inherent, because enqueueing is a database write and survives a crash. Ordering policies become sort keys: sort by priority for priority queues, by created_at for FIFO. Rate limiting becomes an aggregate query — count workflows started in the last minute and decide whether to start more.

The hard part is that with no central orchestrator, every worker polls the same table with the same SELECT ... WHERE status = 'ENQUEUED' ORDER BY created_at query. With a handful of workers this is fine. With hundreds, every worker targets the same oldest row and they pile up in lock contention, which degrades the whole database. The fix is Postgres's FOR UPDATE SKIP LOCKED, which does two things at once: it locks the rows a worker selects so no one else can claim them, and it skips already-locked rows rather than waiting on them. Worker A takes row one, worker B skips it and takes row two, and so on. Li notes this is the standard answer you find if you search how to build a queue on a database — the contribution is recognizing that the workflow table can be the queue table.

Decentralized Cron

Scheduled workflows have the same leaderless problem. The approach: every worker runs the same cron scheduler, and the scheduled time becomes the workflow ID. Because workflow ID carries a primary key constraint, only one worker's insert can succeed; the rest fail the uniqueness check and skip. Correctness comes from the database, not from coordination.

Correctness is not the whole story, though. If hundreds of workers all wake at 5 p.m., you get a vertical spike of highly contending writes that Li says can cause outages on its own. The mitigation is random jitter on the sleep, so workers wake milliseconds to seconds apart. The first writes the row; later workers read the row, see it exists, and skip. Since reads are far cheaper than contended writes, the spike flattens. This is a nice example of a pattern worth internalizing: a uniqueness constraint gives you safety, but you still need jitter to get liveness and performance.

Forking a Workflow to Fix a Bug

Because all state is queryable, workflow management becomes SQL: list workflows, search by time window or criteria, cancel, or re-mark as pending and resume. The most interesting capability is fork, which restarts a workflow from a chosen step.

Li's analogy is Git branching. Suppose workflow V1 has three steps, step two has a bug, and step three fails as a result. Forking from step two creates a new workflow ID (V1-fixed) and copies the original workflow inputs and step one's output rows to it. You then run the workflow function with the new code under the new ID; the checkpoint check skips step one and execution resumes at the fixed step two. The old history is preserved. In an AI context this is a direct cost saving — an expensive inference already recorded in step one is not paid for twice.

Why the Library Packaging Matters

The speakers treat "library, not service" as the source of most practical benefits, and several are easy to overlook.

It gets along with what you already have. Adding durability should not mean refactoring an application to fit a framework's shape. You install the library, register workflows and steps, and invoke them. Because the approach is unopinionated it composes with FastAPI, Spring Boot, LangChain, and similar frameworks rather than competing with them; Transact is already integrated with tools such as Pydantic AI.

Operations stay where they are. The library runs in-process, so there are no workers or orchestration servers to manage — you deploy onto the same Kubernetes cluster, serverless functions, or application servers you already run.

Testing uses your existing tooling. Because a workflow is a function in your language rather than a remote service, your usual mocking and test frameworks apply directly, which is simpler than stubbing an external coordinator.

Everything is observable through SQL. Listing pending, enqueued, and completed workflows, building dashboards, shipping built-in queries, or running anomaly detection for out-of-range inputs or PII are all just queries. Edberg frames this as a compliance advantage too: nothing leaves your infrastructure, and every call with its inputs and outputs is auditable after the fact.

The DBOS reference implementations are MIT-licensed and available for Python, TypeScript, Go, and Java, and the speakers invite readers to build their own rather than adopt theirs.

Trade-offs And Limitations

The speakers split the downsides into "the bad" (fundamental) and "the ugly" (implementable challenges, covered above).

One library per language. Good developer experience means implementing the same functionality separately per ecosystem, because expectations differ — sync and async Python functions, Spring Boot dependency injection in Java. The schema and core SQL are shared, so the duplicated work is mostly ergonomics, but if you build this yourself you pay that cost too.

Database-bound scalability. You scale by adding replicas pointing at the same database, so write throughput becomes the ceiling — two writes per workflow plus one per step. Li's counterargument is that this bottleneck is well studied: large Postgres instances handle tens of thousands of transactions per second, companies including Figma, Notion, and OpenAI ran for years on a single write primary (her anecdote, not a benchmark), and sharding or distributed Postgres exists beyond that. It remains a genuine architectural ceiling that an external orchestrator with its own scaling story does not share.

Determinism and idempotency are your job. Asked whether exactly-once depends on steps being idempotent, Li confirmed it: workflows must be deterministic so checkpoints line up on replay, and steps must be idempotent so re-execution is safe. Edberg preempted the obvious objection about AI — a model call is not deterministic, but once it has run and been checkpointed, replay reads the stored result rather than calling the model again. That is a real property of the design, though it depends on the workflow control flow being deterministic; if your code branches on a fresh model output rather than a checkpointed one, replay can diverge.

Compensation is not provided. Asked how to undo step two after step four fails, both speakers said this is developer-owned: catch the error and write compensating steps in your workflow code. Edberg's consolation is that the database records every input and output along the path, so you have what you need to walk backwards. He mentioned a forthcoming paper on compensating steps, which itself signals the problem is not solved in the library today.

Cross-language and cross-database workflows are awkward. For steps spanning applications that do not share a database, Li suggested passing idempotency keys between separately installed libraries; Edberg suggested a controlling service calling out to other services — at which point, as an audience member observed, you have built your own orchestrator. For mixing languages, Li recommended tiered workflows (a top-level workflow dispatching sub-workflows) and named the real blocker as serialization: Python's pickle and Go's gob do not interoperate. An attendee suggested Protobuf. Practically, if you need polyglot steps, keep the boundary at workflow granularity with a language-neutral payload format.

Stored procedures are a prototype, not a product. Asked about pushing steps into the database to skip round trips, Li said DBOS has a prototype compiling TypeScript into a V8 engine inside Postgres, but the environment is limited and it depends on the database allowing untrusted code execution.

One risk the talk does not dwell on: putting workflow state in your primary database couples your application's transactional capacity to your orchestration load, so a runaway retry storm now degrades user-facing queries. The auditability benefit has a matching cost — workflow payloads, including whatever arguments and model outputs your steps carry, inherit your primary database's retention and access policy, which may not be what you want for sensitive inputs.

Practical Takeaways

  • Start by writing the two tables, not by choosing a vendor. A workflow_status table plus a step_outputs table with a foreign key is enough to prototype durable execution and learn whether you need more.
  • Put the checkpoint check inside the step wrapper. Recovery, exactly-once, and fork are all downstream of that single "has this step already run?" lookup.
  • Use the workflow table as the queue table. Adding queue_name, created_at, and priority columns gets you durable queues, FIFO or priority ordering, and rate limits as aggregate queries, without a second system.
  • Reach for FOR UPDATE SKIP LOCKED before you reach for a broker. It is the standard way to let many pollers claim disjoint rows without contention.
  • Use unique constraints for leaderless coordination, and add jitter. Scheduled-time-as-primary-key gives correctness; randomized sleep gives you a survivable load profile.
  • Audit your step boundaries for idempotency and your control flow for determinism. Exactly-once is conditional on those, not granted by the library.
  • Design compensation explicitly. Assume you own rollback and write it as ordinary error handling in the workflow.
  • Weigh co-location seriously. The latency argument holds only if application servers sit within a few milliseconds of the database; a cross-region hop erodes much of the advantage over an external orchestrator.

Edberg's closing pitch is that this approach shines specifically when you already run Postgres, when latency matters enough that per-step round trips hurt, and when you would rather not express your distributed system in YAML and Terraform. He adds an argument aimed at the present moment: a single-file, in-process durable system is easier for both humans and coding agents to reason about than infrastructure spread across configuration languages. That is an opinion, not a measured result, but it is a coherent one.

Key Terms

  • Durable execution — Running a workflow so its progress survives process crashes, resuming from the last completed step rather than restarting.
  • Checkpoint — A persisted record of a workflow's inputs or a step's output, used to skip already-completed work on replay.
  • Exactly-once execution — The guarantee that each step's effect happens once across a workflow's lifetime, including retries and recovery. It depends on idempotent steps and deterministic workflow control flow.
  • Idempotency key — A unique identifier for an operation that lets the system recognize and deduplicate repeat attempts; here, the workflow ID.
  • External orchestrator — A separate service that holds workflow state and dispatches work to workers, as opposed to an in-process library.
  • FOR UPDATE SKIP LOCKED — A Postgres locking clause that locks selected rows and skips rows already locked by other transactions, enabling contention-free queue polling.
  • Workflow fork — Creating a new workflow instance that copies checkpoints up to a chosen step, then resumes from that step under new code.
  • Compensating step — Application-defined logic that undoes an earlier step's effects when a later step fails permanently.
  • Determinism (of a workflow) — The property that replaying the workflow function follows the same control flow, so checkpoints map to the same steps.

Reference: Jeremy Edberg and Qian Li, Compiling Workflows into Databases: the Architecture That Shouldn't Work (But Does), QCon San Francisco, published by InfoQ on July 23, 2026.