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.classfor 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 |
|---|---|
|
Start on any event of this type. |
|
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 |
|---|---|
|
Extracts the ID from a payload field. Supports transformation. |
|
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 |
|---|---|
|
Global event name customizer for all steps in this workflow. See Event Name Customization. |
|
How to extract the workflow instance ID from triggering events. |
|
React to workflow state changes. See Lifecycle Listeners. |
|
Remove a previously registered listener. |
|
Decide which exceptions escaping the body pause the workflow instead of failing it. See Configuring the policy. |
|
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 /* ... */);
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 |
|---|---|---|
|
|
Number of segments created on first start. |
|
|
Maximum number of events read from the event store per poll. |
|
|
Maximum number of segments claimed by one node. |
|
|
Milliseconds between attempts to claim segments. |
|
|
Milliseconds after which a claim is extended. |
|
|
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 |
|---|---|
|
The unique identifier of the workflow instance. |
|
The name of the step (step events only). |
|
The step’s lifecycle phase: |
|
Internal step classification used by the engine: |
|
The workflow status: |
|
The qualified name of the workflow definition (workflow events). |
|
The version of the workflow definition (workflow events). |
|
The recorded version change identifier and the workflow version, on version marker events. |
|
The payload reducer name, if one was configured on a completed step (for example, |
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 |
|---|---|
|
Added to every engine-published event that carries |
|
Added to workflow lifecycle events. |
|
Added to |
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:
-
An incoming event with metadata arrives and triggers a new workflow.
-
The
CorrelationDataProviderextracts correlation data and attaches it to theProcessingContext. -
The engine passes this
ProcessingContextto the workflow execution. -
When a step publishes events, the engine creates a child
UnitOfWorkand copies all resources from the parent context. -
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 |
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.