Event-triggered workflows
Start a workflow when an external event fires — an AdonisJS emitter event (`@OnEvent`) or a `@adonis-agora/diagnostics` channel (`@OnDiagnostic`) — with the event payload as the run input.
A scheduled workflow starts on time; an event-triggered workflow starts when something happens — a payment succeeded, a webhook arrived, a document changed. Instead of you wiring "event → engine.start(...)" by hand, the workflow class declares its triggers and the durable provider bridges the event source into the engine for you.
Two sources are supported:
- the AdonisJS emitter (
@OnEvent) — exact event names, - the
@adonis-agora/diagnosticsbus (@OnDiagnostic) —agora:<lib>:<event>channels, exact or regex, plus "every event of a lib".
Authoring — decorators and static on
import { BaseWorkflow, OnEvent, OnDiagnostic } from '@adonis-agora/durable'
@OnDiagnostic({ lib: 'payments', event: 'payment.succeeded' })
@OnEvent({ event: 'agora:payments:payment.succeeded' })
export class ProcessPaymentWorkflow extends BaseWorkflow {
static workflow = { name: 'process-payment' }
async run(ctx: WorkflowCtx, input: { externalReference: string }) {
// input is the event payload
}
}Both forms are supported, exactly like schedules:
@OnEvent({ event })/@OnDiagnostic({ lib, event? })— named-parameter decorators, and- a colocated
static on = [...]literal (the decorators only stampstatic on):
static on = [
{ source: 'emitter', event: 'agora:payments:payment.succeeded' },
{ source: 'diagnostics', lib: 'payments', event: 'payment.succeeded' },
{ source: 'diagnostics', lib: 'payments', event: /^payment\./ }, // regex over events
{ source: 'diagnostics', lib: 'payments' }, // every event of the lib
]Repeated decorators and an existing static on accumulate (decorators prepend, top-first).
Source capabilities
The Adonis emitter has no wildcard, so @OnEvent matches exact names only. Regex / "every event" is available on the diagnostics bus, whose channel registry the bridge subscribes to (current and future channels).
How it fires
- Exact triggers route through
engine.publishEvent(canonical, payload)— a fresh run of every workflow registered with that canonicalonEventname, with the payload as input. Idempotent byevt:<id>:<workflow>(durable's own dedup for redelivered events). - Regex / any triggers start the workflow directly with the event payload (the concrete channel name isn't known at registration time).
The diagnostics channel envelope's payload becomes the run input. The payload type is whatever the emitting library published (e.g. @adonis-agora/payments' payment.succeeded payload with externalReference).
Wiring
No config needed. The provider reads the triggers from discovered workflow classes (static on) and bridges the app's emitter (when resolvable) + the diagnostics channels at boot. If the emitter is unavailable, emitter triggers are inert; diagnostics triggers work regardless (they use node:diagnostics_channel directly).
Event payloads → workflow input
The webhook-style flow that motivated this: a payment provider publishes payment.succeeded on agora:payments:payment.succeeded (via its diagnostics bridge); the workflow below runs once per event, with the payment's externalReference — no controller, no manual engine.start:
@OnDiagnostic({ lib: 'payments', event: 'payment.succeeded' })
export class ProcessPaymentWorkflow extends BaseWorkflow {
static workflow = { name: 'process-payment' }
async run(ctx: WorkflowCtx, input: { externalReference?: string }) {
if (!input.externalReference) return
// grant the purchased credits / activate the subscription — durable owns retry + exactly-once
}
}Scheduling
Recurring workflows with ScheduledWorkflow — fixed intervals via everyMs or DST-aware cron via cron + timezone — registered under schedules in config/durable.ts and fired by the durable:work worker tick, started exactly once per window by an idempotent time-bucket run id, with runtime pause/resume/trigger control via engine.listSchedules, setSchedulePaused and triggerSchedule.
Reliability
How @adonis-agora/durable keeps long-running work correct in the face of transient failures, crashes and overload — step retries, saga compensation, durable flow-control queues and the dead-letter queue.