Skip to content

Events

duckflux supports event-driven communication through two complementary primitives: emit (publish) and wait (subscribe). Together they allow steps to communicate asynchronously — within the same workflow, across parallel branches, or between parent and sub-workflows.


The emit participant type publishes a named event to the workflow’s event hub.

FieldTypeRequiredDescription
eventstringYesEvent name to publish. Dot-notation is supported (e.g. order.created).
payloadstring or objectNoEvent payload. A single CEL expression or a map of CEL expression values.
ackbooleanNoIf true, blocks until delivery is confirmed. Default: false.
onTimeoutstringNoBehavior when the ack deadline expires: fail (default) or skip. Only applies when ack: true.

The simplest mode. The event is dispatched and execution continues immediately:

participants:
notifyBuild:
type: emit
event: "build.completed"
payload:
branch: workflow.inputs.branch
status: build.output.status

With ack: true, the step blocks until the event hub confirms the message was persisted. Combine with timeout to limit how long to wait:

participants:
notifyCritical:
type: emit
event: "deploy.started"
payload: deploy.output
ack: true
timeout: 10s
onTimeout: skip

When the ack times out:

  • onTimeout: fail (default) — the step fails and follows the configured onError strategy.
  • onTimeout: skip — the step succeeds with ack: false in its output.

A single CEL expression passes the value directly:

payload: coder.output

A structured object maps individual fields:

payload:
taskId: workflow.inputs.taskId
status: coder.output.status
startedAt: execution.startedAt

The emit participant produces a result map with three fields:

FieldTypeDescription
eventstringThe event name that was published.
payloadanyThe resolved payload that was sent.
ackbooleantrue if acknowledged delivery succeeded, false if it timed out with onTimeout: skip.
flow:
- notifyCritical
- if:
condition: notifyCritical.output.ack == false
then:
- fallbackNotify

The wait construct pauses execution until an event arrives on the hub. It is defined directly in the flow — not as a participant.

FieldTypeRequiredDescription
eventstringYesEvent name to listen for.
matchCEL expressionNoFilter expression evaluated against the event payload. Only matching events unblock the wait.
timeoutdurationNoMaximum time to wait before invoking onTimeout.
onTimeoutstringNoBehavior on timeout: fail (default), skip, or a participant name to redirect to.
flow:
- wait:
event: "order.created"
match: event.orderId == workflow.inputs.expectedOrderId
timeout: 30s
onTimeout: fail

Inside a wait block, the received event payload is accessible as the event variable. After the wait completes, event remains available in subsequent CEL expressions and becomes the chain value for the next step.

flow:
- wait:
event: "approval.response"
match: event.requestId == submitRequest.output.id
timeout: 24h
# event payload is now the chain input for the next step
- as: handleApproval
type: exec
run: ./process-approval.sh

The wait construct supports three distinct modes depending on which fields are present.

Subscribes to the event hub and blocks until a matching event arrives:

- wait:
event: "payment.processed"
match: event.transactionId == order.output.txId
timeout: 5m
onTimeout: fail

When only timeout is present, the step acts as a simple sleep:

- wait:
timeout: 30s

Periodically evaluates a CEL expression until it returns true:

- wait:
until: now >= timestamp("2026-06-01T09:00:00Z")
poll: 1m
timeout: 48h
onTimeout: fail
FieldTypeDescription
untilCEL expressionCondition to evaluate. Wait ends when this returns true.
polldurationInterval between evaluations.
timeoutdurationMaximum total wait time.

The most common event pattern: one step emits an event and a later step (or a parallel branch) waits for it.

participants:
placeOrder:
type: emit
event: "order.created"
payload:
orderId: "ORD-001"
total: 99.95
flow:
- placeOrder
- wait:
event: "order.created"
match: event.orderId == "ORD-001"
timeout: 5s
onTimeout: fail
- as: confirm
type: exec
run: echo "Order received"

When delivery confirmation matters, use ack: true on the emit side:

participants:
publisher:
type: emit
event: "payment.processed"
ack: true
timeout: 5s
onTimeout: fail
payload:
transactionId: "TXN-42"
status: "approved"
flow:
# Start a wait before emitting — the hub replays past events
- wait:
event: "payment.processed"
timeout: 5s
onTimeout: skip
- publisher
output: publisher.output.ack

Events are shared between parent and sub-workflows. The event hub instance is passed down to all type: workflow participants, so events emitted in a parent are visible to sub-workflows and vice versa.

parent.duck.yaml
participants:
trigger:
type: emit
event: "job.started"
payload:
jobId: "42"
child:
type: workflow
path: ./child.duck.yaml
flow:
- trigger # publishes job.started
- child # child can wait for job.started
child.duck.yaml
flow:
- wait:
event: "job.started"
match: event.jobId == "42"
timeout: 10s
- as: process
type: exec
run: echo "Processing job"

Events work across parallel branches within the same workflow. One branch can emit an event that another branch is waiting for:

flow:
- parallel:
- as: producer
type: emit
event: "data.ready"
payload:
source: "branch-a"
- type: exec
run: echo "independent work"
- wait:
event: "data.ready"
timeout: 5s

The onTimeout field on a wait step controls what happens when the deadline expires:

ValueBehavior
failThe step fails. The workflow’s onError strategy applies. This is the default.
skipThe step is marked as skipped and execution continues.
<participant>Execution is redirected to the named participant as a fallback.
- wait:
event: "approval.response"
timeout: 24h
onTimeout: sendReminder

A workflow that publishes an order event, waits for payment confirmation, and then proceeds with fulfillment:

id: order-pipeline
name: Order Pipeline
version: "1"
inputs:
orderId:
type: string
required: true
total:
type: number
required: true
participants:
publishOrder:
type: emit
event: "order.created"
payload:
orderId: workflow.inputs.orderId
total: workflow.inputs.total
fulfill:
type: exec
run: ./fulfill.sh
input:
orderId: workflow.inputs.orderId
paymentId: event.paymentId
notifyTimeout:
type: http
url: https://hooks.example.com/timeout
method: POST
onError: skip
flow:
- publishOrder
- wait:
event: "payment.confirmed"
match: event.orderId == workflow.inputs.orderId
timeout: 30m
onTimeout: notifyTimeout
- fulfill
output:
orderId: workflow.inputs.orderId
paymentId: event.paymentId
fulfillStatus: fulfill.status