Execute Steps
Execute steps are the type of execution primitives that allow calling services, performing computations and triggering side effects in workflows. The engine records each execution as durable events, giving you crash recovery and audit trails automatically.
|
The Java DSL now has two layers:
|
There are two variants:
-
awaitExecute—blocks until the action finishes and returns either the resulting payload map or, via the convenience overload, a single typed value -
execute—starts the action without waiting and returns aWorkflowStepResult
awaitExecute (blocking)
awaitExecute calls an action, waits for it to finish, and returns the resulting payload map.
The engine records a STARTED and COMPLETED event for this step automatically.
Basic usage
For a single-value action, SimpleWorkflowContext offers a typed convenience overload:
var reserved = ctx.awaitExecute("reserveStock", Boolean.class,
InventoryService::reserveStock);
Internally this convenience stores the value under the synthetic payload key "" + stepName + "Result", then unwraps it for the caller.
For reserveStock, that key becomes reserveStockResult.
The canonical form takes a step name, a local payload map, and a processor:
var reservation = ctx.awaitExecute("reserveStock",
Map.of(),
(pc, input) -> Map.of("reserved", InventoryService.reserveStock()));
That’s it—one line of business logic, and the engine gives you event sourcing, crash recovery, and an audit trail.
With payload
Pass data into the step and use the result for control flow:
var customerId = ctx.workflowPayload().get("customerId");
var amount = ctx.workflowPayload().get("amount");
var reservation = ctx.awaitExecute("reserveStock",
payload("customerId", customerId, "amount", amount).getValues(),
(pc, input) -> Map.of("reserved", InventoryService.reserveStock(input))); (1)
if (!Boolean.TRUE.equals(reservation.get("reserved"))) {
ctx.fail(new RuntimeException("Stock unavailable")); (2)
return;
}
| 1 | The params (payload) passed to the step (customerId, amount) are recorded in the STARTED event, and the returned payload map is recorded in the COMPLETED event. |
| 2 | If stock reservation fails, we fail the workflow.
Standard Java control flow—if, loops, try/catch—all work exactly as you’d expect. |
Here’s the event store after a successful run:
Each awaitExecute produces a STARTED/COMPLETED pair—the STARTED event captures the input payload, the COMPLETED event captures the result.
With timeout and event name customizer
The full form adds an explicit timeout and event name customizer:
ctx.awaitExecute("initiatePayment",
payload("customerId", customerId, "amount", amount).getValues(),
PaymentService::initiatePayment,
step -> step.timeout(Duration.ofSeconds(30))
.eventNameCustomizer(baseName("InitiatingPaymentForCustomer"))); (1)
| 1 | baseName(…) replaces the step name in the event name—its events will appear as InitiatingPaymentForCustomerStarted instead of InitiatePaymentStarted. |
|
The step name’s first letter is automatically uppercased in event names (for example, |
Java entry points
| API | Description |
|---|---|
|
|
|
|
|
Same as above, with fluent customization through |
|
Non-blocking counterparts that return a |
execute (non-blocking)
execute starts a step without waiting and returns a WorkflowStepResult you can compose later.
Use this when you want to run multiple steps concurrently, or when you need to combine an execute with a waitForEvent.
| Overload | Description |
|---|---|
|
Starts the step with default timeout and default event names. |
|
Full form with explicit timeout, event name customization, reducer overrides, and retry policy. |
var shipping = ctx.execute("shipOrder", ctx.workflowPayload(),
ShippingService::shipOrder,
step -> step.timeout(Duration.ofMinutes(5)));
See Step Orchestration for combining multiple execute and waitForEvent results with allMatch, anyMatch, and noneMatch.
|
If your workflow body returns while an async step started with |
Kotlin DSL
WorkflowKontext is the runtime-facing context, while Kontext is the Kotlin authoring wrapper.
Kontext supports concise Kotlin-first overloads with defaults for the common case:
fun Kontext.fulfillOrder() {
val reserved = awaitExecute<Boolean>(
stepName = "reserveStock",
timeout = 10.seconds
) {
InventoryService.reserveStock()
}
}
If you want to pass step-local data with a named Kotlin argument, use inputPayload = ….
The typed Kotlin convenience uses the same synthetic payload key pattern "__" + stepName + "Result" internally. The low-level definition API is still available when you want to construct ExecuteStepDefinition explicitly, but most workflows can stay on the overloads with Kotlin default parameters.
What’s next
For cross-cutting configuration that applies to all step types—timeouts, payload reducers, error handling, event naming, and execution semantics—see Understanding Steps.
For waiting on external events, see Waiting for Events.