25.1 Overview
A workflow is a named, multi-step process that coordinates actions across multiple resources in response to events. Workflows are declared at the domain level because they span resources. They are the orchestration layer — the part of a business process that connects one resource’s lifecycle to another.
State Machines vs Workflows
| Dimension | State Machine (§12) | Workflow |
|---|---|---|
| Scope | Single resource | Cross-resource |
| Trigger | Direct API call (transition endpoint) | Event from any resource |
| Actor | Human initiates each transition | Runtime orchestrates automatically |
| State | Stored on the resource record | Stored in the workflow instance table |
| Branching | No (one transition at a time) | Yes (decide, parallel, forEach) |
| Waiting | No (transitions are synchronous) | Yes (wait for events with timeout) |
| Data flow | Request body only | Accumulated from all prior steps |
| Concurrency | None | Parallel steps with join semantics |
State machines define what a single resource can do. Workflows define what happens across resources when something happens. A workflow typically triggers state machine transitions on multiple resources — the state machine enforces the guards, the workflow provides the choreography.
25.2 Declaration
Workflows are declared in the domain-level workflows: block:
domain:
name: lending
version: "2.0.0"
workflows:
loanOrigination:
description: "End-to-end loan origination from application through funding"
trigger:
resource: LoanApplication
event: transition
transition: submitForReview
steps:
- name: orderCreditReport
action: create
resource: CreditReport
input:
applicationId: $trigger.record.id
borrowerName: $trigger.record.borrowerName
then: evaluateCredit
- name: evaluateCredit
wait:
resource: CreditReport
event: transition
transition: complete
filter:
applicationId: $trigger.record.id
condition: "$event.record.creditScore >= 680"
then: orderAppraisal
else: routeToManualReview
- name: orderAppraisal
action: create
resource: AppraisalOrder
input:
applicationId: $trigger.record.id
propertyAddress: $trigger.record.propertyAddress
then: awaitAppraisal
- name: awaitAppraisal
wait:
resource: AppraisalOrder
event: transition
transition: complete
filter:
applicationId: $trigger.record.id
then: routeToUnderwriting
- name: routeToManualReview
action: transition
resource: LoanApplication
target: $trigger.record.id
transition: flagForReview
then: end
- name: routeToUnderwriting
action: transition
resource: LoanApplication
target: $trigger.record.id
transition: completeCreditAnalysis
input:
creditScore: $steps.evaluateCredit.event.record.creditScore
appraisalValue: $steps.awaitAppraisal.event.record.appraisedValue
then: end
timeout:
duration: 30d
action: transition
resource: LoanApplication
target: $trigger.record.id
transition: expire
25.3 Trigger
The trigger block defines what event starts the workflow:
trigger:
resource: LoanApplication
event: transition
transition: submitForReview
condition: "$trigger.record.amount > 50000"
| Property | Type | Required | Description |
|---|---|---|---|
resource | string | yes | The resource whose event triggers the workflow |
event | string | yes | Event type: create, transition, fieldChange, history |
transition | string | if event is transition | Specific transition name |
field | string | if event is fieldChange | Specific field name |
condition | string | no | AEL expression — workflow starts only when true |
When the trigger event fires and the condition (if any) is met, the runtime creates a new workflow instance and begins executing at the first step.
25.4 Step Types
Action Step
Performs an operation on a resource:
- name: createInspection
action: create
resource: Inspection
input:
assetId: $trigger.record.id
inspectionType: emergency
then: awaitInspection
| Action | Description |
|---|---|
create | Create a new record in the target resource |
update | Update fields on an existing record |
transition | Execute a state machine transition |
notify | Emit a bus event for notification consumption |
noop | Do nothing — used for conditional skip patterns |
Action steps execute immediately and advance to the then step.
Wait Step
Pauses the workflow until a matching event occurs:
- name: awaitApproval
wait:
resource: Proposal
event: transition
transition: approve
filter:
id: $trigger.record.id
timeout:
duration: 5d
then: escalateApproval
then: nextStep
| Property | Type | Description |
|---|---|---|
wait.resource | string | Resource to watch for events |
wait.event | string | Event type to match: transition, create, fieldChange |
wait.transition | string | Transition name to match |
wait.filter | object | Field match criteria for the event record |
timeout.duration | string | Maximum wait time: 5d, 48h, 30m |
timeout.then | string | Step to execute if timeout expires |
condition | string | AEL expression evaluated on the matched event |
then | string | Step to execute when the event matches and condition is true |
else | string | Step to execute when the event matches but condition is false |
Wait steps are persisted. The workflow instance is saved to the database and resumed when a matching event arrives on the bus. The server can restart between the wait and the resume without losing workflow state.
Decide Step
Routes the workflow based on conditions:
- name: routeByScore
decide:
- condition: "$steps.creditCheck.event.record.creditScore >= 720"
then: autoApprove
- condition: "$steps.creditCheck.event.record.creditScore >= 620"
then: enhancedReview
- default: true
then: decline
Conditions are evaluated in order. The first matching condition determines the next step. If no condition matches and no default is declared, the workflow fails with an error.
Parallel Step
Executes multiple branches simultaneously:
- name: complianceChecks
parallel:
all: true
steps:
- name: irbCheck
wait:
resource: IRBProtocol
event: transition
transition: approve
filter:
studyId: $trigger.record.id
- name: coiCheck
wait:
resource: COIDisclosure
event: transition
transition: clear
filter:
personnelId: $trigger.record.piId
- name: exportControlCheck
wait:
resource: ExportControlScreening
event: transition
transition: clear
filter:
proposalId: $trigger.record.id
then: submitProposal
| Property | Type | Description |
|---|---|---|
parallel.all | boolean | If true, wait for ALL branches to complete before advancing |
parallel.any | boolean | If true, advance when ANY branch completes (cancel the rest) |
parallel.steps | array | Branches to execute in parallel |
ForEach Step
Iterates over a collection:
- name: notifyAllReviewers
forEach:
collection: $trigger.record.reviewerIds
as: reviewerId
step:
action: create
resource: ReviewAssignment
input:
documentId: $trigger.record.id
reviewerId: $item
dueDate: $trigger.record.reviewDueDate
then: awaitAllReviews
The $item variable refers to the current element. All iterations execute before the workflow advances to then.
25.5 Data Flow
Variable Scopes
| Variable | Description |
|---|---|
$trigger | The event that started the workflow |
$trigger.record | Record data from the triggering event |
$trigger.actor | User who triggered the event |
$event | The event that satisfied the current wait step |
$event.record | Record from the current wait event |
$steps.{name} | Data from a completed step |
$steps.{name}.result | Created/updated record (for action steps) |
$steps.{name}.event | Event that completed a wait step |
$workflow | Workflow-level metadata |
$workflow.id | Unique workflow instance ID |
$workflow.startedAt | Timestamp when the workflow began |
$item | Current item in a forEach loop |
Data Accumulation
Each completed step adds its result to the $steps namespace. Later steps can reference data from any earlier step. This enables data flow across the entire process:
input:
creditScore: $steps.evaluateCredit.event.record.creditScore
appraisalValue: $steps.awaitAppraisal.event.record.appraisedValue
All variable references are evaluated by the AEL engine.
25.6 Timeout and Error Handling
Step-Level Timeout
- name: awaitApproval
wait:
resource: Proposal
event: transition
transition: approve
timeout:
duration: 5d
then: escalate
If the wait step does not receive a matching event within duration, the workflow advances to the timeout.then step instead. The timeout step can escalate (notify), retry (loop back to the wait), or terminate the workflow.
Workflow-Level Timeout
timeout:
duration: 30d
action: transition
resource: LoanApplication
target: $trigger.record.id
transition: expire
If the entire workflow has not completed within duration, the timeout action executes. This is a safety net — it prevents workflow instances from waiting indefinitely.
onError
onError:
action: transition
resource: LoanApplication
target: $trigger.record.id
transition: errorState
notification:
channels: [email]
recipients:
roles: [admin]
template:
subject: "Workflow error — loanOrigination"
body: "Workflow instance {$workflow.id} failed at step {$workflow.failedStep}."
If any step fails with an unrecoverable error, the onError handler executes. It can transition the triggering record to an error state, send notifications, or both.
25.7 Workflow Instance
Each workflow execution creates an instance record:
| Column | Type | Description |
|---|---|---|
id | uuid | Instance identifier |
workflowName | string | Workflow definition name |
status | string | running, waiting, completed, failed, cancelled, timed_out |
currentStep | string | Name of the step currently executing or waiting |
triggerData | json | The event that started the workflow |
stepsData | json | Accumulated data from completed steps |
startedAt | datetime | When the instance was created |
completedAt | datetime | When the instance finished (null if still running) |
error | text | Error message if failed |
Instance Lifecycle
Event matches trigger
→ Instance created (status: running)
→ Steps execute sequentially
→ Wait steps pause instance (status: waiting)
→ Matching event resumes instance (status: running)
→ All steps complete (status: completed)
History Events
Every step produces a history event on the workflow instance. The complete step-by-step timeline is queryable:
{
"event": "stepCompleted",
"label": "Credit report ordered",
"data": {
"step": "orderCreditReport",
"result": { "id": "credit-report-1" }
}
}
Steps that modify resources also produce history events on those resources (if history is configured). Together, the workflow instance timeline and resource timelines provide a complete cross-resource process audit trail.
25.8 Workflow API
List Definitions
GET /api/v1/{domain}/workflows
Returns all workflow definitions.
List Instances
GET /api/v1/{domain}/workflows/{name}/instances
?status=waiting&since=2026-01-01
Returns instances of a specific workflow with status filtering.
Get Instance
GET /api/v1/{domain}/workflows/{name}/instances/{id}
Returns full instance details including trigger data, current step, steps data, and timeline.
Cancel Instance
POST /api/v1/{domain}/workflows/{name}/instances/{id}/cancel
{ "reason": "Application withdrawn by borrower" }
Cancels a running or waiting instance. The onError handler does NOT fire on cancel — cancellation is intentional, not an error.
25.9 Execution Model
Bus-Driven
Workflows are entirely event-driven:
- The runtime subscribes to trigger events on the bus.
- When a trigger fires, a workflow instance is created.
- Action steps execute immediately.
- Wait steps subscribe to specific events on the bus and pause.
- When a matching event arrives, the instance resumes at the next step.
- The cycle continues until the workflow reaches
endor times out.
Persistence
Workflow instances are persisted to the database after each step. If the server restarts, waiting instances are reloaded and their bus subscriptions are re-established. No workflow state is lost on restart.
Concurrency
Multiple instances of the same workflow can run simultaneously. Each instance is independent — they share the workflow definition but have separate state, separate data, and separate timelines.
Rules Integration
Actions performed by workflow steps are subject to the rules engine (§22). If a workflow step attempts a transition that violates a business rule, the rule fires and the step fails. The workflow’s onError handler is invoked.
25.10 The Workflow as Documentation
Workflow definitions serve simultaneously as:
Process documentation. The YAML IS the business process. A business analyst can read it and verify it matches the intended process. No BPMN diagram that diverges from the implementation.
Standard Operating Procedure. In regulated industries, SOPs must be documented, reviewed, and followed. The workflow definition is the SOP — machine-readable, machine-enforceable, and version-controlled.
Audit evidence. The workflow instance timeline shows every step, every decision, every timeout. When an auditor asks “was the process followed for this loan application?” the answer is the instance timeline — not a reconstruction from email threads.
25.11 Integration Points
| Feature | How It Uses Workflows |
|---|---|
| State Machines (§12) | Workflow steps trigger transitions; guards enforce eligibility |
| History (§21) | Step completions produce history events on affected resources |
| Rules (§22) | Workflow actions are subject to business rule enforcement |
| Notifications (§23) | Workflow steps can trigger notifications; wait steps listen for events that notifications may have prompted |
| Schedules (§24) | Scheduled tasks can start workflow instances via action: workflow |
End of Chapter 25.
Next: Chapter 26 — Reports, Views & Dashboards