Triggers
What starts a workflow - event triggers with CEL filters and wildcards, and cron schedules.
A workflow declares what starts it with a triggers array. A trigger is either an event trigger
(fires on a matching event) or a cron trigger (fires on a schedule). A workflow can mix several.
const fulfillment = defineWorkflow<OrderData>({
name: "fulfillment",
triggers: [
{ event: "order.created" },
{ event: "order.reprocessed", if: "event.data.total > 100" },
{ cron: "TZ=UTC 0 9 * * *" },
],
handler: async (ctx) => {
// ctx.event.name tells you which event (or cron) started this run
},
});If you omit triggers, the workflow is started by an event whose name equals the workflow name - the
default, so nothing changes for workflows that don't opt in.
Event triggers
An event trigger fires when an incoming event's name matches event, optionally gated by an if
filter. One event fans out to every matching workflow, and a workflow with several matching triggers
runs once.
An event reaches the engine two ways - from inside another workflow with the SDK, or from outside over the REST API:
await ctx.step.emit("reprocess", {
name: "order.created",
app: "order-app",
data: { orderId: "A1", total: 250 },
});curl -X POST $DURABLEX_ENGINE_URL/events \
-d '{"name":"order.created","app":"order-app","data":{"orderId":"A1","total":250}}'You can also fire an event by hand from the console's Events view with Trigger event: enter
the event name, an optional app, and a JSON payload. It posts the same /events request, so every
workflow whose trigger matches the name runs - handy for exercising a workflow without wiring up a
producer. The dialog reports the run it woke and each workflow the event fanned out to.
Wildcards
An event name can end in a single trailing * to match a prefix:
triggers: [{ event: "order.*" }] // order.created, order.shipped, order.refunded, ...The * is allowed only as the final character. There is no mid-string match and no multi-segment ** -
the rule stays simple and predictable.
Filters
if is a CEL expression evaluated against the event. It sees one variable, event,
with event.name and event.data:
triggers: [{ event: "order.created", if: 'event.data.total > 100 && event.data.tier == "pro"' }]The filter is an admission gate: if it isn't true, the workflow doesn't start for that event. It
runs once at ingest and never again on replay, so it must not depend on anything but the event.
Cron triggers
A cron trigger fires the workflow on a schedule - no event needed.
triggers: [{ cron: "0 9 * * *" }] // every day at 09:00 (engine's local zone)
triggers: [{ cron: "TZ=Europe/Paris 0 9 * * *" }] // 09:00 Paris time
triggers: [{ cron: "@every 30m" }] // every 30 minutesThe schedule is a standard 5-field cron expression or an @every <duration> descriptor, with an optional
TZ=/CRON_TZ= timezone prefix. A cron run receives { cron, scheduledFor } as its event data and the
cron spec as event.name.
Missed ticks are not backfilled: if the engine is down across several ticks, it fires once on
recovery and moves on (skip-and-forward), and overlapping schedules fire a given tick only once. Cron
runs flow through the same retries and flow control as event-triggered runs - pair a frequent cron with
singleton to keep a slow job from overlapping itself.
A scheduled workflow is marked in the console with a clock badge and its next run time, and each
cron run carries triggerKind: "cron" (event-triggered runs are "event"). The runs list can be filtered
to one kind with ?runType=cron / ?runType=event.
Which trigger fired
Because a trigger can start a workflow whose name differs from the event, the handler always dispatches by
the workflow, while ctx.event.name tells you what triggered this run (the event name, or the cron spec
for a scheduled run).
Limits
Wildcards and filters apply to triggers only - step.waitForEvent still rendezvouses on an exact event
name. step.emit publishes through the same matching path, so an emitted event can fan out to
wildcard-matching workflows; watch for accidental cycles. A cycle is bounded - an emit-triggered run
inherits the emitting run's depth, so it hits the same invoke-depth cap as step.runWorkflow and fails
rather than looping forever - but a tight cycle still burns runs up to that cap, so avoid it by design.