Durable
Durable workflows for AdonisJS — write a workflow as plain code; every step is checkpointed, so it survives crashes and deploys. Steps can run across processes, with a built-in control plane.
@adonis-agora/durable brings durable execution to AdonisJS. You write a workflow as ordinary async code — call a step, use its result, call the next — and the engine records every step's output. If the process crashes or you deploy mid-run, the workflow resumes from the last checkpoint instead of starting over. Steps can run in-process or on a remote worker, but it stays one workflow, with one source of truth, and one end-to-end timeline.
The one rule
The engine recovers a run by replaying the workflow function from the top. Completed steps return their saved result instead of executing again, so your workflow body must be deterministic — no Date.now(), Math.random(), or direct I/O outside a step. See Durability & replay.
The problem it solves
Multi-step processes are usually scattered: a queued job here, a cron there, a manual retry script somewhere else, and no single place to read or watch the whole thing. When a process dies halfway through, you are left reconstructing which steps already ran. @adonis-agora/durable collapses that into three guarantees:
- The flow becomes code, in one place. Read the workflow function and you understand the whole sequence — even when steps execute in different processes.
- Durability by replay. A crash or deploy never re-runs completed work. Each step is checkpointed; on recovery, finished steps replay their saved result and only unfinished work executes.
- End-to-end visibility. Because one engine owns the state, it knows about every step — including the remote ones — so a full-flow trace, dashboard, and Telescope view come almost for free.
Quickstart
The minimal loop — install, configure, define a workflow, start a run — with zero infrastructure: the in-memory store and an in-process transport. Swap those for a Lucid store and a queue transport when you go to production. For the full walkthrough, see Getting Started.
Install and configure the umbrella package:
node ace add @adonis-agora/durableThis registers the provider in adonisrc.ts and publishes config/durable.ts.
Define a workflow as a class under app/workflows/ — it is auto-registered at boot. Every ctx.step is checkpointed; ctx.waitForSignal suspends the run until a signal arrives:
import { BaseWorkflow } from '@adonis-agora/durable'
import type { WorkflowCtx } from '@adonis-agora/durable'
export default class CheckoutWorkflow extends BaseWorkflow {
static workflow = { name: 'checkout', version: '1' }
async run(ctx: WorkflowCtx, order: { id: number; total: number }) {
await ctx.localStep('reserveStock', async () => ({ reserved: true }))
const approval = await ctx.waitForSignal<{ approved: boolean }>(`approve:${order.id}`)
if (!approval.approved) return { status: 'rejected' }
await ctx.localStep('ship', async () => ({ shipped: true }))
return { status: 'shipped' }
}
}Start a run from a controller. dispatch enqueues the run and returns { runId } immediately — the HTTP handler never blocks on workflow logic:
import type { HttpContext } from '@adonisjs/core/http'
import CheckoutWorkflow from '#workflows/checkout_workflow'
import Order from '#models/order'
import { createCheckoutValidator } from '#validators/checkout'
export default class CheckoutController {
async store({ request, response }: HttpContext) {
const { total } = await request.validateUsing(createCheckoutValidator)
const order = await Order.create({ total })
const { runId } = await CheckoutWorkflow.dispatch(
{ id: order.id, total: order.total },
{ runId: `checkout:${order.id}` }
)
return response.accepted({ runId })
}
}Resume it later, from your approval webhook — this completes and ships the run:
// engine.signal wakes the exact run suspended on this token
await engine.signal(`approve:${order.id}`, { approved: true })Need the outcome inline instead? await engine.waitForRun(runId) resolves once the run settles.
Steps across processes
A step does not have to run in the same process. ctx.step(refOrName, input) is always dispatched, routed to a handler by name over a pluggable transport to wherever it lives. Author the handler as a @Step class under app/steps, or with defineStep for a typed ref:
import { defineStep } from '@adonis-agora/durable'
import { z } from 'zod'
export const chargeCard = defineStep(
'payments:charge-card',
async (input: { orderId: number; amountCents: number }) => ({ chargeId: await charge(input) }),
{
input: z.object({ orderId: z.number().int(), amountCents: z.number().int() }),
output: z.object({ chargeId: z.string() }),
retries: 3,
},
)
// in the workflow:
const charge = await ctx.step(chargeCard, { orderId: order.id, amountCents: order.total })Steps under app/steps are served by name automatically; the escape hatch is transport.handle(name, fn). With the in-process transport a step runs in the same process; swap the transport for the queue transport to move it to a separate worker — without changing the workflow code.
What you get
- Crash-proof by replay. Each step is checkpointed and runs exactly once, logically; only unfinished work executes after a restart.
- Durable sleep and signals. Pause for minutes or months with
ctx.sleep(no compute while waiting), or wait on a human approval or webhook withctx.waitForSignal. Both survive restarts. - Bring Lucid, any SQL database. State lives in Postgres, MySQL, or SQLite through a
StateStoreinterface, with a Lucid adapter and a published migration. - See the whole flow. A built-in control plane renders each run; OpenTelemetry and a Telescope view give you two more views of the same event log.
Where to go next
Getting Started
Install, configure, register a workflow, and start your first run — step by step.
Concepts
Durability, deterministic replay, and the mental model behind the engine.
Authoring
Child workflows, queries and updates, webhooks, versioning, scheduling, and event-triggered workflows.
Reliability
Retries, sagas, dead-letter, and flow control.
Transports
The in-process default, the @adonisjs/queue transport, and the broker-less DB transport.
State stores
Persist runs to Postgres, MySQL, or SQLite with Lucid.
Observability
The control-plane dashboard, OpenTelemetry traces, and the Telescope view.
Tooling
The CLI, the test harness, and the determinism linter.