Workflow Model
A workflow is a directed graph of nodes connected by edges. It defines process steps and the conditions that determine routing.
Workflow Definition
| Field | Type | Description |
|---|---|---|
id |
String | Unique identifier |
name |
String | Display name |
description |
String | Optional description |
version |
Integer | Optional host-managed revision; instances are not automatically pinned to it |
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.
See typed configuration and wire contract v1 for the shared schema, per-node TypeScript/Java APIs, host extension fields, defaults, and source-compatibility guidance.
Node Types
Every node has an id, type, name, and config (type-specific configuration). position (visual
coordinates) is optional and nullable; the browser can lay out definitions that omit it.
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. COMPLETED output is checked/mapped into context; PENDING parks the action for host completion. FAILED enters error recovery.
Human Task
Blocks until a human responds. The engine sets the instance to WAITING status.
- Config: The engine interprets four keys:
title(String, optional) — an EL expression computing the task title, e.g. the subject of the item in a task inbox. Join text and values with the EL string concatenation operator+=(e.g."'Review ' += context.cveId"; note that+is numeric addition in EL). When the title is absent or blank, fails to evaluate, or yieldsnull, the node name is used instead.description(String) — instructions for the person completing the taskinputs(Map) — expression strings or JSON literals, resolved on entry and info reads (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,
and an INVALID_TASK_TITLE_EXPRESSION warning when the title is not valid EL.
- Behavior: Completes when the host calls completeNode with the response (completeCurrentNode for
one parked branch). The host validates the submitted form values.
Receive Event
Blocks until a matching external event arrives.
- Config:
eventType(required) andmatchexpressions (optional) for event correlation - Behavior: The host uses
matchesEventto correlate, thencompleteNodeto deliver a matching event. Optionaloutputsexpressions map selected event values into context. - Timeout (optional):
timeoutis a positive ISO 8601 duration (e.g.PT1H). A node with a timeout must have exactly one outgoing edge withisTimeout: true(like a BPMN boundary timer) plus at least one normal edge. The host schedules a timer fromReceiveEventInfo.timeout()and callsonReceiveEventTimeoutwhen it fires; the engine then follows the timeout edge. See Event Correlation.
See Event Correlation for details.
Wait
Blocks for a configured duration. The engine sets the instance to WAITING status.
- Config:
duration(String) — ISO 8601, e.g.PT30M,PT2H,P1D. The validator emitsMISSING_WAIT_DURATIONif absent. - Behavior: The host reads
getWaitInfo, schedules a timer, then callscompleteNodewhen 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) |
isTimeout |
boolean | Receive-event timeout edge (optional, default false); never chosen by normal routing |
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: completing it activates every outgoing branch, 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 execution's runtime state, held in a single JSON document. The host handles persistence.
| Field | Type | Description |
|---|---|---|
id |
String | Instance identifier (UUID by default) |
workflowId |
String | Reference to the workflow definition |
currentNodeId |
String | null | Convenience cursor for one branch; null during parallel waits. Terminal snapshots may retain a final/failing cursor. |
activeBranches |
Array | Branch records { branchId, nodeId }. Interpret activity together with status; failed/cancelled snapshots may retain records. A sequential run uses root. |
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 | Initial data plus mapped completed and partial pending outputs |
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 |
At a parallel wait, currentNodeId is null; it is a convenience for the single-branch case, not a
substitute for status and branch records. Each HistoryEntry has an optional branchId tagging its visit;
a missing history branch ID denotes root. Entries also record, when the node is entered, the input
values it received (declared workflow inputs for a start node, resolved inputs for action and human-task
nodes) and, for human-task nodes, the computed task title; both are omitted when absent, so viewers can
show per-visit values even after later nodes change the context. Java timestamps serialize as ISO strings
with the Jackson Java Time module. Old instances missing branch collections normalize to empty collections
in Java; that alone does not synthesize parked branches for safe resume. Hosts must migrate legacy runtime
state and retain the correct definition. See current contracts.
Status Lifecycle
┌─────────┐
│ RUNNING │ ← startWorkflow / completeCurrentNode
└────┬─────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌──────┐ ┌───────────┐
│WAITING │ │ END │ │ ERROR │
│ │ │ │ │ │
└───┬────┘ └──┬───┘ └─────┬─────┘
│ │ │
│ ┌────▼───┐ ┌────▼───┐
│ │COMPLETED│ │ FAILED │
│ └────────┘ └────────┘
│
▼
┌──────────┐
│CANCELLED │ ← cancelWorkflow
└──────────┘