Agora

API reference

Every public member of WorkflowCtx, BaseWorkflow and the authoring decorators, the WorkflowEngine's selected surface, the RunGateway, the run-detail and fan types, the DurableConfig options of config/durable.ts, the driver factories, the observability and command building blocks, the thrown error classes, the run/checkpoint status enums and the package's subpath exports — one line each, distilled from source.

This page is a flat lookup table of the public API of @adonis-agora/durable: the WorkflowCtx, BaseWorkflow and the decorators, the engine, the RunGateway, defineConfig, the driver factories, the observability and command building blocks, the errors and the statuses. It is deliberately terse. For the reasoning behind an entry, follow its link to the concept, authoring or reliability page.

Everything below is exported from the package root (@adonis-agora/durable) unless a row names a subpath.

WorkflowCtx

The context handed to a workflow's run(ctx, input). Every interaction with the outside world goes through it so the engine can checkpoint, and the body itself stays deterministic (see Durability & replay).

Properties

MemberSignatureNotes
runIdreadonly runId: stringThis run's id.

Steps

MemberSignatureNotes
stepstep<TInput, TOutput>(handler: StepRef<TInput, TOutput>, input: TInput, opts?: StepDispatchOpts & { compensate?: StepRef<StepUndo<TInput, TOutput>, unknown> | string }): Promise<TOutput>Dispatch a durable step by typed reference (a @Step method or a defineStep handle). This is the one step primitive, and it is always engine-scheduled: the run suspends until the result lands. compensate registers a dispatched saga undo. See Workflows & steps.
step (by name)step<TOutput = unknown>(name: string, input: unknown, opts?: StepDispatchOpts & { compensate?: StepRef<StepUndo<unknown, TOutput>, unknown> | string }): Promise<TOutput>The same dispatch addressed by string name, for a cross-runtime handler (e.g. a Python step) with no JS reference.
localSteplocalStep<TOutput>(name: string, fn: (log: StepLogger) => Promise<TOutput>, options?: StepOptions): Promise<TOutput>A checkpointed step whose body runs in the engine process instead of being dispatched. It can carry an in-memory compensate for sagas. See Local steps.
transactiontransaction<TOutput>(name: string, fn: (tx: unknown) => Promise<TOutput>): Promise<TOutput>Exactly-once DB step: runs fn and writes the checkpoint in one store transaction, so the business write and the "done" marker commit together. Needs a store that supports transactions and throws otherwise. See External tasks & transactions.

Time

MemberSignatureNotes
sleepsleep(duration: string | number): Promise<void>Durable sleep for a relative duration ('30s', '2h', '7 days', or ms). Uses no compute while waiting and resumes across restarts. See Sleep & signals.
sleepUntilsleepUntil(when: Date | number): Promise<void>Durable sleep until an absolute time. The wake time is fixed on the first run, so it is replay-stable.
nownow(): Promise<number>Deterministic epoch-ms clock: recorded on the first run, the same value on replay. Use it instead of Date.now() in a workflow body. See Versioning & determinism.

Signals & events

MemberSignatureNotes
waitForSignalwaitForSignal<TPayload>(token: string, opts?: { timeoutMs?: number }): Promise<TPayload>Suspend until engine.signal(token, payload) arrives. timeoutMs bounds the wait and throws SignalTimeoutError when it passes. See Sleep & signals.
waitForEventwaitForEvent<TPayload>(name: string, opts?: { match?: Record<string, unknown>; timeoutMs?: number }): Promise<TPayload>Suspend for a named event published with engine.publishEvent. match is a payload subset that must deep-equal, so a publish resumes only the runs it concerns. See Named events.
setEventsetEvent<TValue>(key: string, value: TValue): Promise<void>Publish a queryable value that engine.getEvent(runId, key) reads while the run is in flight. See Queries & updates.
onUpdateonUpdate<TArg>(name: string, opts?: { timeoutMs?: number }): Promise<TArg>A run-scoped update point: suspends until engine.update(runId, name, arg) delivers arg, after any registered validator accepts it.

Children

MemberSignatureNotes
childchild<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, options?: string | ChildCallOptions): Promise<WorkflowOutputOf<C>>Start (or reattach to) a child run and suspend until it is terminal, then resume with its output. Throws a FatalError if the child failed. See Child workflows.
child (by name)child<TOutput>(workflow: string, input: unknown, options?: string | ChildCallOptions): Promise<TOutput>The same, for a cross-runtime child addressed by name.
startChildstartChild<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, options?: string | ChildCallOptions): Promise<string>Fire-and-forget: dispatch a child once and return its run id at once; the parent keeps running. Also takes a workflow name (startChild(workflow: string, input: unknown, options?)).
allall<C extends WorkflowClass>(workflow: C, inputs: WorkflowInputOf<C>[], opts?: AllOptions): Promise<WorkflowOutputOf<C>[]>Run N children of the same workflow in parallel and await all of them, outputs in input order. waitAll (the default) throws an aggregate GatherError; failFast throws on the first failure and cancels the remaining siblings (best-effort). Also takes a workflow name (all<TOutput = unknown>(workflow: string, inputs: unknown[], opts?: AllOptions)).
all (over a step)all<TInput, TOutput>(step: StepRef<TInput, TOutput>, inputs: TInput[], opts?: AllStepsOptions): Promise<TOutput[]>One durable step per input, joined once: the parent replays once per settled wave, not once per result. Failures surface as a GatherError. See Fanning out over a step.
allSettledallSettled<C extends WorkflowClass>(workflow: C, inputs: WorkflowInputOf<C>[], opts?: AllSettledOptions): Promise<Settled<WorkflowOutputOf<C>>[]>The same fan as all in waitAll mode, resolving with one Settled per input instead of throwing. Also takes a @Step reference (opts?: AllStepsSettledOptions) or a workflow name. See Child workflows.

External

MemberSignatureNotes
tasktask<TResult>(name: string, dispatch: () => Promise<void>, options?: StepOptions): Promise<TResult>An external task with async completion: runs dispatch once, then suspends until engine.completeTask or engine.failTask reports back. name must be unique per run. See External tasks.
webhookwebhook<TPayload>(): DurableWebhook<TPayload>Mint a durable callback handle (token plus an optional public url). await handle.wait({ timeoutMs? }) suspends until the callback lands as engine.signal(token, body). See Durable webhooks.
callEntitycallEntity<TResult = unknown>(name: string, key: string, op: string, arg?: unknown): Promise<TResult>Call a durable entity op (serialized per key) and await its result. See Durable entities.
signalEntitysignalEntity(name: string, key: string, op: string, arg?: unknown): Promise<void>Send an entity op fire-and-forget.

State & metadata

MemberSignatureNotes
patchedpatched(id: string): Promise<boolean>Guard an in-place change without a new version: true for a fresh run (new branch), false for a run whose history predates the guard. See Versioning.
continueAsNewcontinueAsNew(input?: unknown): Promise<never>End this run and start a fresh execution of the same workflow with clean history (id <runId>~N). Always throws. See Continue-as-new.
sideEffectsideEffect<TValue>(fn: () => TValue | Promise<TValue>): Promise<TValue>Deterministic capture: runs fn once, checkpoints the result, and replay returns the same value without re-running fn (e.g. uuidv7(), Math.random()).
upsertSearchAttributesupsertSearchAttributes(attrs: Partial<A>): Promise<void>Shallow-merge queryable metadata onto this run's searchAttributes (A is WorkflowCtx<A>'s attribute shape, default SearchAttributes). Exactly-once: skipped on replay. With a declared searchAttributes schema the merged result is validated first (SchemaValidationError on failure). See Typed search attributes and See Search attributes.
breakpointbreakpoint(label?: string): Promise<void>Pause the run here until a human resumes it with engine.continue(runId) or the dashboard. See Human-in-the-loop pauses.

Supporting option types

