Configuration

Throughout this tutorial, workflows have been defined using the @Workflow annotation. This section covers the declarative (programmatic) configuration API, which gives you full control over how workflows are wired up—and how to customize the context, event naming, ID resolution, and more.

Annotation v.s. declarative configuration

The @Workflow annotation is the simplest way to define a workflow:

@Workflow(idProperty = "orderId",
          startOnEventClass = OrderPlacedEvent.class,
          workflowNamespace = "io.myapp.orders")
public void execute(SimpleWorkflowContext workflowContext) { /* ... */ }

The start trigger can be declared in two ways:

  • startOnEventClass = OrderPlacedEvent.class for a type-safe class reference

  • startOnEventName = "io.myapp.orders.OrderPlaced" for an explicit qualified event name

Use exactly one of these attributes on a workflow method. startOnConditions can be combined with either form to further filter which events start a workflow. Use canonical qualified association strings such as payload:status=vip or metadata:tenantId=acme. The canonical association DSL is described in Association DSL.

The declarative API offers the same capabilities plus additional options not available via annotations. The configuration is the same whether you use Java or Kotlin—the API is identical in both languages. Here’s a complete example showing how to set up the Axon configuration with a workflow module:

var configurer = WorkflowConfigurer.create();

configurer.registerWorkflowModule(
        WorkflowModule
                .defaults("order-workflows", SimpleWorkflowContext.class)
                .contextFactory(c -> new SimpleWorkflowContextFactory())
                .definition(d -> d
                        .declarative(c -> new OrderFulfillmentWorkflow()::execute)
                        .workflowName("OrderFulfillment")
                        .on(EventConditions.fromType(OrderPlacedEvent.class))
                        .customized((c, w) -> w
                                .eventNameCustomizer(namespace("io.myapp.orders"))
                                .workflowIdProvider(new PayloadPropertyWorkflowIdProvider("orderId"))
                        )
                )
);

var configuration = configurer.start(); (1)
1 start() initializes the Axon configuration and makes the workflow engine ready to receive events.

The WorkflowModule builder

The builder follows a step-by-step flow:

WorkflowModule
    .defaults("module-name", ContextType.class) // 1. Module name and context type
    .contextFactory(...)                    // 2. How to create context instances
    .definition(d -> d                          // 3. Define a workflow
        .declarative(...) or .autodetected(...)
    );

Step 1: Module name and context type

WorkflowModule.defaults("order-workflows", SimpleWorkflowContext.class)

Specifies the module name and the author-facing workflow context type. Use SimpleWorkflowContext.class for the built-in context, or your own custom context class. Both implement WorkflowContext, the contract supplied to workflow bodies. The context type must extend AbstractWorkflowContext. WorkflowModule.configure(name, contextType) is the variant that also lets you supply a WorkflowConfigurationRegistry, a WorkflowExecutionRepository, history settings, and the configuration of the event processors, see Running on several nodes.

Step 2: Context factory

.contextFactory(c -> new SimpleWorkflowContextFactory())

Provides a factory that creates new context instances for each workflow execution.

Import AbstractWorkflowContext from io.axoniq.framework.workflow.runtime.execution. The author-facing WorkflowContext, step definitions, EventCondition, EventConditions, and workflow lifecycle exceptions reside in io.axoniq.framework.workflow.dsl.api; retry configuration resides in io.axoniq.framework.workflow.dsl.api.retry.

Step 3: Workflow definitions

You can define workflows in two ways:

Declarative—full control:

.definition(d -> d
        .declarative(c -> workflow::execute)
        .workflowName("OrderFulfillment")
        .on(EventConditions.fromType(OrderPlacedEvent.class))
        .customized((c, w) -> w /* ... */)
)

Autodetected—convention-based, reads @Workflow annotations:

.definition(d -> d
        .autodetected(c -> new OrderFulfillmentWorkflow())
)

You can add multiple definitions to the same module by calling definition(…​) repeatedly:

