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",
startOnEvent = "OrderPlaced",
workflowNamespace = "io.myapp.orders")
public void execute(SimpleWorkflowContext ctx) { /* ... */ }
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 = MessagingConfigurer.create();
configurer.componentRegistry(r -> r.registerEnhancer(registry ->
registry.registerModule(
WorkflowModule
.usingContext(SimpleWorkflowContext.class)
.workflowContextFactory(c -> new SimpleWorkflowContextFactory())
.workflowExecutionFactory(c ->
new DSLAdoptingExecutionFactory<>(SimpleWorkflowContext.class))
.definitions(d -> d
.declarative(c -> new OrderFulfillmentWorkflow()::execute)
.workflowName("OrderFulfillment")
.on(EventConditions.fromType(OrderPlacedEvent.class))
.customized((c, w) -> w
.eventNameCustomizer(namespace("io.myapp.orders"))
.workflowIdProvider(fromPayloadAttribute(c, "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
.usingContext(ContextType.class) // 1. Which context type
.workflowContextFactory(...) // 2. How to create context instances
.workflowExecutionFactory(...) // 3. How to create executions
.definitions(d -> d // 4. Define one or more workflows
.declarative(...) or .autodetected(...)
);
Step 1: Context type
WorkflowModule.usingContext(SimpleWorkflowContext.class)
Specifies the workflow context type. Use SimpleWorkflowContext.class for the built-in context, or your own custom context class.
Step 2: Context factory
.workflowContextFactory(c -> new SimpleWorkflowContextFactory())
Provides a factory that creates new context instances for each workflow execution.
Step 3: Execution factory
.workflowExecutionFactory(c -> new DSLAdoptingExecutionFactory<>(SimpleWorkflowContext.class))
DSLAdoptingExecutionFactory is the standard execution factory for DSL-based contexts (anything extending AbstractDSLWorkflowContext). It bridges the context to the engine’s execution model.
Step 4: Workflow definitions
You can define workflows in two ways:
Declarative—full control:
.definitions(d -> d
.declarative(c -> workflow::execute)
.workflowName("OrderFulfillment")
.on(EventConditions.fromType(OrderPlacedEvent.class))
.customized((c, w) -> w /* ... */)
)
Autodetected—convention-based, reads @Workflow annotations:
.definitions(d -> d
.autodetected(c -> new OrderFulfillmentWorkflow(),
SimpleWorkflowContext.class)
)
You can chain multiple definitions:
.definitions(d -> d
.declarative(c -> workflowA::execute)
.workflowName("WorkflowA")
.on(EventConditions.fromType(EventA.class))
.notCustomized()
.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 a predicate (for example, association filtering). |
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(fromPayloadAttribute(c, "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(fromPayloadAttribute(c, "orderId"))
.registerWorkflowStatusChangeListener(WorkflowStatus.COMPLETED,
(status, context) -> {
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. |
If you don’t need any customizations, use .notCustomized().
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: |
|
The workflow status: |
|
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.
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.eventProcessing(ep -> ep
.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.eventProcessing(ep -> ep
.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. You can extend it to build a domain-specific DSL that hides the engine primitives behind business language.
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, 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
.usingContext(ApprovalWorkflowContext.class)
.workflowContextFactory(c -> new ApprovalWorkflowContextFactory())
.workflowExecutionFactory(c -> new DSLAdoptingExecutionFactory<>(ApprovalWorkflowContext.class))
.definitions(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 ctx) {
if (!ctx.requestApproval("team-lead", Duration.ofDays(3))) {
ctx.escalate("department-head");
}
ctx.notifyOutcome(ctx.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.