TypeShapeNotes
StepDispatchOpts{ queue?, priority?, fairnessKey?, transport?, retries?, backoff?, backoffMs?, backoffMaxMs?, jitter?, timeoutMs?, pickupTimeoutMs?, requires? }Per-call override of the @Step-declared policy, merged as { ...stepConfigOf(ref), ...opts }. queue routes through a flow-control queue, where priority and fairnessKey apply. transport pins a named transport in the pool. requires lists capabilities a worker must advertise. See Dispatch options.
StepOptions{ retries?, backoff?, backoffMs?, backoffMaxMs?, jitter?, timeoutMs?, pickupTimeoutMs?, compensate?, requires? }Options for ctx.localStep and ctx.task. compensate: () => Promise<void> registers an in-process saga undo, retried up to compensationRetries (default 1). See Retries & backoff.
BackoffStrategy'fixed' | 'exp'Constant or doubling delay between retries.
StepUndo<TInput, TOutput>{ input: TInput; output: TOutput }The single argument a dispatched saga undo receives: the compensated call's own input and result.
UndoOf<H>H extends (input: infer I, ...rest: never[]) => infer R ? StepUndo<I, Awaited<R>> : StepUndo<unknown, unknown>Derives an undo's argument from the original step's signature, so a compensate ref is compile-checked: async cancelBooking(undo: UndoOf<FlightService['book']>) { ... }.
AllOptions{ mode?: 'waitAll' | 'failFast'; concurrency?: number }For ctx.all over children. concurrency sends waves of floor(concurrency / 2), two at a time. It is part of the fan's identity, so don't change it under an in-flight run.
AllStepsOptionsOmit<StepDispatchOpts, 'queue' | 'timeoutMs' | 'pickupTimeoutMs'> & { mode?: 'waitAll' | 'failFast'; concurrency?: number }For ctx.all over a @Step. The retry/backoff options apply to every step of the fan; concurrency sends strict waves of that size.
AllSettledOptions / AllStepsSettledOptionsOmit<AllOptions, 'mode'> / Omit<AllStepsOptions, 'mode'>The ctx.allSettled counterparts.
Settled<T>{ status: 'fulfilled'; value: T } | { status: 'rejected'; id: string; error: string; code?: string; details?: unknown }One item of an allSettled fan, in input order. id is the child run id or the step id.
ChildCallOptions{ childId?: string; priority?: number }Options for ctx.child and ctx.startChild; a bare string is shorthand for { childId }. priority only affects a remote child.
DurableWebhook<TPayload>{ readonly token: string; readonly url?: string; wait(opts?: { timeoutMs?: number }): Promise<TPayload> }What ctx.webhook() returns. url is built by the webhookUrl config option.

DynamicWorkflowCtx — the non-overloaded view

WorkflowCtx minus its five overloaded methods, re-declared with one string-addressed signature each: step(name, input, opts?), child(workflow, input, options?), startChild(workflow, input, options?), all(workflow, inputs, opts?) and allSettled(workflow, inputs, opts?). Every WorkflowCtx is assignable to it without a cast. Depend on it (or a Pick of it) in code that drives the ctx by names, such as a graph interpreter, and in test fakes, where satisfying every overload of the full WorkflowCtx is impractical:

type InterpreterCtx = Pick<DynamicWorkflowCtx, 'runId' | 'localStep' | 'child' | 'all' | 'sleep'>
new Interpreter(ctx) // a WorkflowCtx IS a DynamicWorkflowCtx — no `as unknown as` cast

StepLogger

The second argument a step handler (and a ctx.localStep body) receives. Events it records are checkpointed with the step and rendered under it in the dashboard. See Steps vs. sub-process events.

MemberSignatureNotes
debug info warn error(message: string, data?: unknown): voidOne method per level.
heartbeatheartbeat(progress?: unknown): voidReport "alive mid-flight": rearms the step's timeoutMs window and persists the latest beat on the checkpoint. Throttled internally (about 5s). See Reporting liveness.
subsub(name: string, status: 'ok' | 'failed' | 'skipped', message?: string, data?: unknown): voidRecord a sub-step outcome.
subEventsubEvent(e: { id; name; group?; phase?; status?; message?; data? }): voidRecord a sub-process event (a phase transition or a terminal status).
subProcesssubProcess<T>(name: string, body: (sp: SubProcessHandle) => Promise<T> | T, opts?: { group?: string; id?: string }): Promise<T>Time body and record ok or failed. sp.phase(label), sp.skip(reason) and sp.fail(reason) mark other outcomes.

currentStep()

currentStep(): StepLogger | undefined, exported from @adonis-agora/durable, returns the logger of the step body running on the current async path: the same instance the body received, so what it records lands in the same event list. Every step body has one, local (ctx.step, ctx.localStep) and remote (a handler on a worker). It is undefined outside a step, so a helper deep in your code can instrument itself without being handed the logger and still runs in a unit test:

import { currentStep } from '@adonis-agora/durable'

export async function insertBatch(rows: Row[]) {
  await db.table('lines').multiInsert(rows)
  currentStep()?.info('inserted batch', { rows: rows.length })
}

Authoring

BaseWorkflow

The base class for a workflow under app/workflows/, auto-registered at boot. See app/workflows.

MemberSignatureNotes
workflowstatic workflow?: WorkflowOptionsThe workflow's identity and options (table below). A class without it is not registrable.
schedulestatic schedule?: WorkflowScheduleConfig | WorkflowScheduleConfig[]Colocated recurring schedule(s), merged with config.schedules (config wins on a key collision). See Scheduling.
runabstract run(ctx: WorkflowCtx, input: unknown): unknownThe workflow body. Annotate input with the type you want WorkflowInputOf to infer.
deadLetterdeadLetter?(ctx: WorkflowCtx, dead: DeadLetter): unknownOptional inline dead-letter handler, registered as <name>.dlq (same version and origin) and started as dlq:<deadRunId> when a run of this workflow goes dead. DeadLetter<TInput> is { deadRunId: string; workflow: string; input: TInput; error?: StepError }. Declaring it with static workflow.deadLetterWorkflow throws. See Dead-letter queue.
startstatic start<C extends WorkflowClass>(this: C, input: WorkflowInputOf<C>, opts?: WorkflowDispatchOptions): Promise<WorkflowOutputOf<C>>Run and wait for the result. Outside a workflow: engine.start plus waitForRun(runId, { terminal: true }). Inside one: ctx.child.
dispatchstatic dispatch<C extends WorkflowClass>(this: C, input: WorkflowInputOf<C>, opts?: WorkflowDispatchOptions): Promise<{ runId: string }>Fire-and-forget. Outside a workflow: engine.start. Inside one: ctx.startChild.

WorkflowDispatchOptions is { runId?: string } & StartOptions. Outside a workflow runId sets the new run's id (default randomUUID()); inside one it is the deterministic child id, and omitting it lets the ctx derive a replay-stable one. See Child workflows.

Related helpers:

ExportSignatureNotes
setWorkflowEngineResolversetWorkflowEngineResolver(resolver: WorkflowEngineResolver | undefined): voidOverride how the statics find the engine outside a workflow (default: the AdonisJS container). A testing seam; pass undefined to restore. See Testing.
getCurrentWorkflowCtxgetCurrentWorkflowCtx(): WorkflowCtx | undefinedThe ambient WorkflowCtx on the executing body's async path, or undefined outside one.
workflowNameworkflowName(ref: WorkflowRef): stringResolve a class or a name to the registered name. Throws for a class without static workflow.
workflowMetaworkflowMeta(target: unknown): WorkflowMeta | undefinedRead a class's static workflow with version normalized to '1'.

WorkflowOptions (static workflow)