WorkflowModule
    .defaults("my-workflows", SimpleWorkflowContext.class)
    .contextFactory(c -> new SimpleWorkflowContextFactory())
    .definition(d -> d
            .declarative(c -> workflowA::execute)
            .workflowName("WorkflowA")
            .on(EventConditions.fromType(EventA.class))
            .notCustomized())
    .definition(d -> d
            .declarative(c -> workflowB::execute)
            .workflowName("WorkflowB")
            .on(EventConditions.fromType(EventB.class))
            .notCustomized());

Start conditions

The .on(…​) method defines which events trigger a new workflow instance.

Method Description

EventConditions.fromType(MyEvent.class)

Start on any event of this type.

EventConditions.fromType(MyEvent.class, associations)

Start on events of this type that match the provided associations.

Example with association filtering—only start for VIP customers:

.on(EventConditions.fromType(
        RegistrationReceivedEvent.class,
        associate(payloadProperty("status"), equalsTo("vip"))
))

Workflow ID provider

The workflow ID provider determines how each workflow instance gets its unique ID from the triggering event.

Provider Description

PayloadPropertyWorkflowIdProvider

Extracts the ID from a payload field. Supports transformation.

MessageWorkflowIdProvider

Uses the event message’s own identifier. This is the default.

// Extract orderId from payload, prefix it
.workflowIdProvider(fromPayloadAttribute(c, "orderId", id -> "order-" + id))

// Extract directly without transformation
.workflowIdProvider(new PayloadPropertyWorkflowIdProvider("orderId"))

Customization options

The .customized(…​) lambda receives the Axon Configuration and a WorkflowCustomization object:

.customized((c, w) -> w
        .eventNameCustomizer(namespace("io.myapp.orders")
                .stepCompleted("Done")
                .workflowBaseName("OrderFulfillment"))
        .workflowIdProvider(new PayloadPropertyWorkflowIdProvider("orderId"))
        .registerWorkflowStatusChangeListener(WorkflowStatus.COMPLETED,
                (status, context, processingContext) -> {
                    logger.info("Workflow {} completed", context.workflowId());
                })
)
Option Description

eventNameCustomizer(…​)

Global event name customizer for all steps in this workflow. See Event Name Customization.

workflowIdProvider(…​)

How to extract the workflow instance ID from triggering events.

registerWorkflowStatusChangeListener(status, listener)

React to workflow state changes. See Lifecycle Listeners.

unregisterWorkflowStatusChangeListener(status, listener)

Remove a previously registered listener.

recoverableExceptionPolicy(…​)

Decide which exceptions escaping the body pause the workflow instead of failing it. See Configuring the policy.

workflowVersion(String)

The version of this workflow definition. Defaults to the default version. See Versioning.

If you don’t need any customizations, use .notCustomized().

Running on several nodes

Each WorkflowModule runs its engine on its own pooled streaming event processor, named after the module. The history projector runs on a second processor named WorkflowHistoryProjector[<module name>]. Each workflow instance belongs to one segment of the engine processor, chosen by the hash of its workflow id. Subscribing processors, including Axon Server persistent streams, cannot back workflows.

Token store

The processor claims its segments in the unnamed TokenStore component of your application. Register a durable store, for example JpaTokenStore, as an unnamed component. Without one, the engine falls back to an in-memory token store and logs a warning. Claims then stay inside the process, so run a single node only.

Processor settings

WorkflowModule.configure(…​) accepts processorConfiguration(…​) for the engine processor and historyProcessorConfiguration(…​) for the history processor. Both take a function that receives and returns a PooledStreamingEventProcessorConfiguration. Only the initial segment count is applied to a new token store. To change it later, split or merge the stored tokens.

WorkflowModule
    .configure("order-workflows", SimpleWorkflowContext.class)
    .processorConfiguration(processor -> processor.initialSegmentCount(8))
    .contextFactory(c -> new SimpleWorkflowContextFactory())
    .definition(d -> d /* ... */);

Failover

When a node stops, another node claims its segments and resumes the unfinished workflow instances. Every event an instance appends carries an append condition on its workflow id. A node that lost its claim without noticing has its append rejected, logs a warning, and stops that instance.

Spring Boot

