Waiting for Events
Many business processes need to pause and wait for something external—a payment confirmation, an approval decision, a callback from a third-party system.
The awaitEvent method lets your workflow suspend until a matching event arrives.
|
The events that |
The awaitEvent method
After initiating payment, we need to wait for a PaymentConfirmed event before continuing:
var confirmation = ctx.awaitEvent("awaitPayment",
PaymentConfirmed.class, (1)
Duration.ofMinutes(15)); (2)
| 1 | The event type to wait for. The engine subscribes to the event bus and watches for events of this type. |
| 2 | Maximum time to wait.
After 15 minutes, the step completes with StepStatus.TIMED_OUT. |
When a matching event arrives, the workflow resumes and awaitEvent returns the deserialized event object.
Here’s what the event store looks like when a PaymentConfirmed event arrives before the timeout:
The STARTED event records when the wait began (startTime is used to recalculate remaining timeout on replay).
When the external PaymentConfirmed event arrives, it unblocks the workflow and the matched payload is recorded in the COMPLETED event.
If no matching event arrives within 15 minutes:
Filtering with associations
There’s a problem with the code above.
Without filtering, any PaymentConfirmed event would wake up every waiting workflow.
That’s clearly not what we want.
Associations let you correlate events to specific workflow instances:
var confirmation = ctx.awaitEvent("awaitPayment",
PaymentConfirmed.class,
associate( (1)
payloadProperty("orderId"), (2)
equalsTo(ctx.workflowPayload().get("orderId")) (3)
),
Duration.ofMinutes(15));
| 1 | associate(…) combines a value extractor and a matcher into an event predicate. |
| 2 | payloadProperty("orderId") extracts the orderId field from the incoming event’s payload. |
| 3 | equalsTo(…) creates an equality matcher.
Only PaymentConfirmed events whose orderId equals this workflow’s orderId will match. |
|
Without associations, event matching is based solely on the event type. For production workflows, you will almost always want to use associations to ensure events reach the correct workflow instance. |
Timeouts
When a waitFor step times out, it completes with StepStatus.TIMED_OUT.
You can use this to implement fallback logic.
Use waitForEvent (non-blocking) and check the result:
var stepResult = ctx.waitForEvent("awaitPayment",
PaymentConfirmed.class,
associate(payloadProperty("orderId"),
equalsTo(ctx.workflowPayload().get("orderId"))),
Duration.ofMinutes(15));
if(stepResult.success()){
// handle success
} else if (stepResult.timeout()) {
// handle timeout
}
Or use awaitEvent (blocking) with try-catch:
try {
var confirmation = ctx.awaitEvent("awaitPayment",
PaymentConfirmed.class,
associate(payloadProperty("orderId"),
equalsTo(ctx.workflowPayload().get("orderId"))),
Duration.ofMinutes(15));
} catch (StepFailedException e) {
// handle error, timeout or cancellation
}
|
You can branch on the timeout outcome—cancel the order, retry payment, or escalate to manual review. The right strategy depends on your business requirements. |
Waiting for multiple events
awaitEvent blocks until a single event arrives.
If you need to wait for multiple events concurrently, use the non-blocking waitFor and combine results with a combinator:
var payment = ctx.waitForEvent("awaitPayment", (1)
PaymentConfirmed.class, Duration.ofMinutes(15));
var approval = ctx.waitForEvent("awaitApproval", (1)
ManagerApproved.class, Duration.ofMinutes(30));
ctx.allMatch(WorkflowStepResult::isCompleted, payment, approval) (2)
.await();
logger.info("Both payment and approval received — proceeding!");
| 1 | waitForEvent returns a WorkflowStepResult immediately without blocking—both waits start concurrently. |
| 2 | allMatch blocks until both events have arrived. See Step Orchestration for more on combinators. |
Sleep—a simple delay
Sometimes you just need to pause for a while.
The sleep method provides a durable delay:
ctx.sleep("cooldownPeriod", Duration.ofDays(10)); (1)
| 1 | Pauses the workflow for 10 days.
Under the hood, this is a waitFor with an event condition that never matches—so it always times out after the specified duration. |
sleep always produces a STARTED followed by a TIMED_OUT—that’s by design, since the "event" it waits for never arrives.
sleepAsync (non-blocking)
sleepAsync returns a WorkflowStepResult immediately, allowing you to compose delays with other steps using combinators:
var cooldown = ctx.sleepAsync("cooldown", Duration.ofMinutes(5));
var approval = ctx.waitForEvent("approval",
ManagerApproved.class, Duration.ofHours(1));
// Proceed when either the cooldown finishes OR approval arrives
ctx.anyMatch(WorkflowStepResult::isCompleted, cooldown, approval)
.await();
Updated workflow
Here’s our workflow with the payment step added. Notice the order—we subscribe to the event before initiating payment, to avoid a race condition where the confirmation arrives before we’re listening:
@Workflow(idProperty = "orderId", startOnEvent = "OrderPlaced")
public void execute(SimpleWorkflowContext ctx) {
var reserved = ctx.awaitExecute("reserveStock", Boolean.class,
InventoryService::reserveStock);
if (!reserved) {
ctx.fail(new RuntimeException("Stock unavailable"));
return;
}
// First: subscribe to the payment confirmation event (non-blocking)
var paymentConfirmation = ctx.waitForEvent("awaitPayment", (1)
PaymentConfirmed.class,
Duration.ofMinutes(15));
// Then: initiate payment (non-blocking) — this triggers the external service
var paymentInitiation = ctx.execute("initiatePayment", (2)
ctx.workflowPayload(),
PaymentService::initiatePayment,
Duration.ofSeconds(30), defaults());
// Wait for both to complete
ctx.allMatch(WorkflowStepResult::isCompleted, (3)
paymentConfirmation, paymentInitiation).await();
logger.info("Payment initiated and confirmed for order {}",
ctx.workflowPayload().get("orderId"));
}
| 1 | waitForEvent starts listening for PaymentConfirmed immediately and returns a WorkflowStepResult without blocking. |
| 2 | execute calls the payment service—this may trigger a third-party provider that eventually publishes a PaymentConfirmed event. |
| 3 | allMatch blocks until both the payment initiation completes and the confirmation event arrives. |
|
By subscribing to the event before calling the service that produces it, you guarantee no events are missed—even if the external service responds instantly. |
Here’s the full event store for this workflow:
The event subscription (AwaitPaymentStarted) appears before InitiatePaymentStarted—no race condition.
The external PaymentConfirmed event unblocks the workflow, and its payload is recorded in AwaitPaymentCompleted.
Now that we can execute actions and wait for external events, let’s dive deeper into all the options you can configure—timeouts, error handling, event naming, payload reducers, and execution semantics.