OptionTypeNotes
namestringRequired. The registered, cross-runtime workflow name.
versionstringDefault '1'. Old runs keep resuming on the version they started on. See Versioning.
tagsstring[]Labels merged onto every run, together with per-start tags.
executionTimeoutstring | numberWall-clock budget for the whole run ('5m' or ms). An overrunning run is cancelled by sweepTimeouts.
onEventstring[]Event names whose engine.publishEvent starts a run of this workflow. See Named events.
singletonSingletonConfig ({ key: (input: unknown) => string; limit?: number; maxQueueDepth?: number })Durable FIFO mutex per key. See Singleton workflows.
concurrencyConcurrencyConfig ({ key: (input: unknown) => string | undefined; limit: number | ((key: string) => number | Promise<number>); countStatuses?: RunStatus[] })Start-time quota per key, shared across workflows. An over-limit start throws ConcurrencyLimitError. See Concurrency quotas.
originstringPackage attribution stamped on every run (WorkflowRun.origin). Overrides the origin derived from the nearest package.json name. See Run origin.
onError{ code?: string | { readonly code: string }; workflow: WorkflowRef }[]Route a failed run to a handler workflow by error code (string or an error class with a static code; omit for the catch-all). See Routing a run's failure.
deadLetterWorkflowWorkflowRefThe workflow a dead run of this one is handed to (overrides the config default). Mutually exclusive with a deadLetter() method. See Dead-letter queue.
inputSchemaStandardSchemaV1Any Standard Schema (zod, valibot, arktype, …). Validated at start before a run is created. The run stores the input as sent; run receives the schema's output, re-parsed before every turn. A failure throws SchemaValidationError. See Validating the input.
searchAttributesSearchAttributesSchemaA Standard Schema with a flat output: every ctx.upsertSearchAttributes is validated against the merged attributes. Must validate synchronously. See Typed search attributes.

@Step(nameOrOptions?) and defineStep

Step(nameOrOptions?: string | StepDecoratorOptions): MethodDecorator marks a class method as a durable step handler, routed by name. Steps under app/steps/ are discovered and served automatically (config stepsPath). See Workflows & steps.

FormNotes
@Step()Name derived as `${ClassName}.${method}`.
@Step('custom:name')Explicit name, a stable cross-runtime contract.
@Step({ name?, input?, output?, retries?, backoff?, backoffMs?, backoffMaxMs?, jitter?, timeoutMs?, pickupTimeoutMs?, requires? })input / output are opt-in Standard Schemas (zod, valibot, arktype, …) validated at the serve boundary; a failure is a non-retryable SchemaValidationError. The rest is the def-level dispatch policy ctx.step reads (a per-call opts overrides it field by field). See Retries & backoff.
StepDecoratorOptions fieldTypeNotes
namestringExplicit routing name.
input / outputStandardSchemaV1Runtime schemas for the step's input and output — any Standard Schema library. The handler gets the input schema's output value; the caller gets the output schema's output value.
retriesnumberMax attempts before the step (and run) fails.
backoffBackoffStrategy'fixed' or 'exp'.
backoffMs / backoffMaxMsnumberBase delay and upper bound in ms.
jitterbooleanRandom 50–100% jitter on the delay.
timeoutMsnumberLiveness window: no result or heartbeat within it fails the dispatch with RemoteStepTimeout (retryable).
pickupTimeoutMsnumberLiveness window before the worker's first heartbeat (queue wait plus pickup). Defaults to timeoutMs. See pickupTimeoutMs.
requiresstring[]Capabilities a live worker must advertise; with none live, the run parks blocked. See Capability-aware routing.
ExportSignatureNotes
defineStepdefineStep<TInput, TOutput>(name: string, fn: (input: TInput, log: StepLogger) => TOutput | Promise<TOutput>, config?: Omit<StepDecoratorOptions, 'name'>): StepRef<TInput, TOutput>A step handler without a class or decorator. With an input / output schema and an unannotated handler, the types are inferred: StepRef<InferInput<input>, InferOutput<output>>, handler argument InferOutput<input>. See defineStep.
StepRef<TInput, TOutput>((input: TInput) => Promise<TOutput> | TOutput) & { [DURABLE_STEP_NAME]?: string; [DURABLE_STEP_CONFIG]?: StepConfig }A typed handle onto a stamped step name and policy.
StandardSchemaV1 / validateStandardvalidateStandard(schema, value, label): Promise<InferOutput<schema>>The vendored Standard Schema V1 type, and the helper every schema slot uses (sync or async validate; throws SchemaValidationError). An optional fourth argument sets the error's code ('invalid_input', 'invalid_output', default 'schema_validation').
InferSearchAttributes / readSearchAttributes / attributesOfattributesOf(WorkflowClass, run): AInfer a searchAttributes schema's shape; read a run's attributes typed through a schema or by workflow class (lenient: history that fails the schema reads as {}).
stepNameOf / stepConfigOfstepNameOf(ref: unknown): string | undefined / stepConfigOf(ref: unknown): StepConfig | undefinedRead the stamped name or policy off a reference.

Class decorators

DecoratorSignatureNotes
@ScheduledScheduled(config: WorkflowScheduleConfig | WorkflowScheduleConfig[]): ClassDecoratorDecorator form of static schedule; stacks in source order. See The @Scheduled decorator.
@OnEventOnEvent(config: EmitterTriggerConfig): ClassDecoratorStart the workflow on an exact AdonisJS emitter event ({ event: string }). Stamps static on. See Event-triggered workflows.
@OnDiagnosticOnDiagnostic(config: DiagnosticsTriggerConfig): ClassDecoratorStart the workflow on an @adonis-agora/diagnostics channel ({ lib: string; event?: string | RegExp }); omit event for every event of the lib.

onError types

TypeShapeNotes
OnErrorRoute{ code?: string; workflow: string }A route with names resolved. The engine's register / registerRemote take onError?: OnErrorRoute[].
OnErrorInput<TInput>{ runId: string; workflow: string; input: TInput; error: StepError }What a handler workflow is started with. See onError.

WorkflowEngine (selected public surface)

The engine singleton is the default export of @adonis-agora/durable/services/main (bound on the standalone and control-plane roles only; a tenant pod uses runGateway from the same module, see Roles & config). It is also resolvable from the container as WorkflowEngine. This is a curated subset, not the full class.

import engine from '@adonis-agora/durable/services/main'

Registering

MemberSignatureNotes
registerregister(name: string, version: string, fn: WorkflowFn, opts?: { tags?, singleton?, concurrency?, executionTimeout?, validateInput?, inputSchema?, searchAttributesSchema?, onEvent?, eventBatch?, requires?, origin?, onError?, deadLetterWorkflow? }): voidRegister a function-form workflow (WorkflowFn is (ctx: WorkflowCtx, input: unknown) => Promise<unknown>). app/workflows discovery calls this for you. See Workflows & steps.
registerRemoteregisterRemote(name: string, version: string, opts: { group: string; executor: WorkflowExecutor; tags?; singleton?; concurrency?; executionTimeout?; validateInput?; inputSchema?; requires?; onError?; deadLetterWorkflow? }): voidRegister a workflow whose body runs in another SDK; the engine still owns its state. inputSchema (any Standard Schema) runs before validateInput, which sees the parsed value. See Cross-ecosystem interop.
remoteremote(name: string, opts: { group: string; version?: string; tags?; singleton?; concurrency?; executionTimeout?; validateInput?; inputSchema?; requires?; onError?; deadLetterWorkflow? }): voidregisterRemote with the executor built over the engine's primary transport; version defaults to '1'. Throws without a transport.
handlehandle(name: string, fn: StepHandler): voidServe a step body by name on the engine's transport. It makes the engine a StepServer, so registerStepsFromDir(engine, dir) serves an extra directory from a preload. Throws when the transport can't serve handlers. See Registering an extra directory by hand.
registerQueueregisterQueue(config: QueueConfig): voidRegister a flow-control queue ({ name, concurrency?, rateLimit?, retryMs?, fairness?, order? }). See Flow control.
registerEntityregisterEntity<S>(name: string, config: EntityConfig<S>): voidRegister a durable entity ({ initialState: () => S; handlers: Record<string, EntityHandler<S>> }). See Durable entities.
registerUpdateValidatorregisterUpdateValidator<TArg>(workflow: string, name: string, validate: UpdateValidator<TArg>): voidRegister (or replace) the validator gating update for a (workflow, name) pair. See Queries & updates.

Starting and delivering