Workflow support is part of the Axoniq Framework Spring Boot starter, io.axoniq.framework:axoniq-spring-boot-starter. It activates when the workflow engine is on the classpath, for example through axoniq-workflow-dsl. Set axon.workflow.enabled=false to turn it off. The auto-configuration registers every Spring bean that has @Workflow annotated methods. A RecoverableWorkflowExceptionPolicy bean sets the exception policy, as described in Configuring the policy. All @Workflow beans with the same context type share one WorkflowModule, named after the simple name of that context type.

The engine processor is configured with axon.workflow. properties. The history processor uses the same properties under axon.workflow.history..

Property Default Description

initial-segment-count

16

Number of segments created on first start.

batch-size

1

Maximum number of events read from the event store per poll.

thread-count

4

Maximum number of segments claimed by one node.

token-claim-interval

5000

Milliseconds between attempts to claim segments.

claim-extension-threshold

5000

Milliseconds after which a claim is extended.

coordinator-claim-extension

false

Whether the coordinator extends claims.

See Running on several nodes for the token store and failover.

Metadata and correlation data

Every workflow event carries metadata—key-value pairs attached to the event message alongside the payload. The engine manages a set of internal metadata keys automatically, and Axon Framework’s CorrelationDataProvider mechanism lets you propagate additional metadata from triggering events through the entire workflow.

Engine-managed metadata

The engine attaches the following metadata to every workflow and step event:

Key Description

workflowId

The unique identifier of the workflow instance.

stepName

The name of the step (step events only).

stepType

The step’s lifecycle phase: STARTED, RETRYING, RETRY_STARTED, COMPLETED, FAILED, TIMED_OUT, or CANCELLED (step events only).

stepPrimitive

Internal step classification used by the engine: WAIT_FOR_EVENT on waitForEvent step events, PUBLISH on events published with publish / awaitPublish.

workflowStatus

The workflow status: STARTED, COMPLETED, FAILED, TIMED_OUT, or CANCELLED (workflow events only).

workflowDefinitionName

The qualified name of the workflow definition (workflow events).

workflowDefinitionVersion

The version of the workflow definition (workflow events).

versionChangeId, version

The recorded version change identifier and the workflow version, on version marker events.

modifyPayload

The payload reducer name, if one was configured on a completed step (for example, COMBINE_GLOBAL_AND_LOCAL).

These keys are managed by the engine and cannot be overridden.

Engine-managed event-store tags

Engine-published workflow events are also written with event-store tags.

Tag Description

workflowId=<id>

Added to every engine-published event that carries workflowId metadata, including business events published with publish / awaitPublish (their own tags are kept).

workflowEvent=lifecycle

Added to workflow lifecycle events.

workflowEvent=waitForStep

Added to waitForEvent step STARTED and terminal events.

These tags are intended for event-sourced read models such as "currently running workflow executions" and "currently waiting steps".

Correlation data propagation

When a workflow is triggered by an incoming event, Axon Framework’s CorrelationDataProvider mechanism can automatically propagate metadata from that event to all events published by the workflow—including step events.

This works because the workflow engine propagates the ProcessingContext from the triggering event through the entire workflow execution. Resources attached to the ProcessingContext—including correlation data—are copied to child contexts at each step boundary via ProcessingContextUtils.copyResources().

Axon Framework automatically registers a MessageOriginProvider by default, so correlationId and causationId are propagated out of the box. The example below shows how to register it explicitly or add custom providers.
configurer.eventSourcing(es -> es.messaging(m -> m
    .registerCorrelationDataProvider(config ->
        new MessageOriginProvider()) (1)
));
1 MessageOriginProvider is a built-in provider that copies the correlationId and causationId from the incoming message metadata to all outgoing messages published within the same processing context.

You can also create a custom provider to propagate additional metadata fields:

configurer.eventSourcing(es -> es.messaging(m -> m
    .registerCorrelationDataProvider(config ->
        message -> {
            var metadata = message.metadata();
            var result = new HashMap<String, Object>();
            if (metadata.containsKey("tenantId")) {
                result.put("tenantId", metadata.get("tenantId"));
            }
            if (metadata.containsKey("userId")) {
                result.put("userId", metadata.get("userId"));
            }
            return result;
        })
));

With this configuration, if the triggering event (for example, OrderPlaced) carries tenantId and userId in its metadata, every step event (ReserveStockStarted, ReserveStockCompleted, etc.) will also carry those values—automatically, with no changes to your workflow code.

