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.
There are two variants:
-
awaitExecute—blocks until the action finishes and returns a typed result -
execute—starts the action without waiting and returns aWorkflowStepResult
awaitExecute (blocking)
awaitExecute calls an action, waits for it to finish, and returns the typed result.
The engine records a STARTED and COMPLETED event for this step automatically.
Basic usage
The simplest form takes a step name, return type, and a supplier:
var reserved = ctx.awaitExecute("reserveStock", Boolean.class,
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 reserved = ctx.awaitExecute("reserveStock",
payload("customerId", customerId, "amount", amount).getValues(),
Boolean.class,
InventoryService::reserveStock); (1)
if (!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. |
| 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,
Duration.ofSeconds(30),
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, |
All overloads
| Overload | Description |
|---|---|
|
Simplest form. No input payload, no timeout override, no customizer. Calls a |
|
Like above, but the action receives the (empty) payload map as input. |
|
Passes a payload map to the action. The payload is recorded in the STARTED event. |
|
Adds an event name customizer to control how events are named. |
|
The action receives payload but returns nothing. |
|
Full form. The |
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 and event name customizer. |
var shipping = ctx.execute("shipOrder", ctx.workflowPayload(),
ShippingService::shipOrder,
Duration.ofMinutes(5), defaults());
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
The Kotlin DSL uses extension functions on Kontext, giving you a clean, idiomatic API with named parameters and kotlin.time.Duration support:
fun Kontext.fulfillOrder() {
// Blocking awaitExecute
val reserved = awaitExecute(
"reserveStock",
{ InventoryService.reserveStock() },
local = mapOf("customerId" to customerId, "amount" to amount)
)
if (!reserved) {
fail(RuntimeException("Stock unavailable"))
return
}
// Blocking awaitExecute with all options
val result = awaitExecute(
"reserveStock",
action = { InventoryService.reserveStock() },
local = mapOf("customerId" to customerId),
timeout = 10.seconds,
eventNameCustomizer = baseName("ReservingStock")
)
// Non-blocking execute with all options
execute(
"shipOrder",
action = { pc, p -> ShippingService.shipOrder(pc, p) },
local = mapOf("address" to address),
parameterMapping = PayloadReducer.LOCAL_ONLY,
resultMapping = PayloadReducer.GLOBAL_ONLY,
timeout = 5.minutes,
eventNameCustomizer = namespace("io.shipping")
)
}
Kotlin’s named parameters and default values mean you only specify what you need—everything else falls back to sensible defaults.
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.