Skip to content
Get started
Get started
Workflow Engine for Bun: Durable Multi-Step Jobs
guide · workflow engine

Multi-step, with a rollback plan.

Some jobs are really a sequence: reserve the stock, charge the card, send the confirmation. The workflow engine runs that sequence, retries the flaky parts, resumes after a crash, and when a later step fails it undoes the earlier ones for you.

Write that sequence as plain code and three things go wrong the first time it breaks in production:

  1. The process dies halfway. The card was charged, the confirmation never went out, and nothing remembers where it got to.
  2. A later step fails after an earlier one already changed the world. The stock is reserved for an order that will never exist.
  3. A retry runs the effect twice. Two charges, one order.

A workflow engine exists to make those three cases boring.

reserve stock compensate: release stock
charge card compensate: refund
send confirmation ✗

When send confirmation fails, the engine walks back: refund, then release stock. That automatic undo is the saga pattern, and each step declares its own inverse with a compensate handler.

Three ideas carry everything else:

A workflow is a list of nodes. Steps, branches, loops, approval gates. You describe them; the engine walks them.

Each top-level node gets durable queue delivery. Finishing one writes its outcome to SQLite and enqueues the next. Inline branch, parallel and loop body steps share that node job but persist their own records. On restart, completed records short-circuit; only work whose outcome is still unknown may replay.

Failure walks the journal backwards. The engine already knows which steps completed and in what order, so it can undo them in reverse without asking your code to remember anything.

import { Workflow, Engine } from 'bunqueue/workflow';
const flow = new Workflow('checkout')
.step('reserve', reserveStock, { compensate: releaseStock })
.step('charge', chargeCard, { compensate: refund })
.step('confirm', sendEmail);
const engine = new Engine({ embedded: true, dataPath: './data/wf.db' });
engine.register(flow);
await engine.recover();
await engine.start('checkout', { orderId: 'ORD-1' });

Everything runs in your process, on bunqueue’s Queue and Worker, persisted to SQLite. No extra services, no YAML, no control plane.

For a production service, four details are part of the setup rather than optional tuning:

  1. Pass a durable dataPath; omitting it creates an in-memory execution store.
  2. Register every definition, then call recover() during startup.
  3. Make externally visible steps idempotent and pass ctx.idempotencyKey to providers that support one.
  4. Run one workflow Engine per process and call engine.close() during shutdown.

The engine guarantees durable orchestration state, not exactly-once effects in another system. If a process dies after an API accepted a charge but before the completed record reached SQLite, the only safe recovery is to replay the call with the same provider idempotency key.

Quick StartBuild and run your first workflow
Steps & Control FlowContext, retries, branching, parallel, loops
RollbackCompensation, unwind order, the point of no return
DurabilityIdempotency keys, crash recovery, what resumes
Human ApprovalPausing a run until a person decides
AI AgentsDurable agent loops with the Vercel AI SDK
API ReferenceEngine methods, events, execution shape, limits
SituationUse instead
Independent jobs with no orderingQueue + Worker
Parent/child fan-out without rollbackFlow Producer, lighter
One queue, one processor, a few routesSimple Mode
Multi-region HA, exactly-once across servicesTemporal