How it works

The propagation path:

  1. An incoming event with metadata arrives and triggers a new workflow.

  2. The CorrelationDataProvider extracts correlation data and attaches it to the ProcessingContext.

  3. The engine passes this ProcessingContext to the workflow execution.

  4. When a step publishes events, the engine creates a child UnitOfWork and copies all resources from the parent context.

  5. The correlation data flows through to the published event’s metadata.

This means correlation data propagation is transparent—workflow code does not need to know about it.

Correlation data propagation carries metadata that originates from the triggering event. If you need to attach metadata that doesn’t come from the triggering event (for example, computed values, workflow-specific tags), a dedicated MetadataCustomizer API is planned—see #103.

Custom workflow context

You don’t have to use SimpleWorkflowContext. WorkflowContext is the shared author-facing contract, and BaseWorkflowContext is the canonical Java DSL layer implementing it with Java-friendly named overloads. SimpleWorkflowContext adds helpers such as typed awaitEvent, sleep(Duration), setPayload, and cancel(String). Extend whichever layer best fits the API you want to expose to workflow authors.

For example, an approval workflow context:

public class ApprovalWorkflowContext extends SimpleWorkflowContext {

    public ApprovalWorkflowContext(String workflowId, Map<String, Object> payload,
                                   ProcessingContext processingContext,
                                   WorkflowConfiguration<?> workflowConfiguration) {
        super(workflowId, payload, processingContext, workflowConfiguration);
    }

    /** Request approval and wait for a response event. Returns true if approved. */
    public boolean requestApproval(String approver, Duration deadline) {
        awaitExecute("requestApproval",
                payload("approver", approver, "requestId", workflowId()).getValues(),
                ApprovalService::sendRequest);

        var decision = awaitEvent(
                "awaitDecision",
                ApprovalDecisionEvent.class,
                associate(payloadProperty("requestId"), equalsTo(workflowId())),
                step -> step.timeout(deadline)
        );
        return "approved".equals(decision.outcome());
    }

    /** Escalate to a manager when the original approver doesn't respond in time. */
    public void escalate(String manager) {
        awaitExecute("escalate",
                payload("manager", manager, "requestId", workflowId()).getValues(),
                ApprovalService::escalate);
    }

    /** Notify the requester of the final outcome. */
    public void notifyOutcome(String recipient, String decision) {
        awaitExecute("notifyOutcome",
                payload("recipient", recipient, "decision", decision).getValues(),
                NotificationService::sendDecision);
    }

    /** Domain accessors for the workflow payload. */
    public String requester() { return (String) workflowPayload().get("requester"); }
    public String department() { return (String) workflowPayload().get("department"); }
    public double amount() { return (double) workflowPayload().get("amount"); }
}

Register it with a factory:

public class ApprovalWorkflowContextFactory
        implements WorkflowContextFactory<ApprovalWorkflowContext> {

    @Override
    public ApprovalWorkflowContext createContext(Map<String, Object> payload, String workflowId,
                                                ProcessingContext processingContext,
                                                WorkflowConfiguration<?> workflowConfiguration) {
        return new ApprovalWorkflowContext(workflowId, payload,
                                           processingContext, workflowConfiguration);
    }
}

Wire it all together:

WorkflowModule
    .defaults("purchase-approval", ApprovalWorkflowContext.class)
    .contextFactory(c -> new ApprovalWorkflowContextFactory())
    .definition(d -> d
            .declarative(c -> new PurchaseApprovalWorkflow()::execute)
            .workflowName("PurchaseApproval")
            .on(EventConditions.fromType(PurchaseRequestSubmitted.class))
            .notCustomized()
    );

Your workflow then reads like plain business logic—no engine primitives in sight:

public void execute(ApprovalWorkflowContext workflowContext) {
    if (!workflowContext.requestApproval("team-lead", Duration.ofDays(3))) {
        workflowContext.escalate("department-head");
    }
    workflowContext.notifyOutcome(workflowContext.requester(), "approved");
}

The engine still records every step as durable events, handles crash recovery, and provides audit trails—but the workflow code speaks the language of your domain.