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:

  • BaseWorkflowContext is the canonical primitive API. Its blocking awaitExecute(…​) methods return a payload map, and customization happens through UnaryOperator<ExecuteStepDefinition>.

  • SimpleWorkflowContext adds convenience helpers on top, such as execute overloads without extra input payload, typed awaitExecute(…​) for single-value actions, typed awaitEvent(…​), sleep(…​), and setPayload(…​).

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 a WorkflowStepResult

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:

1 started ReserveStockStarted {"customerId": 456, "amount": 99.95} 2 completed ReserveStockCompleted {"reserved": true}

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, reserveStock becomes ReserveStock). For full details on naming rules, see Event name customization.

Java entry points

API Description

awaitExecute(stepName, returnType, supplier)

SimpleWorkflowContext convenience for single-value actions. Internally stores the result under the payload key "__" + stepName + "Result" and returns it as the requested type.

awaitExecute(stepName, payload, processor)

BaseWorkflowContext blocking form. Passes a local payload map to a PayloadProcessor and returns the resulting payload map.

awaitExecute(stepName, payload, processor, customizer)

Same as above, with fluent customization through ExecuteStepDefinition methods such as timeout(…​), eventNameCustomizer(…​), parameterPayloadReducer(…​), resultPayloadReducer(…​), and retryPolicy(…​).

execute(stepName, payload, processor) / execute(stepName, payload, processor, customizer)

Non-blocking counterparts that return a WorkflowStepResult.

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

execute(stepName, payload, action)

Starts the step with default timeout and default event names.

execute(stepName, payload, action, customizer)

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 execute(…​) is still running, the engine cancels that step and writes its CANCELLED event before the workflow transitions to COMPLETED. If you need the workflow to wait for the step’s real result, call .await() on the returned WorkflowStepResult (or use awaitExecute / allMatch / anyMatch). See Normal completion with running steps.

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.