MemberSignatureNotes
startstart<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, runId: string, opts?: StartOptions): Promise<RunResult>Create and enqueue a run. Idempotent by runId. Also takes a workflow name (start<TInput>(workflow: string, input: TInput, runId: string, opts?)).
waitForRunwaitForRun(runId: string, opts?: { timeoutMs?: number; terminal?: boolean }): Promise<RunResult>Resolve once the run settles (terminal or suspended; a run suspended while it awaits a remote workflow turn's decision is not settled yet). terminal: true waits past suspensions for a terminal status.
signalsignal(token: string, payload: unknown): Promise<RunResult | null>Deliver a signal; buffered if no run is waiting yet (null). On a transport with a run queue the result is the run's status once the wake is handed over, not after the resumed turn; waitForRun gives the outcome. See Signals.
signalWithStartsignalWithStart(workflow: WorkflowRef, input: unknown, runId: string, signal: { token: string; payload?: unknown }, opts?: StartOptions): Promise<{ runId: string }>Ensure the run exists, then signal it, race-free.
publishEventpublishEvent(name: string, payload: unknown, opts?: { id?: string; buffer?: boolean }): Promise<number>Resume every run parked on ctx.waitForEvent(name, …) and start every onEvent subscriber; returns the count touched. A publish that reaches nobody is buffered once unless buffer: false. See Named events.
updateupdate(runId: string, name: string, arg: unknown): Promise<UpdateResult>Deliver a validated update to ctx.onUpdate(name). A rejection never touches the run.
getEventgetEvent<TValue = unknown>(runId: string, key: string): Promise<TValue | undefined>Read the latest value a run published with ctx.setEvent.
completeTaskcompleteTask(runId: string, name: string, result: unknown): Promise<RunResult | null>Report a ctx.task(name, …) result back to its run. See External tasks.
failTaskfailTask(runId: string, name: string, error: string): Promise<RunResult | null>Report a ctx.task failure; the run resumes and throws a FatalError at the task.
signalEntitysignalEntity(name: string, key: string, op: string, arg?: unknown): Promise<void>Send an entity op from outside a run.
getEntityStategetEntityState<S = unknown>(name: string, key: string): Promise<S | undefined>Read an entity's current durable state.

Controlling runs

MemberSignatureNotes
cancelcancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null>Cancel a run. { compensate: true } resumes it first so its saga undos run in reverse. See Compensating cancellation.
cancelWherecancelWhere(filter: Omit<RunQuery, 'page' | 'size'>, opts?: { compensate?: boolean }): Promise<RunResult[]>Bulk-cancel every run matching a RunQuery filter, each through cancel.
resumeresume(runId: string): Promise<RunResult>Re-execute a run's body. Explicit, so it also re-drives a failed run (prefer requeue). Completed, cancelled and dead runs are never re-executed.
requeuerequeue(runId: string): Promise<RunResult | null>The dispatch-model retry: set the run back to pending, clear its lease and dispatch it. Returns without running the body inline. (A second parameter is internal.)
redispatchPendingredispatchPending(runId: string): Promise<(RunResult & { redispatched: number }) | null>Re-dispatch every remote step of a run stuck pending after a lost job. See Failure modes.
retryWithInputretryWithInput(runId: string, input: unknown, newRunId?: string): Promise<{ runId: string } | null>Fix-and-replay: a new run (default id <runId>~retry~<uuid>) with corrected input and clean history. See Dead-letter queue.
continuecontinue(runId: string): Promise<RunResult | null>Resume a run paused at ctx.breakpoint.
deleteRundeleteRun(runId: string): Promise<number>Hard-delete a run and its child subtree; returns the count removed.
purgeRunspurgeRuns(scope: RunScope, opts?: { batchSize?: number; cancelLive?: boolean; children?: boolean }): Promise<number>Hard-delete every run matching scope, in batches. See Purging on demand.
purgeNamespacepurgeNamespace(namespace: string, opts?: { batchSize?: number; cancelLive?: boolean }): Promise<number>purgeRuns({ namespace }).

Reading

MemberSignatureNotes
getRungetRun(runId: string): Promise<WorkflowRun | null>A run's current persisted state.
listRunslistRuns(query: RunQuery): Promise<WorkflowRun[]>Filter by workflow, status, tags, namespace and search attributes. See Querying by attribute.
countRunscountRuns(query: Omit<RunQuery, 'page' | 'size'>): Promise<number>Exact count through the store when it supports one.
listCheckpointslistCheckpoints(runId: string): Promise<StepCheckpoint[]>A run's checkpoint timeline.
getRunDetailgetRunDetail(runId: string, options?: RunDetailOptions): Promise<RunDetail | null>The run, its timeline and children; with summarizeFansOver every wider fan is summarized into fans. When summarizeFansOver is set and the store has checkpointStats + queryCheckpoints (the Lucid store does), the detail is built from those aggregates and carries an etag; options.ifNoneMatch naming it returns { notModified: true } without reading the timeline (the dashboard's 304). See Run detail and fan types.
getFanMembersgetFanMembers(runId: string, group: string, query: FanPageQuery): Promise<FanPage | null>One page of a summarized fan, versioned like getRunDetail (query.ifNoneMatch).
runFacetsrunFacets(query: RunFacetQuery): Promise<RunFacetRow[]>How the matching runs split across (status, origin): one GROUP BY through the store's runFacets, else counted over listRuns.
runValueFacetsrunValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>The distinct values of one filter axis (workflow, tag, …) over the matching runs, with counts: what a console's pickers list. Delegates to the store's runValueFacets, else counts a bounded scan (RUN_VALUE_FACET_SCAN, 5000 runs).
getRunChildrengetRunChildren(parentRunId: string): Promise<string[]>Ids of the runs this run spawned, stable after they finish.
workerHealthworkerHealth(extra?: string[]): Promise<GroupHealth[]>Per-group queue backlog and live worker heartbeats, over knownGroups(extra) plus every group seen in a heartbeat. Empty unless the transport can introspect health (the bullmq, queue and db transports).
knownGroupsknownGroups(extra?: string[]): string[]The worker groups this engine dispatches to: every registered remote workflow's group plus extra. Pass local-step groups in extra so a group with backlog and no workers still reports.
workflowDirectoryworkflowDirectory(): Promise<WorkflowDirectory>Every workflow the live fleet can run in this engine's partition (workflows, each declared or observed), whether the fleet could be asked at all (supported), workers live on other partitions (otherPartitions) and a one-line detail. See What can I call?.
announcedWorkflowsannouncedWorkflows(): Promise<AnnouncedWorkflow[]>workflowDirectory()'s workflows.
unservedOnErrorRoutesunservedOnErrorRoutes(): Promise<{ workflow: string; route: OnErrorRoute }[]>onError routes whose handler nothing can run. See Catching a misnamed handler.

Schedules

MemberSignatureNotes
listScheduleslistSchedules(now?: number): Promise<ScheduleInfo[]>Config and colocated schedules with their live state. See Runtime control.
setSchedulePausedsetSchedulePaused(key: string, paused: boolean): booleanPause or resume a schedule, fleet-wide over the control plane.
triggerScheduletriggerSchedule(key: string, now?: number): Promise<RunResult | null>Start the current window now (idempotent by its run id).
schedulesreadonly schedules: ScheduleClient — create / upsert / get / list / pause / resume / trigger / delete / tickPersisted, runtime-managed schedules stored in durable_schedules, fired when persistedSchedules is on. create throws ScheduleAlreadyExistsError; the others throw ScheduleNotFoundError on an unknown id. See Persisted schedules.

runSchedules(engine, schedules, nowMs, opts?) is a standalone function, not an engine method: it starts each ScheduledWorkflow's current time-bucket run (idempotent through engine.start). The durable:work loop calls it for you. resolveSchedules(schedules) resolves the lazy imports and classes to registered names first (async); runSchedules refuses a schedule whose lazy import was not resolved. See Lower-level — runSchedules.

Listening and middleware

MemberSignatureNotes
subscribesubscribe(listener: EngineListener): () => voidEvery EngineEvent the engine emits. See Events & interceptors.
useuse(interceptor: StepInterceptor): () => voidOnion middleware around real local-step execution; never fires on replay.
onDeadonDead(listener: (run: WorkflowRun) => void): () => voidRuns moved to dead past maxRecoveryAttempts. See Dead-letter queue.
onEnqueuedonEnqueued(listener: (runId: string) => void): () => voidRuns enqueued on another instance (control-plane broadcast).
onCancelonCancel(listener: (runId: string) => void): () => voidCancellations on any instance, for cooperative abort.
onStalledonStalled(listener: (info: StalledRunInfo) => void): () => voidStranded in-flight runs, paged by sweepStalled after stalledAfter. See The stalled-run pager.
onEvictonEvict(listener: (run: WorkflowRun, checkpoints: StepCheckpoint[]) => void | Promise<void>): () => voidArchive a run before retention deletes it; a throwing hook skips that deletion. See Retention.

Worker loop

These are the phases node ace durable:work (or the embedded worker) drives on each tick. Call them yourself only when composing your own loop. See CLI.

MemberSignatureNotes
runPendingrunPending(nowMs?: number): Promise<RunResult[]>Pick up and execute every pending run.
runOnerunOne(runId: string): Promise<RunResult | null>Execute one run, e.g. from onEnqueued.
consumeRunsconsumeRuns(): (() => Promise<void>) | undefinedConsume this engine's namespace's run queue and return the consumer's stop. The engine calls it itself when built (a second call returns the same stop); undefined without a run queue or with a custom runDispatcher. drain() stops it.
runEnqueuedrunEnqueued(run: RunDispatch | string): Promise<void>Take one run delivered by the transport's run queue and act on its state: a pending run is run, a suspended one is leased and resumed, a finished one is skipped. The engine calls it from its own run-queue consumer.
recoverIncompleterecoverIncomplete(nowMs?: number): Promise<RunResult[]>Resume runs left running by a crash or deploy.
resumeDueTimersresumeDueTimers(nowMs?: number): Promise<RunResult[]>Resume suspended runs whose timer is due, at most timerBatchSize per pass.
sweepTimeoutssweepTimeouts(now?: number): Promise<void>Cancel runs past their executionTimeout, cascading to children.
sweepRetentionsweepRetention(now?: number): Promise<number>Apply the retention policy (throttled to once a minute).
sweepStalledsweepStalled(now?: number): Promise<StalledRunInfo[]>Detect stranded runs and notify onStalled listeners.
startConsumersstartConsumers(): voidStart broker consumption deferred by consumers. Idempotent.
beginDrain / isDrainingbeginDrain(spentMs?: number): void / isDraining(): booleanStop picking up new work without waiting. spentMs reports shutdown wait already spent (e.g. on an in-flight tick); it is deducted from the next drain budget, and from one already waiting. Non-finite or negative values are ignored.
draindrain(timeoutMs?: number): Promise<void>Graceful shutdown: stop new work and wait for in-flight executions (default 10000 ms), minus any wait reported through beginDrain(spentMs).

StartOptions

OptionTypeNotes
versionstringPin the run to this registered version instead of the newest. An unregistered version throws before any run row exists; never falls back. Convention routing cannot honour a pin.
tagsstring[]Merged with the workflow's static tags.
searchAttributesSearchAttributesTyped, queryable run data (Record<string, string | number | boolean>).
prioritynumberDispatch priority for a remote run; higher wins.
namespacestringStamp this run into another worker pool. See Tenancy.
startAtnumber | DateCreate now, park suspended, start when due. See Delayed starts.
originstringOverride the registration's package attribution for this run.
concurrencyConcurrencyQuota ({ key: string; limit: number; countStatuses?: RunStatus[] })A per-start quota that overrides the registered one. See Concurrency quotas.

RunGateway

The read, control and start surface every role exposes, as runGateway from @adonis-agora/durable/services/main. On standalone and control-plane it is a StoreRunGateway over the engine; on a tenant pod it is a ProxyRunGateway that round-trips each call over the transport to the control plane's RunRequestResponder. The dashboard and the CLI read through it, so code written against it runs unchanged on every role. See Reading and steering runs.

import { runGateway } from '@adonis-agora/durable/services/main'

const detail = await runGateway.getRunDetail?.(runId, { summarizeFansOver: 50 })
MemberSignatureNotes
topologytopology(): DurableTopology{ role: 'control-plane' | 'standalone' | 'tenant'; tenant? }.
getRun / listRunsgetRun(runId): Promise<WorkflowRun | null> / listRuns(query: RunQuery): Promise<WorkflowRun[]>As on the engine.
getCheckpoints / getRunChildren(runId: string) => Promise<StepCheckpoint[]> / Promise<string[]>A run's timeline (the engine's listCheckpoints) and its child ids.
getRunDetail? / getFanMembers?as on the engineSummarized detail and fan pages, ETag-versioned (ifNoneMatch → notModified). Optional: absent on a gateway that predates them.
getSearchAttributesgetSearchAttributes(runId: string): Promise<SearchAttributes | undefined>A run's search attributes.
workerHealthworkerHealth(): Promise<GroupHealth[]>Per-group backlog and live workers.
startstart(workflow: WorkflowRef, input: unknown, opts?: StartRunOptions): Promise<RunResult>StartRunOptions is StartOptions & { runId? }; a missing runId is minted. Idempotent by run id.
signalsignal(runId: string, signal: string, payload?: unknown): Promise<RunResult | null>Deliver a signal by token. runId lets a proxy route to the owning control plane; an unclaimed payload is buffered.
cancelcancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null>As on the engine.
redispatchPendingredispatchPending(runId: string): Promise<(RunResult & { redispatched: number }) | null>As on the engine.
retry? / continue? / retryWithInput?retry(runId) (the engine's requeue), continue(runId), retryWithInput(runId, input)Optional control verbs. Against a control plane that predates them (it answers unsupported-request), the dashboard degrades: retry falls back to redispatchPending, continue and retryWithInput answer null (the handlers 404).
runFacets? / runValueFacets?as on the engineStatus/origin split and per-axis value counts for the runs list. Optional; against an older control plane the dashboard counts a bounded listRuns scan instead.
waitingFor?waitingFor(runIds: string[]): Promise<Record<string, RunWaiting>>What each suspended run is parked on (signal, webhook, child, breakpoint), for the ones parked on something. Against an older control plane the list rows carry no waiting stamp.
subscribesubscribe(runId: string, onEvent: (event: EngineEvent) => void): () => voidLive lifecycle events of one run (the dashboard's live tail).
ExportSignatureNotes
StoreRunGatewaynew StoreRunGateway(engine: RunGatewayEngine, options?: { role?: 'standalone' | 'control-plane' })The store-backed gateway the provider binds on the store roles.
ProxyRunGatewaynew ProxyRunGateway(transport: ProxyTransport, options: { partition: string; token?: string; requestTimeoutMs?: number })The tenant pod's gateway. requestTimeoutMs is the config's requestTimeoutMs.
RunRequestRespondernew RunRequestResponder(transport: ResponderTransport, gateway: RunGateway, options?: { verifyTenant?; subscribeEngineEvents? })Answers proxied run requests on the control plane; a request it cannot serve is answered UNSUPPORTED_RUN_REQUEST.
signTenantTokensignTenantToken(tenant: string, secret: string, opts?: { ttlMs?: number; now?: number }): stringMint a pod's HMAC tenant claim (config.tenant.token); ttlMs makes it expire.
hmacTenantVerifierhmacTenantVerifier(secret: string | string[], capabilities?: string[]): TenantVerifierThe matching verifyTenant. A list of secrets allows rotation. See Layered tenant authentication.

Run detail and fan types

What getRunDetail and getFanMembers return, on the engine and the RunGateway, and the helpers they are built from.

Type / exportShapeNotes
RunDetail{ run, timeline, children, fans?, childCount?, etag?, notModified? }When summarized, the members of a wide fan are left out of timeline and described in fans, and children lists at most SUMMARY_CHILD_LIMIT (200) ids, childCount the total. notModified: true comes back with an empty body when ifNoneMatch matched.
RunDetailOptions{ summarizeFansOver?: number; ifNoneMatch?: string }Summarize every fan with more members than summarizeFansOver. ifNoneMatch is ignored when no etag can be computed (no summarizeFansOver, or a store without checkpoint aggregates).
FanSummary{ group, label, firstSeq, lastSeq, size, total?, notStarted?, failFast, counts, startedAt?, finishedAt?, failures }One summarized fan; failures holds at most FAN_SUMMARY_FAILURES (20) failed members.
FanPageQuery{ offset?, limit?, failedOnly?, ifNoneMatch? }offset defaults to 0, limit to 100, clamped to 1–500.
FanPage{ items: StepCheckpoint[]; total: number; etag?; notModified? }One page of a fan's members in seq order.
summarizeTimeline / summarizeFan / pageFansummarizeTimeline(timeline, over) → { timeline, fans }; summarizeFan(group, members) → FanSummary; pageFan(timeline, group, query) → FanPageThe in-memory versions, over a loaded checkpoint list.
summarizeRunFromStore / pageFanFromStore(store, runId, over | group, …)The same, read from a store's checkpointStats / queryCheckpoints aggregates.
runDetailEtag / fanPageEtag / etagMatches→ string / → string / (ifNoneMatch, etag) → booleanThe ETags the detail and fan reads carry, and the weak If-None-Match comparison (* included).
childIdsOf / parentRunIdOfchildIdsOf(checkpoints): string[] / parentRunIdOf(runId): string | undefinedThe child ids a timeline references; the parent of an engine-minted child id (also stamped as parentRunId on diagnostics events).

DurableConfig

defineConfig(config) in config/durable.ts returns a DurableConfig, a union discriminated on role: StandaloneConfig (the default when role is omitted), ControlPlaneConfig or TenantConfig. All roles share the fields below. See Roles & config.

config/durable.ts
import { defineConfig, stores, transports } from '@adonis-agora/durable'

export default defineConfig({
  transport: 'queue',
  transports: { queue: transports.queue({ connection: 'main' }) },
  store: 'lucid',
  stores: { lucid: stores.lucid({ connection: 'pg' }) },
})
OptionTypeDefaultNotes
role'standalone' | 'control-plane' | 'tenant''standalone'Explicit topology, never inferred. See Cluster.
transportstringin-processKey of transports to dispatch over. See Transports.
transportsRecord<string, TransportFactory>—Named transports, built with the transports factory.
storestringin-memoryKey of stores for runs, checkpoints and timers. See State stores.
storesRecord<string, StoreFactory>—Named stores, built with the stores factory.
autoSchemabooleantrueProvision the store's schema at boot with ensureSchema(). Set false to migrate with createDurableTables yourself. See Who provisions the schema.
controlPlaneControlPlane | ControlPlaneFactorysingle-instanceCross-instance lifecycle and cancellation broadcast. See Control plane.
admissionAdmissionBackend | AdmissionFactoryin-process capsBackend for flow-control queues; admissions.redis(...) makes caps fleet-wide. See Distributed admission.
queuesQueueConfig[]noneFlow-control queues registered at boot, the declarative engine.registerQueue. A duplicate name throws at boot; a later engine.registerQueue of the same name replaces it. See Flow control.
deadLetterWorkflowWorkflowRef (name, class or ref)noneFleet-default handler for dead runs, started as dlq:<deadRunId> with a DeadLetter input in the dead run's namespace. A workflow's deadLetter() method or own deadLetterWorkflow wins. See Dead-letter queue.
webhookUrl(token: string) => stringnoneBuilds DurableWebhook.url for ctx.webhook(). See Durable webhooks.
traceparent() => string | undefinedwired by @adonis-agora/diagnostics-otel when installedW3C traceparent stamped on dispatched tasks. See OpenTelemetry.
trackStepStartbooleantruePersist a running checkpoint when a local step starts; false halves checkpoint writes on hot paths.
leaseMsnumber30000Recovery lease duration. See Delivery under multiple instances.
instanceIdstringrandomUnique per process; run leases are held by it.
namespacestring'default' on standalone; unset (operator) on control-planeWorker-pool partition; poll and recovery paths act only on this namespace. A control-plane with no namespace is the operator and drives every pool (before 0.51 an unset namespace meant operator on every role). See Tenancy.
maxRecoveryAttemptsnumberunlimitedCrash-recovery pickups before a run moves to dead. See Dead-letter queue.
compensationRetriesnumber1Attempts per saga compensation on run failure. See Retrying a transient undo.
compensationTimeoutMsnumber300000Upper bound on awaiting one dispatched compensation without its own timeoutMs.
retention{ completed?, failed?, cancelled?, dead?: string | number; policies?: Array<{ statuses: TerminalRunStatus[]; maxAge?: string | number; maxCount?: number; scope: RunScope }> }keep everythingHard-delete terminal runs past an age, per status or per scope. A policy needs maxAge and/or maxCount (keep the N newest by start time within the scope). See Run retention.
stalledAfterstring | number'15m'Idle time before onStalled listeners are paged.
runDispatcherRunDispatcherin-processWhere a freshly started run executes. Wins over drive.
drivebooleantruefalse: this process never executes the runs it starts, requeues or recovers (no-op dispatcher); they stay pending for durable:work or a control plane. Directly requested resumes (a signal or webhook handled here) still execute here. Throws at boot with worker: { embedded: true }. durable:work drives regardless. See Where a run actually executes.
drainTimeoutMsnumber10000Shutdown budget: on app shutdown every store-backed process (standalone, control-plane) waits this long for in-flight executions before closing the transport and control plane. A worker.drainTimeoutMs, when set, takes precedence; the embedded loop drains within that budget and is not drained twice.
consumers'auto' | 'always' | 'never''auto'When this process's transport starts broker consumers; 'auto' defers in console/repl. See consumers.
remoteRedispatchMsnumberoffOpt-in self-heal: re-dispatch a no-timeoutMs remote step still pending past this window. The step must be idempotent. See Failure modes.
remoteRedispatchMaxnumber10Re-dispatches per lost step before failing it (remote_step_lost).
remoteAdvanceSilenceMsnumberoffLiveness window of a remote workflow turn: with no decision and no run-scoped heartbeat for this long, the turn is re-dispatched (fresh taskId, same history) instead of waiting forever; heartbeats rearm it. A RemoteWorkflowExecutor's timeoutMs is the per-workflow version (the shorter wins). See Remote workflow turns.
timerBatchSizenumber500Most due timers one timer pass loads, most overdue first.
eventBufferTtlstring | numberkeep until consumedAge after which a buffered event is expired: never delivered, and deleted by the worker tick's retention phase (StateStore.pruneBufferedEvents).
scopeReadsbooleanfalseFence the store's run reads to namespace (a tenant-boundary view) through the store's withScope. Writes are unaffected. See Lucid store.
reconcileMsnumber300000Fallback re-drive for a run suspended on an event (child, signal, remote result) whose wake-up was lost; also the wake a recovered run with a remote step in flight is parked on (a remote workflow's run whose turn was lost is re-driven after about a second instead). 0 disables it; a non-finite or negative value falls back to the default.
blockedPollMsnumber5000How often a run parked blocked (no capable, compatible live worker) re-checks the fleet. A non-finite or sub-1 value falls back to the default.
schedulesScheduledWorkflow[]noneRecurring workflows (interval or cron), merged with colocated static schedule. Name each workflow with a lazy import, () => import('#workflows/…') (resolved when the worker starts, after boot), a class, or a registered name; ScheduledWorkflow<LazyWorkflowImport<typeof X>> types input. See Scheduling.
persistedSchedulesbooleanfalseFire the runtime-managed engine.schedules on each worker tick.
workerEmbeddedWorkerConfig ({ embedded?: boolean; intervalMs?: number; drainTimeoutMs?: number })no embedded loopRun the worker loop inside the web process. See Running the loop in the web process.
workflowsPathstring | string[] | false'app/workflows'Directory (or directories) scanned for BaseWorkflow classes; false or [] disables discovery. A workflow name + version defined in two directories fails boot. See app/workflows.
stepsPathstring | string[] | false'app/steps'Directory (or directories) scanned for @Step classes and defineStep handlers; false or [] disables. A step name defined in two directories fails boot.

Role-specific fields:

RoleFieldTypeNotes
control-planestore, transportstringBoth required.
control-plane, standaloneverifyTenantTenantVerifierVerifies tenant tokens on incoming requests (a standalone engine answers tenant pods too when its transport carries the store-less protocol). See Layered tenant authentication.
tenantstore, storesneverA store-less pod may not name a store; any value is a compile error.
tenanttransport, partitionstringBoth required. partition suffixes the pod's <name>@<tenant> routing tokens.
tenanttenant{ token?: string }The pod's signed claim.
tenantcapabilitiesstring[]Extra advertised features beyond registered handler names.
tenantrequestTimeoutMsnumberProxyRunGateway round-trip timeout. See Thin workers.

Driver factories

Each factory returns a lazy thunk, so a driver's peer dependency is imported only when that driver is selected.

FactorySignatureNotes
transports.memorymemory(_config?: MemoryTransportConfig): TransportFactoryTest-only in-process transport (the default when no transport is named). See In-process for tests.
transports.eventEmittereventEmitter(config?: EventEmitterTransportConfig): TransportFactoryProduction in-process transport and control plane over a Node EventEmitter. See Single-process production.
transports.queuequeue(config?: QueueTransportConfig): TransportFactoryCross-process over @adonisjs/queue. See Queue transport.
transports.bullmqbullmq(config: BullMQTransportConfig): TransportFactoryCross-process over BullMQ (bullmq / ioredis imported lazily). Its options include pingIntervalMs (default 30000) and the opt-in stepTimeoutMs, a wall-clock ceiling per step handler past which the step fails retryable (wedged steps). See BullMQ transport.
transports.dbdb(config?: DbTransportConfig): TransportFactoryCross-process over the app's database through @adonisjs/lucid, no broker. See DB transport.
stores.lucidlucid(config?: LucidStoreConfig): StoreFactoryPersist in SQL through @adonisjs/lucid ({ connection?: string; collation?: string }). See Lucid store.
controlPlanes.redisredis(config?: RedisControlPlaneConfig): ControlPlaneFactoryLifecycle and cancellation broadcast over @adonisjs/redis pub/sub. See Multi-replica: controlPlanes.redis.
admissions.redisredis(config?: RedisAdmissionConfig): AdmissionFactoryFleet-wide flow-control caps in one atomic Lua script on Redis. See Distributed admission.

The concrete classes (InMemoryTransport, EventEmitterTransport, QueueTransport, BullMQTransport, DbTransport, InMemoryStateStore, LucidStateStore, RedisControlPlane) are exported too, for passing an instance directly or writing tests. Schema helpers for a hand-written migration: createDurableTables(db: Database, connectionName?: string, options?: CreateDurableTablesOptions): Promise<void>, dropDurableTables(db: Database, connectionName?: string): Promise<void> (CreateDurableTablesOptions is { logger?, collation?, force? }; DURABLE_SCHEMA_REVISION, DURABLE_SCHEMA_META_TABLE and DURABLE_JSON_COLUMNS describe what it manages), onCorruptJsonColumn(reporter) to route the Lucid store's report of a JSON column that does not parse, createDurableTransportTables(db: Database): Promise<void> and dropDurableTransportTables(db: Database): Promise<void>. See Lucid store and DB transport.

Observability building blocks

Root exports for wiring the engine's lifecycle events into your own tooling. The /otel and /telescope subpaths hold the heavier integrations.

ExportSignatureNotes
collectMetricscollectMetrics(engine: { subscribe }): MetricsCollectorDependency-free counters; .prometheus() returns the text exposition, .snapshot() the raw MetricsSnapshot, .stop() unsubscribes. Counts only this instance's events. See Metrics.
attachDurableDiagnosticsattachDurableDiagnostics(engine: DurableEventSource, options?: { includeRemote?: boolean }): () => voidBridge every EngineEvent onto the @adonis-agora/diagnostics bus as durable / <event type>. The provider attaches it for you. includeRemote (default false) also forwards events heard from other instances over the control plane. See Diagnostics.
isRemoteEventisRemoteEvent(event: EngineEvent): booleantrue for an event another instance produced and the control plane re-delivered here. Filter on it in a listener that records or counts, so a fleet counts each event once.
withScheduleOrigin / asHeartbeat<T>(fn: () => T) => TLabel fn as scheduler-driven work, or as a liveness probe an observability tool may drop. A no-op unless a driver (Telescope's) is installed.
currentStepcurrentStep(): StepLogger | undefinedThe running step's logger. See currentStep().

Command building blocks

The pieces durable:work, durable:runs and durable:retry are built from, exported so an app can compose its own command or scheduler without re-implementing the loop. See Building your own.

ExportSignatureNotes
runTickrunTick(engine: WorkflowEngine, options?: { now?: number; schedules?: ScheduledWorkflow[] }): Promise<TickResult>One worker tick, in phases: runPending, recoverIncomplete, resumeDueTimers, sweepTimeouts, schedules (config and, with persistedSchedules, persisted), sweepRetention, sweepStalled. TickResult is { pending, recovered, timers, scheduled, evicted, errors: { phase, error }[] }; a failing phase does not stop the others.
runWorkerLooprunWorkerLoop(engine: WorkflowEngine, options: WorkerLoopOptions): Promise<number>runTick every intervalMs until stopSignal resolves, then drains. Starts deferred consumers first. WorkerLoopOptions is { intervalMs, stopSignal, logger: WorkerLogger, sleep?, schedules?, … }.
listRunslistRuns(source: RunLister, opts: { status?, statuses?, workflow?, limit? }): Promise<WorkflowRun[]>The durable:runs query.
attachLiveness / filterStaleattachLiveness(source, runs, now?): Promise<RunLiveness[]> / filterStale(liveRuns, thresholdMs = DEFAULT_STALE_MS)Add each run's age and oldest pending remote step; keep the ones idle past the threshold (DEFAULT_STALE_MS is 15 minutes).
renderRunsTable / staleHint / parseDurationMsrenderRunsTable(liveRuns): string, staleHint(runId): string, parseDurationMs(input): number | undefinedThe durable:runs table, its remediation hint, and its --stale parser.
retryRunretryRun(engine, runId): Promise<RunResult | null>engine.requeue(runId), what durable:retry calls.

Discovery helpers

What the provider runs over workflowsPath / stepsPath, for a directory it does not scan (a preload, a package). See Registering an extra directory by hand.

ExportSignatureNotes
registerWorkflowsFromDirregisterWorkflowsFromDir(engine, dir: string | string[], factory?): Promise<WorkflowMeta[]>Import every module under dir and register its BaseWorkflow classes. The returned metadata carries each workflow's resolved origin.
registerWorkflowsFromBarrelregisterWorkflowsFromBarrel(engine, barrel, factory?, options?: { appRoot?: string | URL }): Promise<WorkflowMeta[]>The same over a generated barrel (hooks/workflows). appRoot is the app the barrel was generated for: its modules derive their origin from it. Without it they're attributed to the calling package.
registerStepsFromDir / registerStepsFromBarrel(server: StepServer, dir | barrel) => Promise<DurableStepMeta[]>Serve every @Step class and defineStep handler found. The engine is a StepServer.
registerWorkflowClass / registerStepsregisterWorkflowClass(engine, cls, factory?) / registerSteps(server, exports)Register one class, or the steps in a list of module exports.
parseDurationparseDuration(duration: string | number): numberThe duration parser every '30s' / '7 days' option goes through.

Errors

ErrorThrown when
FatalErrornew FatalError(message, code?, details?). Throw it yourself in a step or workflow for an unrecoverable business failure (e.g. a declined card). It is never retried, in a local or a dispatched step, and fails the run at once. code and details travel with the failure to the run's error, an allSettled item, a ctx.child catch and an onError handler. See Fatal errors.
SignalTimeoutErrorThe timeoutMs of ctx.waitForSignal, ctx.waitForEvent, ctx.onUpdate or a webhook's wait passes before delivery (token, timeoutMs). Catch it to take a default branch.
NonDeterminismErrorOn resume, the step at a position has a different name or kind than its checkpoint: the code changed under an in-flight run without a new version (code: 'non_determinism', runId, seq). Never routed by onError. See The non-determinism guard.
NestedWorkflowCallErrorA ctx primitive (or a BaseWorkflow.start() / .dispatch() through the ambient ctx) was called from inside a step body (ctx.localStep, transaction, sideEffect, now, task's dispatch). A FatalError with code nested_workflow_call (runId, primitive, step). See Workflow primitives inside a step body.
SchemaValidationErrorA value failed a declared Standard Schema: a step's input / output, a workflow's inputSchema (thrown by start, nothing created), or its searchAttributes schema (thrown by ctx.upsertSearchAttributes). A FatalError, never retried, whose code says which schema rejected the value: invalid_input (a step's input, a workflow's inputSchema), invalid_output (a step's output) or schema_validation (a searchAttributes write, a direct validateStandard call). Its static code is schema_validation, and an onError route for it also catches invalid_input and invalid_output that no route of their own matches; issues / details.issues hold { message, path: string[] }[].
ConcurrencyLimitErrorstart whose quota key already has limit runs in flight (workflow, key, limit, active). Nothing was created. See Concurrency quotas.
SingletonQueueFullErrorstart on a singleton workflow whose in-flight plus gated backlog equals limit + maxQueueDepth (workflow, key, maxQueueDepth). See Singleton workflows.
GatherErrorctx.all: one or more children or steps of a fan failed. failures holds GatherItemFailure items ({ index, id, error, code?, details? }). See Child workflows.
RemoteStepTimeoutA dispatched step produced no result or heartbeat within timeoutMs (stepId, timeoutMs). Retryable per the step's retries. See Retries & backoff.
RemoteStepErrorRaised inside the workflow when a remote worker reports a step failure (stepError?: StepError).
RemoteWorkflowTimeoutAn inline remote workflow advance produced no decision or run-scoped heartbeat within remoteAdvanceSilenceMs (taskId, timeoutMs). Not a failure: the engine releases the lease and crash recovery re-drives the run. With a call of the lost turn still in flight, recovery parks the run on a wake about a second out (not the reconcileMs window) and the timer pass dispatches a fresh turn; the call is not dispatched again.
NamespaceMismatchengine.resume on a run that belongs to a different namespace. See Tenancy.
DuplicateDiscoveryErrorBoot, when two workflowsPath / stepsPath directories (or two roots of a generated barrel) define the same workflow name + version, or the same step name, with different classes or handlers (kind, id, files). The message names both files. See Several directories.
ScheduleAlreadyExistsError / ScheduleNotFoundErrorengine.schedules.create on a taken id / any engine.schedules.* call on an unknown id (scheduleId).
ContinueAsNewThrown by ctx.continueAsNew(input) to unwind the run and hand off to a fresh execution. A control-flow signal.
WorkflowSuspendedThrown by the engine to stop a turn and persist the suspension (wakeAt, waitSeqs). A control-flow signal.

The thin-worker turn runtime (@adonis-agora/durable/worker, also re-exported from the root) has its own set: WorkflowStepFailedError, WorkflowGatherFailedError, WorkflowNondeterminismError and WorkflowTurnCancelled. See Thin workers.

Control-flow signals

WorkflowSuspended and ContinueAsNew are thrown internally to unwind the current turn (a durable sleep, a still-pending step, ctx.continueAsNew). They are not failures. Because they use the same exception mechanism as a real step failure, a workflow's own try/catch around ctx.step, ctx.sleep and so on can catch one, and it must rethrow it untouched before running any cleanup. Otherwise the cleanup is recorded as extra history during what replay later sees as a plain suspend, and the run fails with NonDeterminismError on resume. See Catching errors and Sagas.

ExportSignatureNotes
CONTROL_FLOW_SIGNALconst CONTROL_FLOW_SIGNAL: unique symbolSymbol.for('@agora/durable:control-flow'), the global-registry marker stamped on every control-flow signal class. Rarely used directly.
isWorkflowControlFlowSignalisWorkflowControlFlowSignal(error: unknown): booleantrue for WorkflowSuspended and ContinueAsNew, whichever module copy threw them. Not true for a cancellation or a FatalError / step rejection, which are real outcomes a catch may handle.
try {
  await ctx.step(chargeCard, input)
} catch (error) {
  if (isWorkflowControlFlowSignal(error)) throw error // suspend / continue-as-new: rethrow as-is
  await ctx.step(refund, input) // a real failure: safe to compensate
  throw error
}

Statuses

Run status (RunStatus)

StatusMeaning
pendingCreated and enqueued by start, not yet picked up.
runningA worker is executing (or replaying) the body.
suspendedParked with zero compute on a sleep, a signal, event or update, an awaited step or a child.
blockedParked because no live worker can run the next dispatch (a required capability is unadvertised, or every live worker is protocol-incompatible). Not terminal: the blocked-recovery poll re-drives it once a capable worker appears. See Handshake.
completedFinished successfully. Terminal.
failedFinished with an unrecovered error. Terminal.
cancelledCancelled, with or without compensation, or past its executionTimeout. Terminal.
deadDead-lettered: crash recovery gave up after maxRecoveryAttempts. Terminal.

Only completed, failed, cancelled and dead are terminal (TerminalRunStatus, listed in TERMINAL_RUN_STATUSES). Only those are eligible for retention pruning, and they never transition on their own again.

Checkpoint status (StepCheckpoint['status'])

StatusMeaning
pendingA remote step was dispatched and awaits its worker's result; the run is durably suspended.
runningA local step's body is executing in-process right now (see trackStepStart).
completedSettled successfully. Short-circuits on replay: the step is not re-executed.
failedSettled with an error. Re-awaited (remote) or re-run (local) on replay unless retries are exhausted.

Subpath exports

Import pathWhat it provides
@adonis-agora/durableEverything on this page: ctx types, BaseWorkflow, decorators, WorkflowEngine, defineConfig, factories, errors, statuses.
@adonis-agora/durable/services/mainThe WorkflowEngine singleton (default export, undefined on a tenant pod) and runGateway. See Roles & config.
@adonis-agora/durable/durable_providerThe AdonisJS provider registered by node ace configure. See Getting started.
@adonis-agora/durable/dashboard_providerThe dashboard provider. See Dashboard.
@adonis-agora/durable/dashboardDashboard building blocks: its own defineConfig / resolveConfig / defaultAuthorize, the framework-agnostic handlers (listRuns, getRun, getRunEvent, cancelRun, retryRun, retryWithInputRun, continueRun, redispatchPendingRun, bulkAction, runValues, workers, topology, health, metricsText, deliverWebhook), dashboardEngineForRole / gatewayDashboardEngine, the auth helpers (resolveDashboardAuth, performLogin, performSession, readSession, SESSION_COOKIE_NAME) and the access-denied / login page renderers. See Dashboard and Dashboard auth.
@adonis-agora/durable/workerThe store-less worker entry: WorkerRuntime, step registration helpers (registerSteps, registerStepsFromDir, registerStepsFromBarrel), runWorkflowTurn and its errors, the worker registries (RedisWorkerRegistry, NoopWorkerRegistry) and the descriptor / routing-token helpers. See Thin workers.
@adonis-agora/durable/otelattachDurableOtel, attachDurableMetrics, otelTraceparent. See OpenTelemetry.
@adonis-agora/durable/telescopedurableTelescopeExtension, durableDashboard, durableSchedules, the data providers behind its widgets (durableStateProvider, durableWorkerHealthProvider, durableWorkerStatusProvider, durableTimeseriesProvider, …) and dedupeRunEvents. See Telescope.
@adonis-agora/durable/testingcreateTestEngine, MutableClock, assertRunStatus, assertOutput, assertStepsRan, assertStepAttempts, recordedSteps, failOnce, failTimes, captureHistory, parseRunHistory, assertReplayable, and the transport conformance kit assertTransportConformance. See Testing.
@adonis-agora/durable/testing/conformanceState-store, admission-backend and run-queue conformance kits: runStateStoreContract, runAdmissionBackendContract, runRunDispatchContract, and StateStoreUnavailableError (throw it from a store factory to skip the contract when the backend is down). See Conformance kits.
@adonis-agora/durable/hooks/workflows / @adonis-agora/durable/hooks/stepsAssembler hooks that generate the app/workflows and app/steps barrels (default exports; workflowsHook(options) / stepsHook(options) take { source?, importAlias?, output? }, output .adonisjs/durable/workflows.ts / steps.ts). See app/workflows.
@adonis-agora/durable/admission-redisRedisAdmissionBackend, for passing an instance directly. See Flow control.
@adonis-agora/durable/control-plane-redisRedisControlPlane, for passing an instance directly. See Control plane.
@adonis-agora/durable/commandsThe ace commands: durable:work, durable:worker, durable:runs, durable:retry, durable:export and make:workflow. See CLI.
@adonis-agora/durable/configureThe node ace configure hook (also re-exported from the root).

On this page

Partner

Your product as the official integration for Durable, right here.

Become a partner →