Engine Usage
The WorkflowEngine is a stateless, synchronous Java class. All methods take state in and return updated state out — the input instance is never mutated.
Creating the Engine
WorkflowEngine engine = new WorkflowEngine(
NodeExecutorProvider.fromList(executor1, executor2), // NodeExecutorProvider
List.of(listener1, listener2), // WorkflowEventListener implementations
myErrorHandler // WorkflowErrorHandler (optional, defaults to fail-on-error)
);
The NodeExecutorProvider is a functional interface — implement it directly for custom executor lookup (e.g. a service registry), or use the fromList convenience factory. All dependencies are passed via constructor — no CDI, no service discovery.
Starting a Workflow
// With auto-generated UUID
WorkflowInstance instance = engine.startWorkflow(definition, initialContext);
// With caller-provided ID
WorkflowInstance instance = engine.startWorkflow(definition, initialContext, "my-id-123");
startWorkflow does the following:
- Validates the workflow definition (rejects if any ERROR-level validation problems exist)
- Validates the initial context against the start node's input schema
- Creates a new instance in
RUNNINGstatus - Fires
onWorkflowStarted - Enters the start node and evaluates outgoing edges
- Chains through action nodes until a wait state or end is reached
The returned instance is in WAITING (hit a human-task, receive-event, or wait node), COMPLETED (reached an end node), or FAILED (error during execution).
Completing a Waiting Node
NodeResult result = new NodeResult(NodeResultStatus.COMPLETED,
Map.of("approved", true, "comment", "Looks good"));
WorkflowInstance updated = engine.completeCurrentNode(definition, instance, result);
- Throws
IllegalStateExceptionif the instance is not inWAITINGstatus - Merges the result's output into the workflow context
- Evaluates outgoing edges and chains through action nodes until the next wait or end
Cancelling a Workflow
- Sets status to
CANCELLEDand firesonWorkflowCancelled - No-op if the instance is already in a terminal state (
COMPLETED,FAILED,CANCELLED)
Checking Event Correlation
See Event Correlation for details.
Handling Wait States
The engine is stateless and synchronous — when it reaches a node that requires external input (Human Task, Receive Event, or Wait), it sets the instance to WAITING and returns. The consuming application is responsible for detecting the wait, handling it, and resuming the workflow. All three wait-state node types follow the same pattern:
- Detect — after
startWorkfloworcompleteCurrentNodereturns, checkinstance.status() == WAITING - Introspect — call the appropriate
get*Infomethod to learn what the instance is waiting for - Handle — perform the external work (present a task inbox, listen for events, schedule a timer)
- Resume — call
completeCurrentNode(workflow, instance, result)with the outcome
WorkflowInstance instance = engine.startWorkflow(workflow, inputs);
if (instance.status() == InstanceStatus.WAITING) {
// Try each introspection method — exactly one will return non-null
HumanTaskInfo task = engine.getHumanTaskInfo(workflow, instance);
if (task != null) {
// Create an inbox item with task.description(), task.inputs(), task.outputs()
// When the human completes it, call completeCurrentNode with their response
}
ReceiveEventInfo event = engine.getReceiveEventInfo(workflow, instance);
if (event != null) {
// Index the instance by event.eventType() for efficient matching
// When a matching event arrives, call completeCurrentNode with the event payload
}
WaitInfo wait = engine.getWaitInfo(workflow, instance);
if (wait != null) {
// Schedule a timer for wait.duration()
// When it fires, call completeCurrentNode with an empty result
}
}
The consuming application persists the instance and resumes it later when the external condition is met. The engine does not manage persistence, scheduling, or event subscriptions — those are the application's responsibility.
Resolving Expressions
Evaluates a Jakarta EL expression against a workflow context and returns the resolved value. Useful for rendering human task input values — for example, resolving display labels to their current context values. Supports nested map access and Jackson JsonNode navigation.
Getting Human Task Info
Returns a HumanTaskInfo record when the instance is waiting at a human-task node, null otherwise. The record contains:
| Field | Type | Description |
|---|---|---|
nodeId |
String | The human-task node ID |
nodeName |
String | The human-task node name |
description |
String | Instructions for the person completing the task |
inputs |
Map |
Display labels as keys, resolved context values as values |
outputs |
List |
Expected outputs, each with name, type, and required |
Input EL expressions (from the node's config) are evaluated against the instance context automatically — the caller receives fully resolved values.
Getting Receive Event Info
Returns a ReceiveEventInfo record when the instance is waiting at a receive-event node, null otherwise. The record contains:
| Field | Type | Description |
|---|---|---|
nodeId |
String | The receive-event node ID |
nodeName |
String | The receive-event node name |
eventType |
String | The event type this node is waiting for |
matchExpressions |
List |
Raw EL expressions used for event correlation |
The eventType can be used to index waiting instances for efficient event matching — only instances waiting for a given event type need to be checked when an event arrives.
Getting Wait Info
Returns a WaitInfo record when the instance is waiting at a wait node, null otherwise. The record contains:
| Field | Type | Description |
|---|---|---|
nodeId |
String | The wait node ID |
nodeName |
String | The wait node name |
duration |
Duration | The configured wait duration (parsed from ISO 8601) |
The consuming application reads the duration, schedules a timer, and calls completeCurrentNode when the timer expires.
Action Chaining
When an action node completes, the engine immediately evaluates edges and transitions to the next node. If the next node is also an action, it executes that too — continuing until it reaches a wait state or end. A single call to startWorkflow or completeCurrentNode may execute multiple action nodes in sequence.
A safety limit of 100 transitions per call prevents infinite loops from automated cycles. If the limit is reached, the workflow fails with a descriptive failureReason.
Immutability
All engine methods return a new WorkflowInstance. The input instance is never mutated:
WorkflowInstance waiting = engine.startWorkflow(definition, context);
WorkflowInstance completed = engine.completeCurrentNode(definition, waiting, result);
// 'waiting' is unchanged
assert waiting.status() == InstanceStatus.WAITING;
assert completed.status() == InstanceStatus.COMPLETED;
Threading
All engine methods are synchronous. When the engine invokes a NodeExecutor, it blocks until the executor returns. The consuming application can run engine calls on a background thread if async behavior is desired.