self-hosted · one Postgres · no Docker required

Workflows that survive anything,
written as ordinary code.

Durable replays memoized checkpoints through crashes, deploys, retries, and week-long waits. External effects remain at-least-once and should use idempotency keys. Background jobs, crons, human-in-the-loop approvals and event-driven pipelines, without building queues, timers or state machines.

run trace · onboardingcompleted
load-user57ms
cooldown2d
await-approval4h
send-email6ms
durable (queue / sleep)your serverwaiting for event

Everything a workflow engine should do.
Nothing you have to build yourself.

Durable steps

Each step.run is checkpointed. Crash mid-run or deploy new code — persisted steps are replayed and the run resumes where it left off.

Sleep & wait for events

step.sleep("2d") holds no server hostage. step.waitForEvent parks a run until an approval, payment or webhook arrives — with a timeout.

Cron & scheduling

Trigger functions on a schedule — 5-field cron or 6-field with seconds. No double-fires, no catch-up storms after downtime.

Flow control, complete

Concurrency limits (exact, per key), priority, throttle, rate-limit, debounce and batching — declared per function, enforced in the queue.

Apps × environments

Every app × environment (dev/acc/prod) is a fully isolated workspace with its own signing key. Prod actions require a confirming second click.

Realtime dashboard

Live trace waterfalls split durable-time vs app-time per step. Metrics, backlog history, rerun, rerun-from-step and cancel — updates pushed over SSE.

One Postgres, zero infra

State, queue (SKIP LOCKED), timers and history in one database. Steps persist and schedule in a single transaction — no dual writes, no Redis.

TypeScript & PHP SDKs

The engine never runs your code — it calls your app over signed HTTP. Install @dicabrio/durable-sdk from npm; a PHP SDK ships in the repo. Register functions by push (sync) or let Durable pull them from your app URL — the wire protocol is small enough to port in an afternoon.

Ops-grade by default

Graceful drain on shutdown, worker processes that scale horizontally, admin-token auth for production dashboards, HMAC on every hop.

Plain functions. Durable execution.

TypeScript
const onboarding = createFunction({
  id: "onboarding",
  trigger: { event: "user.created" },
  concurrency: { limit: 2, key: "tenantId" },
  handler: async ({ event, step }) => {
    const user = await step.run("load-user", () =>
      db.users.find(event.data.id));

    await step.sleep("cooldown", "2d");   // survives deploys

    const ok = await step.waitForEvent("approve", {
      event: "approval.received",
      match: { userId: user.id },
      timeout: "7d",
    });

    await step.run("send-email", () => mail.send(user));
    return { approved: ok !== null };
  },
});
PHP
$onboarding = new DurableFunction(
  id: 'onboarding',
  trigger: ['event' => 'user.created'],
  handler: function (array $event, Step $step) {
    $user = $step->run('load-user', fn () =>
      loadUser($event['data']['id']));

    $step->sleep('cooldown', '2d');  // survives deploys

    $ok = $step->waitForEvent('approve', [
      'event'   => 'approval.received',
      'match'   => ['userId' => $user['id']],
      'timeout' => '7d',
    ]);

    $step->run('send-email', fn () => sendMail($user));
    return ['approved' => $ok !== null];
  },
);

Same replay model, same wire protocol. If the process dies between steps, the next invocation replays the memoized results in microseconds and continues exactly where it stopped.

Running in one command.

Ships with an embedded Postgres — dev workspaces reset on startup, then Durable discovers DURABLE_DEV_APP_URL (default localhost:3000/api/durable). No Docker.

$ npx @dicabrio/durable
[durable] embedded postgres on :5434
[durable] migrations applied
[durable] service on http://localhost:3030
[durable] 2 workers + scheduler running