Workflow Model
A workflow is a directed graph of nodes connected by edges. The graph defines the steps a process follows and the conditions that determine which path to take.
Workflow Definition
| Field | Type | Description |
|---|---|---|
id |
String | Unique identifier |
name |
String | Display name |
description |
String | Optional description |
nodes |
List | The nodes in the graph |
edges |
List | The edges connecting nodes |
Workflow definitions are JSON-serializable. The consuming application stores them however it chooses.
Node Types
Every node has an id, type, name, config (type-specific configuration), and position (visual coordinates).
Start
Entry point for the workflow. One per workflow.
- Config: Defines expected inputs via an
inputsarray - Behavior: Validates the initial context against the input schema, then transitions to the next node
- Edges: Supports multiple conditional outgoing edges evaluated against the initial context
{
"inputs": [
{ "name": "cveId", "type": "string", "required": true, "description": "CVE to triage" },
{ "name": "severity", "type": "string", "required": false }
]
}
Action
Automated work delegated to a NodeExecutor provided by the host application.
- Config: Must include an
actionTypefield matching a registered executor - Behavior: Invokes the executor synchronously. Output merges into the workflow context.
Human Task
Blocks until a human responds. The engine sets the instance to WAITING status.
- Config: The engine interprets three keys:
description(String) — instructions for the person completing the taskinputs(Map) — map of display label to EL expression, resolved against the workflow context at render time (e.g. {"Credit Score": "context.creditScore"})outputs(List of{name, type, required}) — defines the form schema for task completion
The validator emits MISSING_TASK_DESCRIPTION and MISSING_TASK_OUTPUTS warnings when these are absent.
- Behavior: Completes when the consuming application calls completeCurrentNode with the human's response.
Receive Event
Blocks until a matching external event arrives.
- Config:
eventType(required) andmatchexpressions (optional) for event correlation - Behavior: Completes when the consuming application calls
completeCurrentNodeafter a matching event is detected viamatchesEvent
See Event Correlation for details.
Wait
Blocks for a configured duration. The engine sets the instance to WAITING status.
- Config:
duration(String) — ISO 8601 duration (e.g.PT30M,PT2H,P1D). The validator emitsMISSING_WAIT_DURATIONif absent. - Behavior: The consuming application reads the duration via
getWaitInfo, schedules a timer, and callscompleteCurrentNodewhen it expires.
End
Terminal state. One or more per workflow.
- Config: Pass-through — typically carries outcome metadata (e.g.
"outcome": "mitigated") - Behavior: Sets the instance to
COMPLETEDstatus
Edges
Edges connect nodes and control the flow of execution.
| Field | Type | Description |
|---|---|---|
id |
String | Unique identifier |
source |
String | Source node ID |
target |
String | Target node ID |
condition |
String | Jakarta EL expression (optional) |
priority |
int | Evaluation order — lower numbers first |
isDefault |
boolean | Fallback when no conditions match |
label |
String | Display label (optional) |
Conditional Routing
When a node completes, the engine evaluates its outgoing edges:
- Edges are sorted by
priority(ascending) - Each edge's
conditionis evaluated against the workflow context using Jakarta EL - The first edge whose condition returns
trueis followed - If no condition matches, the
defaultedge is followed - If no edge matches and there is no default, the error handler is invoked
Condition expressions use context as the root variable:
Parallel Fork/Join
A node with two or more outgoing edges that are all unconditional (no condition and
isDefault: false) is a fork: entering it activates every outgoing branch concurrently, rather
than choosing one. This is the parallel counterpart to conditional routing, where exactly one edge is
taken.
{ "id": "e-analyze", "source": "fetch-cve", "target": "analyze-impact", "priority": 0, "isDefault": false }
{ "id": "e-notify", "source": "fetch-cve", "target": "notify-team", "priority": 1, "isDefault": false }
The branches re-converge at a join — the first node reachable from every branch. A join behaves as an AND-join: it waits for all incoming parallel branches to arrive before it fires, and it fires exactly once. No special node type or config marks a join; it is identified statically from the graph shape.
Fork/join regions must be well-formed:
- Every fork must have exactly one matching join (otherwise
FORK_WITHOUT_JOIN). - A parallel branch must not reach an
endnode before joining (otherwisePARALLEL_BRANCH_REACHES_END). - A node's outgoing edges must be all unconditional (fork) or use conditions/a default (exclusive
choice) — never a mix (otherwise
MIXED_FORK_EDGES).
See Validation for the rules and the worked example for a complete fork/join workflow.
Cycles
Edges can loop back to earlier nodes — this is intentional for retry/re-review patterns:
Human Task (approve plan)
→ [approved] Action (implement)
→ [rejected] Action (revise plan) → Human Task (approve plan) // loop back
Workflow Instance
A workflow instance is the runtime state of a workflow execution. It is a single JSON document — the consuming application handles persistence.
| Field | Type | Description |
|---|---|---|
id |
String | Instance identifier (UUID by default) |
workflowId |
String | Reference to the workflow definition |
currentNodeId |
String | null | The node the instance is currently at when exactly one branch is active; null when zero or multiple branches are active |
activeBranches |
Array | The branches currently executing, each { branchId, nodeId }. A non-parallel run has a single root branch. |
joinArrivals |
Object | For each pending join node, the incoming edge ids that have already arrived and are waiting for the rest. |
status |
Enum | running, waiting, completed, failed, cancelled |
context |
Map | Accumulated data from completed nodes |
history |
List | Record of visited nodes with timestamps and edge info |
failureReason |
String | Why the instance failed (null if not failed) |
createdOn |
Instant | When the instance was created |
updatedOn |
Instant | When the instance was last modified |
When multiple branches are active, currentNodeId is null (it is a convenience for the
single-branch case). Each HistoryEntry carries an optional branchId tagging which branch made the
visit; a missing branchId denotes the root branch.
Status Lifecycle
┌─────────┐
│ RUNNING │ ← startWorkflow / completeCurrentNode
└────┬─────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌──────┐ ┌───────────┐
│WAITING │ │ END │ │ ERROR │
│ │ │ │ │ │
└───┬────┘ └──┬───┘ └─────┬─────┘
│ │ │
│ ┌────▼───┐ ┌────▼───┐
│ │COMPLETED│ │ FAILED │
│ └────────┘ └────────┘
│
▼
┌──────────┐
│CANCELLED │ ← cancelWorkflow
└──────────┘