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 |
The form fields to complete the task (see below) |
Input EL expressions (from the node's config) are evaluated against the instance context automatically — the caller receives fully resolved values.
Output field metadata
Each OutputDefinition describes one form field a person fills in to complete the task. Only name
is required; every other attribute is optional and backward-compatible — an output that declares only
{name, type, required} behaves exactly as before, and the engine derives sensible defaults for
anything omitted. Hosts (such as Axiom) use this metadata to render the runtime completion form.
| Field | Required | Meaning | Default when omitted |
|---|---|---|---|
name |
yes | context key the answer is stored under | — |
type |
no | semantic type: string/number/boolean/object |
string |
required |
no | must be provided to complete | false |
label |
no | human-readable field label | name |
description |
no | help/hint text shown under the field | none |
widget |
no | rendering hint: text/textarea/select (string types only) |
inferred from type |
defaultValue |
no | pre-filled value | none |
options |
no | List<OutputOption> (label, value) — choices for widget: select |
none |
Default widget inference (when widget is omitted): string → text, number → number,
boolean → checkbox, object → textarea. widget only meaningfully applies to string-typed
outputs, and select requires options. The values in HumanTaskInfo.outputs are fully resolved —
label and widget are populated with their defaults so the caller never has to re-derive them.
These attributes are authoring metadata only. As before, the engine does not validate submitted human answers against the declared outputs — hosts remain responsible for validating submissions.
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.
Parallel Execution
When execution reaches a fork (a node whose outgoing edges are all unconditional), the engine activates
every branch. advance progresses all runnable branches; the instance stays RUNNING while any branch
can still make progress and WAITING when every remaining branch is parked on a human task, received
event, or wait node.
WorkflowInstance instance = engine.startWorkflow(workflow, context);
// After a fork, more than one branch may be active at once.
List<ActiveBranch> active = instance.activeBranches();
Key semantics:
- AND-join (wait-for-all). A join node fires once, only after every parallel branch has arrived.
Arrivals accumulate in
joinArrivalsuntil the last one lands. - Fail-fast. If any branch fails and is not recovered by an error handler, the whole instance
transitions to
FAILED; sibling branches stop. - END cancels siblings. An
endnode reached on any branch terminates the entire instance. currentNodeIdisnullwhenever zero or more than one branch is active. UseactiveBranchesto inspect concurrent progress.
Resume a specific parked branch by node id — sibling branches keep waiting:
// Complete one waiting human-task/event/wait branch, addressed by node id:
WorkflowInstance next = engine.completeNode(workflow, instance, "notify-team", result);
completeCurrentNode remains available and delegates to completeNode for the single-branch case.
The branch-addressable info accessors (getHumanTaskInfo, getReceiveEventInfo, getWaitInfo,
matchesEvent) take a nodeId so concurrent waiting branches can be inspected independently.
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.