Workflow: Flow Type (Future Development)
Introduction
A Flow workflow is a specialized workflow type that declaratively manages entity status transitions based on conditions evaluated against related data. Unlike traditional imperative workflows that execute a sequence of steps, flows define states, transitions, and conditions that automatically move an entity through its lifecycle.
Flow workflows are ideal when you need to:
- Manage order lifecycle based on multiple data sources (tracking events, commodity statuses, charges, invoices)
- Enforce business rules for valid status transitions
- React to changes across related entities (e.g., update order status when all commodities are delivered)
- Support hierarchical states for complex workflows with sub-states
Flow Engine Behavior
This section describes the runtime behavior of the Flow engine when evaluating and executing transitions.
Cascade Depth
When a transition changes an entity's status, the flow may re-evaluate and trigger additional transitions (e.g., Picked Up → In Transit → Out for Delivery from a single trigger). This is called cascading.
- Default max depth:
5cascading evaluations per trigger - Configurable: Override per-workflow using
workflow.maxCascadeDepth - On limit reached: The engine stops evaluation and logs a warning. The entity remains in the last successfully transitioned state.
workflow:
name: "Order Status Flow"
maxCascadeDepth: 10 # Allow deeper cascading for complex flows
Concurrency Model
Flow evaluations for the same entity are processed sequentially via a queue:
- When multiple triggers fire for the same entity (e.g., simultaneous API calls), they are queued and processed one at a time
- Each evaluation uses the latest persisted state of the entity
- Different entities are evaluated independently and can run in parallel
- The queue guarantees ordering: triggers are processed in the order they were received
Consumed Events
To prevent transitions from re-triggering on historical data, the engine tracks consumed events:
- When an event (e.g., a
TrackingEvent) satisfies a transition condition and the transition executes, that event is marked as consumed for this flow - Consumed events are persisted in the database and survive application restarts
- Aggregation functions (
any(),all(), etc.) automatically exclude consumed events from evaluation - This prevents circular transitions (e.g.,
In Transit ↔ At Hub) from oscillating on historical events
Execution Order
When a transition fires, the engine executes in this order:
1. Evaluate conditions → All pass
2. Update entity status (set to target state)
3. Execute FROM state's onExit steps
4. Execute transition steps (sequentially)
5. Execute TO state's onEnter steps
6. Mark triggering events as consumed
7. Fire lifecycle events (onStateEntered, onTransitionCompleted)
8. Re-evaluate for cascading transitions (if within depth limit)
Step Failure Behavior
Transition steps and state lifecycle steps (onEnter, onExit) are best-effort side-effects:
- If a step fails and
continueOnErrorisfalse(default), remaining steps in that phase are skipped - The status change is NOT reverted — the entity remains in the new state
- Failed steps are logged with error details for troubleshooting
- Use
continueOnError: trueon non-critical steps (e.g., notifications) to ensure subsequent steps still execute
This design reflects that steps typically perform side-effects (emails, logging, webhooks) that should not block state progression.
Key Concepts
States
States represent the possible statuses an entity can be in. Flow workflows use a hybrid approach:
- Reference existing status entities (e.g.,
OrderStatus) by name - Define transition rules and hierarchy in the workflow YAML
- Override properties like
requireConfirmationper workflow
States can be organized hierarchically, where parent states contain child states:
InProgress (parent)
├── Picked Up (child)
├── In Transit (child)
└── Out for Delivery (child)
When in a child state, the entity is also considered to be in the parent state, enabling transitions that apply to all InProgress states.
Transitions
Transitions define how an entity moves from one state to another. Each transition includes:
- Source state(s): The state(s) the entity must be in
- Target state: The state to transition to
- Trigger type:
auto,manual, orevent - Conditions: Expressions that must evaluate to
true - User-facing messages (manual transitions): Optional messages shown when a condition blocks a manual transition
- Priority: Determines which transition executes when multiple are valid
- Actions: Steps to execute during the transition
Data Loading & Aggregations
Flow workflows load the entity with related data (specified via includes) and evaluate conditions using aggregations:
entity:
name: "Order"
includes:
- "orderCommodities.commodity.commodityStatus"
- "orderEvents.trackingEvent.eventDefinition"
Aggregations query directly against the loaded entity using NCalc expressions:
| Function | Description | Example |
|---|---|---|
all() | True if all items match the condition | all([Order.OrderCommodities], [each.Status] = 'Delivered') |
any() | True if any item matches the condition | any([Order.OrderEvents], [each.EventCode] = 'PICKUP') |
sum() | Sum of numeric values | sum([Order.OrderCharges], [each.Amount]) |
count() | Number of items in collection | count([Order.OrderCommodities]) |
Triggers
Triggers define when the flow should re-evaluate conditions:
- Entity triggers: Evaluate when the managed entity or related entities change (Added, Modified, Deleted)
- Position:
Before(pre-save, allows entity modification) orAfter(post-save, default) - Field filtering: Optionally limit
Modifiedtriggers to specific field changes for better performance
For time-based re-evaluation (e.g., overdue detection, stale tracking), use a separate scheduled trigger workflow with the Order/Transition@1 task to programmatically trigger flow re-evaluation on a schedule.
YAML Structure
A Flow workflow uses the standard workflow manifest with specialized sections:
workflow:
workflowId: "00000000-0000-0000-0000-000000000000"
name: "Order Status State Machine"
isActive: true
workflowType: "Flow"
executionMode: "Sync"
version: "1.0"
maxCascadeDepth: 5 # Optional: max cascading transitions per trigger (default: 5)
tags:
- "flow"
- "orderStatus"
# Entity configuration (what to manage and what data to load)
entity:
name: "Order"
type: "ParcelShipment" # required for Order and Accounting Transactions
includes: # Related data to load for condition evaluation
- "orderCommodities.commodity.commodityStatus"
- "orderEvents.trackingEvent.eventDefinition"
- "orderCharges.charge"
- "accountingTransactions"
query: "{ id, number, commodities { id, quantity }, customer { name } }" # Optional: GraphQL-style default query
# Optional variables for expressions and step templates
variables: []
# Reusable conditions over collections (All/Any/None/Sum/Custom)
aggregations: []
# Hierarchical state definitions
states: []
# Transition rules with conditions and actions
transitions: []
# When to re-evaluate the flow
triggers: []
# Lifecycle event handlers
events: []
Attribute Description
Workflow-Level Attributes
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
workflow.workflowId | string | Yes | Unique identifier (UUID format) | "a1b2c3d4-..." |
workflow.name | string | Yes | Display name of the flow | "Order Status State Machine" |
workflow.isActive | boolean | Yes | Whether the flow is active. Only one active Flow per entity+type is allowed. | true |
workflow.workflowType | string | Yes | Must be "Flow" | "Flow" |
workflow.executionMode | string | Yes | Should be "Sync" for immediate transitions | "Sync" |
workflow.version | string | Yes | Version of the workflow definition | "1.0" |
workflow.maxCascadeDepth | number | No | Max cascading transitions per trigger. Default: 5 | 10 |
workflow.tags[] | array | Recommended | Tags for categorization | ["stateMachine", "order"] |
Entity Configuration
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
entity.name | string | Yes | Entity type this flow manages | "Order" |
entity.type | string | Conditional | Entity subtype filter. Required for Order (e.g., "ParcelShipment") and AccountingTransaction flows. | "ParcelShipment" |
entity.includes[] | array | No | Related data to load (navigation paths). If omitted, loads common relations. | ["orderCommodities.commodity"] |
entity.query | string | No | GraphQL-style query specifying default fields to load. States can override this. If omitted, uses system defaults. | "{ commodities { id, quantity }, customer { name } }" |
Active Flow Uniqueness (Per Entity + Type)
For Flow workflows, CXTMS supports only one active Flow workflow per entity and type combination at a time.
- Rule: At most one Flow workflow with
workflow.isActive: truecan target a givenentity.name+entity.typecombination.- Example: One active Flow for
entity.name: "Order"withentity.type: "ParcelShipment", AND a separate active Flow forentity.name: "Order"withentity.type: "AirShipmentOrder". - You may still have another active Flow for
entity.name: "Commodity"at the same time.
- Example: One active Flow for
- Why: Multiple active Flows targeting the same entity+type would compete to transition the same record, resulting in ambiguous and potentially conflicting state changes.
- Without type: For entities that don't use
entity.type(e.g.,Commodity), uniqueness is perentity.nameonly.
If you need different behavior for a subset of records within the same type, implement that variation inside the same Flow using conditions (e.g., custom fields, division) rather than creating multiple active Flows for the same entity+type.
Entity Includes
The includes property specifies which related entities should be loaded for condition evaluation. Use dot notation to specify nested relationships:
entity:
name: "Order"
includes:
- "orderCommodities.commodity.commodityStatus" # Load commodities with their status
- "orderEvents.trackingEvent.eventDefinition" # Load tracking events with definitions
- "orderCharges.charge" # Load charges
- "accountingTransactions" # Load invoices/transactions
- "jobOrders.job.jobStatus" # Load related jobs
If includes is omitted, the flow loads a default set of common relationships for the entity type.
Entity Query (GraphQL-style Data Loading)
The query property allows you to specify a GraphQL-style query string that controls exactly which fields are loaded for the entity. This provides fine-grained control over data loading for performance optimization.
entity:
name: "Order"
type: "ParcelShipment"
query: "{ id, number, status, commodities { id, quantity, weight }, customer { name, email } }"
Key points:
- Optional: If omitted, the system loads default fields based on entity type
- Overridable: Individual states can override this query with their own (see State Query)
- Multiline support: Use YAML literal block syntax for complex queries:
entity:
name: "Order"
query: |
{
id
number
customer {
name
email
billingAddress { city, state }
}
commodities {
id
quantity
description
}
}
Aggregations
Aggregations define reusable, named NCalc expressions that evaluate against entity collections. They are especially useful for:
- Parameterized conditions (e.g.,
hasTrackingEvent('DEPART_DC')) - Complex expressions that benefit from a descriptive name
- Conditions reused across multiple transitions
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
aggregations[].name | string | Yes | Unique identifier for this aggregation | "allCommoditiesDelivered" |
aggregations[].expression | string | Yes | NCalc expression using aggregation functions | "all([Order.OrderCommodities], [each.Commodity.CommodityStatus.StatusStage] = 'Completed')" |
aggregations[].parameter | string | No | Dynamic parameter name for parameterized aggregations | "eventCode" |
NCalc Aggregation Functions
The expression engine supports these built-in aggregation functions:
| Function | Description | Example |
|---|---|---|
all(collection, condition) | True if all items match the condition | all([Order.OrderCommodities], [each.Status] = 'Delivered') |
any(collection, condition) | True if any item matches the condition | any([Order.OrderEvents], [each.EventCode] = 'PICKUP') |
sum(collection, expression) | Sum of numeric values from expression | sum([Order.OrderCharges], [each.Amount]) |
count(collection) | Number of items in collection | count([Order.OrderCommodities]) |
first(collection, [expression]) | First item (optionally evaluate expression against it) | first([Order.OrderEvents], [item.EventDate]) |
last(collection, [expression]) | Last item (optionally evaluate expression against it) | last([Order.OrderEvents], [item.EventDate]) |
distinct(collection, expression) | Unique values from collection | distinct([Order.OrderCommodities], [each.WarehouseId]) |
groupBy(collection, keyExpression) | Group items by key | groupBy([Order.OrderCommodities], [each.CommodityStatus]) |
Note: Inside aggregation functions, use [each.property] to reference the current item being evaluated. For first() and last(), use [item.property].
Variables
Variables define named values that can be referenced from:
- Step templates (e.g.,
{{ variableName }}) - Expressions (where supported by the expression engine)
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
variables[].name | string | Yes | Variable name (unique within the workflow) | "customerId" |
variables[].value | string | Yes | Template/expression that resolves to a value | "{{ Order.CustomerId }}" |
Aggregation Examples
aggregations:
# Simple aggregation - checks all commodities are delivered
- name: "allCommoditiesDelivered"
expression: "all([Order.OrderCommodities], [each.Commodity.CommodityStatus.StatusStage] = 'Completed')"
# Check if any tracking event exists with specific code
- name: "hasPickupEvent"
expression: "any([Order.OrderEvents], [each.TrackingEvent.EventDefinition.EventCode] = 'PICKUP')"
# Parameterized aggregation - reusable with different event codes
- name: "hasTrackingEvent"
parameter: "eventCode"
expression: "any([Order.OrderEvents], [each.TrackingEvent.EventDefinition.EventCode] = [eventCode])"
# Sum of charge amounts
- name: "totalCharges"
expression: "sum([Order.OrderCharges], [each.Charge.Amount])"
# Count commodities
- name: "commodityCount"
expression: "count([Order.OrderCommodities])"
# Check all charges are paid or void
- name: "allChargesPaid"
expression: "all([Order.OrderCharges], [each.Charge.ChargeStatus] = 'Paid' || [each.Charge.ChargeStatus] = 'Void')"
# Used in transitions as:
# - expression: "[allCommoditiesDelivered] = true"
# - expression: "hasTrackingEvent('DEPART_DC') = true"
# - expression: "[totalCharges] > 0"
Inline Expressions (Alternative)
For simple, one-off conditions, keep expressions readable and focused. If you need to evaluate collections repeatedly (commodities, events, charges), prefer named aggregations instead of embedding complex collection logic inline.
transitions:
- name: "check_draft"
conditions:
# Simple property check - no aggregation needed
- expression: "[Order.IsDraft] = false"
Recommendation: Use aggregations for parameterized and frequently-used conditions; use inline expressions for simple property checks.
States (YAML)
States define the possible statuses and their hierarchy.
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
states[].name | string | Yes | State name (references OrderStatus) | "Scheduled" |
states[].stage | string | No | Status stage: Pending, InProgress, Completed | "InProgress" |
states[].parent | string | No | Parent state name for hierarchical states | "InProgress" |
states[].isInitial | boolean | No | Whether this is an initial state | true |
states[].isFinal | boolean | No | Final state: no outgoing transitions allowed (including from: "*" wildcards) | true |
states[].requireConfirmation | boolean | No | Whether transitions to this state require user confirmation | false |
states[].query | string | No | GraphQL-style query overriding the entity's default query for this state | "{ commodities, charges }" |
states[].onEnter | array | No | Steps to execute when entering this state (after transition) | [...] |
states[].onExit | array | No | Steps to execute when exiting this state (before transition) | [...] |
State-Level Steps
States can have onEnter and onExit steps that execute for any transition entering or leaving the state:
states:
- name: "Delivered"
parent: "Completed"
onEnter:
# Runs whenever ANY transition enters "Delivered" state
- task: "Email/Send@1"
name: "sendDeliveryNotification"
inputs:
to: "{{ Order.Customer.Email }}"
templateWorkflowId: "delivery-notification"
templateVariables:
orderNumber: "{{ Order.OrderNumber }}"
- task: "ActionEvent/Create@1"
name: "logDelivery"
inputs:
action:
eventName: "order.delivered"
eventData:
orderId: "{{ Order.OrderId }}"
- name: "InProgress"
stage: "InProgress"
onExit:
# Runs whenever leaving ANY InProgress child state
- task: "Utilities/Log@1"
inputs:
level: "Info"
message: "Order {{ Order.OrderId }} leaving InProgress stage"
Execution Order with State Steps:
1. Evaluate conditions → All pass
2. Update entity status (set to target state)
3. Execute FROM state's onExit steps
4. Execute transition steps (sequentially)
5. Execute TO state's onEnter steps
6. Mark triggering events as consumed
7. Fire lifecycle events
8. Re-evaluate for cascading transitions (if within depth limit)
See Flow Engine Behavior > Execution Order for full details including failure behavior.
When to use State steps vs Transition steps:
| Use Case | Where to Define |
|---|---|
| Action specific to one transition | transitions[].steps |
| Action for ANY entry to a state | states[].onEnter |
| Action for ANY exit from a state | states[].onExit |
| Example: "Send email when Delivered" | states[name="Delivered"].onEnter |
| Example: "Log when manually rescheduled" | transitions[name="manual_reschedule"].steps |
State Query
States can define their own query property to override the entity-level query when the entity is in that state. This enables state-based data loading optimization — loading only the data needed for the current workflow phase.
entity:
name: "Order"
type: "ParcelShipment"
query: "{ id, number, status }" # Default: minimal data
states:
- name: "Draft"
isInitial: true
query: "{ customer { name, email } }" # Only need customer info while drafting
- name: "Shipped"
query: "{ commodities { id, quantity, tracking }, charges { amount } }" # Need tracking data
- name: "Delivered"
parent: "Completed"
# No query - uses entity default "{ id, number, status }"
- name: "Received"
parent: "Completed"
isFinal: true
query: "{ invoices { id, amount, status }, payments { id, amount } }" # Need financial data
Use cases for state-specific queries:
| State Phase | Query Focus | Example |
|---|---|---|
| Draft/Initial | Customer and basic info | "{ customer { name, contact } }" |
| In Transit | Tracking and logistics | "{ commodities { tracking }, stops { eta } }" |
| Billing | Financial data | "{ charges, invoices, payments }" |
| Completed | Minimal for archival | "{ id, number }" or use entity default |
Note: If a state does not specify a query, it inherits the entity-level query. If neither is specified, system defaults are used.
Transitions (YAML)
Transitions define the rules for moving between states.
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
transitions[].name | string | Yes | Unique identifier for this transition | "schedule_to_dcOutbound" |
transitions[].from | string or array | Yes | Source state(s). Use "*" for any state | "Scheduled" or ["A", "B"] |
transitions[].to | string | Yes | Target state (must be a leaf state, not a parent) | "DC Outbound" |
transitions[].trigger | string | Yes | Trigger type: auto, manual, event | "auto" |
transitions[].priority | number | No | Priority (higher = checked first). Default: 50 | 100 |
transitions[].displayName | string | No | UI label for manual transitions | "Move to DC Outbound" |
transitions[].conditions[] | array | No | Conditions that must all be true for transition | [...] |
transitions[].steps[] | array | No | Tasks to execute after the transition completes | [...] |
transitions[].eventName | string | No | Event name for event trigger type | "order.exception.raised" |
transitions[].requireConfirmation | boolean | No | Require user confirmation for manual transitions (overrides state setting) | true |
Transition Steps
Steps are tasks that execute after the transition completes (status is updated). They follow the same structure as workflow activity steps.
| Attribute | Type | Required | Description |
|---|---|---|---|
steps[].task | string | Yes | Task type and version (e.g., "Email/Send@1") |
steps[].name | string | No | Step name for output reference |
steps[].inputs | object | No | Input parameters for the task |
steps[].conditions | string | No | Condition to execute this step |
steps[].continueOnError | boolean | No | Continue if step fails (default: false) |
Execution Order:
1. Evaluate conditions → All pass
2. Update entity status (set to target state)
3. Execute FROM state's onExit steps
4. Execute transition steps (sequentially)
5. Execute TO state's onEnter steps
6. Fire lifecycle events (onStateEntered, onTransitionCompleted)
Step failures do not revert the status change. See Step Failure Behavior.
Example:
transitions:
- name: "arrived_to_delivered"
from: "Arrived At Store"
to: "Delivered"
trigger: "auto"
conditions:
- expression: "[allCommoditiesDelivered] = true"
steps:
# Create tracking event
- task: "TrackingEvent/Create@1"
name: "createDeliveryEvent"
inputs:
orderId: "{{ Order.OrderId }}"
eventCode: "DELIVERED"
description: "All commodities delivered"
# Send notification email
- task: "Email/Send@1"
name: "sendDeliveryEmail"
inputs:
to: "{{ Order.Customer.Email }}"
templateWorkflowId: "delivery-notification"
templateVariables:
orderNumber: "{{ Order.OrderNumber }}"
# Create action event for audit
- task: "ActionEvent/Create@1"
name: "logDelivery"
inputs:
action:
eventName: "order.delivered"
eventData:
orderId: "{{ Order.OrderId }}"
Transition Conditions
Conditions determine whether a transition can execute. All conditions must evaluate to true for the transition to proceed.
| Attribute | Type | Required | Description |
|---|---|---|---|
conditions[].expression | string | Yes | NCalc expression that must evaluate to true |
conditions[].message | string | No | Error message shown to user when condition fails (for manual transitions) |
Behavior based on trigger type:
| Trigger Type | Condition Fails | User Experience |
|---|---|---|
auto | Transition silently skipped | No feedback - tries next transition |
manual | Transition blocked | Error message shown to user |
event | Transition silently skipped | No feedback - event ignored for this transition |
Example:
transitions:
- name: "arrived_to_delivered"
from: "Arrived At Store"
to: "Delivered"
trigger: "auto"
conditions:
# For auto: silently skips if false
- expression: "[allCommoditiesDelivered] = true"
- name: "manual_forceComplete"
from: "InProgress"
to: "Completed"
trigger: "manual"
displayName: "Force Complete"
conditions:
# For manual: shows error message if false
- expression: "[Order.IsDraft] = false"
message: "Cannot complete draft orders"
- expression: "[currentUser.hasPermission('order.forceComplete')]"
message: "You don't have permission to force complete"
Trigger Configuration
Triggers define when the flow should re-evaluate transitions. Flow workflows use Entity triggers that fire when related entities are created, modified, or deleted.
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
triggers[].type | string | Yes | Must be "Entity" for Flow workflows | "Entity" |
triggers[].entityName | string | Yes | Entity type to monitor (e.g., Order, Commodity, TrackingEvent) | "TrackingEvent" |
triggers[].eventType | string | Yes | Event type: Added, Modified, or Deleted | "Added" |
triggers[].position | string | No | When to fire: Before (pre-save) or After (post-save). Default: After | "After" |
triggers[].fields[] | array | No | Specific fields to monitor for Modified events (improves performance) | ["commodityStatusId"] |
triggers[].conditions[] | array | No | NCalc conditions for trigger activation | [...] |
Trigger Position:
After(default): Flow evaluates after entity changes are persisted. Use for most scenarios.Before: Flow evaluates before persistence. Use when the flow needs to modify the entity via[trackedEntity]before it's saved.
For time-based re-evaluation (e.g., overdue detection, stale tracking), use a separate scheduled trigger workflow with the Order/Transition@1 task.
Events
Lifecycle events that fire during flow execution.
| Attribute | Type | Required | Description |
|---|---|---|---|
events[].type | string | Yes | Event type (see below) |
events[].state | string | No | Specific state or "*" for all |
events[].steps[] | array | Yes | Steps to execute |
Event Types:
onStateEntered: Fires after entering a stateonStateExited: Fires before exiting a stateonTransitionStarted: Fires before a transition executesonTransitionCompleted: Fires after a transition completesonTransitionBlocked: Fires when a condition blocks a manual transitiononEvaluationCompleted: Fires after flow evaluation (even if no transition)
Examples
Example 1: Simple Parcel Delivery and Return Flow
This example manages a straightforward parcel delivery lifecycle with return support. The flow tracks parcels from scheduling through delivery and payment, with a separate return path for customer returns:
workflow:
name: "Parcel Delivery and Return Flow"
workflowId: "b2c3d4e5-f6a7-8901-bcde-f23456789012"
isActive: true
workflowType: "Flow"
executionMode: "Sync"
version: "1.0"
maxCascadeDepth: 5
tags:
- "flow"
- "parcel"
- "delivery"
- "returns"
entity:
name: "Order"
type: "ParcelShipment"
# Load related data for condition evaluation
includes:
- "orderCommodities.commodity.commodityStatus"
- "orderEvents.trackingEvent.eventDefinition"
- "orderCharges.charge"
- "accountingTransactions"
variables:
- name: "organizationId"
value: "{{ Order.OrganizationId }}"
# Reusable aggregations for cleaner condition logic
aggregations:
- name: "allParcelsDelivered"
expression: "all([Order.OrderCommodities], [each.Commodity.CommodityStatus.StatusStage] = 'Completed')"
- name: "hasTrackingEvent"
parameter: "eventCode"
expression: "any([Order.OrderEvents], [each.TrackingEvent.EventDefinition.EventCode] = [eventCode])"
- name: "allChargesPaid"
expression: "all([Order.OrderCharges], [each.Charge.ChargeStatus] = 'Paid' || [each.Charge.ChargeStatus] = 'Void')"
- name: "hasPostedInvoice"
expression: "any([Order.AccountingTransactions], [each.TransactionType] = 'Invoice' && [each.Status] = 'Posted')"
# State hierarchy: Pending → InProgress → Completed (with Return branch)
states:
# Top-level stages
- name: "Pending"
stage: "Pending"
- name: "InProgress"
stage: "InProgress"
- name: "Completed"
stage: "Completed"
# Pending sub-states
- name: "Scheduled"
parent: "Pending"
isInitial: true
# InProgress sub-states (linear delivery flow)
- name: "Picked Up"
parent: "InProgress"
- name: "In Transit"
parent: "InProgress"
- name: "Out for Delivery"
parent: "InProgress"
# Completed sub-states (forward delivery flow)
- name: "Delivered"
parent: "Completed"
onEnter:
- task: "Email/Send@1"
name: "sendDeliveryNotification"
inputs:
to: "{{ Order.Customer.Email }}"
templateWorkflowId: "delivery-notification-template"
templateVariables:
orderNumber: "{{ Order.OrderNumber }}"
deliveryDate: "{{ now() }}"
- name: "Invoiced"
parent: "Completed"
- name: "Received"
parent: "Completed"
isFinal: true
# Return flow states
- name: "Return Requested"
parent: "Completed"
- name: "Return In Transit"
parent: "Completed"
- name: "Returned"
parent: "Completed"
isFinal: true
onEnter:
- task: "Email/Send@1"
name: "sendReturnConfirmation"
inputs:
to: "{{ Order.Customer.Email }}"
templateWorkflowId: "return-confirmation-template"
templateVariables:
orderNumber: "{{ Order.OrderNumber }}"
# Transition rules
transitions:
# === Forward Delivery Flow ===
# Scheduled → Picked Up (when parcel is collected)
- name: "scheduled_to_pickedUp"
from: "Scheduled"
to: "Picked Up"
trigger: "auto"
priority: 50
conditions:
- expression: "[Order.IsDraft] = false"
- expression: "hasTrackingEvent('PICKUP') = true"
steps:
- task: "Utilities/Log@1"
inputs:
level: "Info"
message: "Order {{ Order.OrderNumber }} picked up"
# Picked Up → In Transit (parcel leaves origin facility)
- name: "pickedUp_to_inTransit"
from: "Picked Up"
to: "In Transit"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('IN_TRANSIT') = true"
# In Transit → Out for Delivery (parcel arrives at destination facility)
- name: "inTransit_to_outForDelivery"
from: "In Transit"
to: "Out for Delivery"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('OUT_FOR_DELIVERY') = true"
# Out for Delivery → Delivered (successful delivery)
- name: "outForDelivery_to_delivered"
from: "Out for Delivery"
to: "Delivered"
trigger: "auto"
priority: 50
conditions:
- expression: "[allParcelsDelivered] = true"
- expression: "hasTrackingEvent('DELIVERED') = true"
# Delivered → Invoiced (invoice posted)
- name: "delivered_to_invoiced"
from: "Delivered"
to: "Invoiced"
trigger: "auto"
priority: 50
conditions:
- expression: "[hasPostedInvoice] = true"
# Invoiced → Received (payment completed)
- name: "invoiced_to_received"
from: "Invoiced"
to: "Received"
trigger: "auto"
priority: 50
conditions:
- expression: "[allChargesPaid] = true"
# === Return Flow ===
# Delivered → Return Requested (customer initiates return)
- name: "delivered_to_returnRequested"
from: "Delivered"
to: "Return Requested"
trigger: "manual"
displayName: "Request Return"
priority: 60
conditions:
- expression: "[Order.AllowReturns] = true"
message: "Returns are not allowed for this order"
steps:
- task: "Email/Send@1"
name: "sendReturnInstructions"
inputs:
to: "{{ Order.Customer.Email }}"
templateWorkflowId: "return-instructions-template"
templateVariables:
orderNumber: "{{ Order.OrderNumber }}"
# Return Requested → Return In Transit (return shipment picked up)
- name: "returnRequested_to_returnInTransit"
from: "Return Requested"
to: "Return In Transit"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('RETURN_PICKUP') = true"
# Return In Transit → Returned (return received at origin)
- name: "returnInTransit_to_returned"
from: "Return In Transit"
to: "Returned"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('RETURN_RECEIVED') = true"
# === Manual Override Transitions ===
# Force complete from any InProgress state
- name: "manual_forceComplete"
from: "InProgress"
to: "Received"
trigger: "manual"
displayName: "Force Complete"
priority: 100
requireConfirmation: true
conditions:
- expression: "[currentUser.hasPermission('order.forceComplete')]"
message: "You do not have permission to force complete orders"
# Triggers - when to re-evaluate the flow
triggers:
# Monitor Order changes
- type: "Entity"
entityName: "Order"
eventType: "Modified"
position: "After"
fields: ["orderStatusId", "isDraft"]
# Monitor TrackingEvent additions
- type: "Entity"
entityName: "TrackingEvent"
eventType: "Added"
position: "After"
conditions:
- expression: "[entity.OrderId] <> null"
# Monitor Commodity status changes
- type: "Entity"
entityName: "Commodity"
eventType: "Modified"
position: "After"
fields: ["commodityStatusId"]
# Monitor Charge payment status
- type: "Entity"
entityName: "Charge"
eventType: "Modified"
position: "After"
fields: ["chargeStatus"]
# Monitor Invoice posting
- type: "Entity"
entityName: "AccountingTransaction"
eventType: "Added"
position: "After"
conditions:
- expression: "[entity.OrderId] <> null"
# Lifecycle events for notifications and logging
events:
- type: "onStateEntered"
state: "*"
steps:
- task: "ActionEvent/Create@1"
inputs:
action:
eventName: "order.status.changed"
eventData:
orderId: "{{ Order.OrderId }}"
orderNumber: "{{ Order.OrderNumber }}"
fromStatus: "{{ previousState.OrderStatusName }}"
toStatus: "{{ currentState.OrderStatusName }}"
transitionName: "{{ transition.name }}"
Example 2: Invoice and Payment Processing Flow
This example demonstrates a complete invoice lifecycle with email notifications and payment tracking:
workflow:
name: "Invoice and Payment Processing Flow"
workflowId: "c3d4e5f6-a7b8-9012-cdef-345678901234"
isActive: true
workflowType: "Flow"
executionMode: "Sync"
version: "1.0"
tags:
- "flow"
- "invoice"
- "accounting"
- "payment"
entity:
name: "AccountingTransaction"
includes:
- "payments"
- "customer"
- "order"
- "transactionLines"
variables:
- name: "customerId"
value: "{{ AccountingTransaction.CustomerId }}"
- name: "invoiceNumber"
value: "{{ AccountingTransaction.TransactionNumber }}"
# Aggregations for payment tracking
aggregations:
- name: "totalPaymentAmount"
expression: "sum([AccountingTransaction.Payments], [each.Amount])"
- name: "hasAnyPayment"
expression: "any([AccountingTransaction.Payments], [each.Status] = 'Applied' && [each.Amount] > 0)"
- name: "isFullyPaid"
expression: "[totalPaymentAmount] >= [AccountingTransaction.TotalAmount]"
- name: "hasValidLineItems"
expression: "all([AccountingTransaction.TransactionLines], [each.Amount] <> 0 && [each.Description] <> null)"
states:
# Draft state - invoice being prepared
- name: "Draft"
stage: "Pending"
isInitial: true
# Pending Review - awaiting approval
- name: "Pending Review"
stage: "Pending"
# Posted - invoice sent to customer
- name: "Posted"
stage: "InProgress"
onEnter:
# Email invoice to customer when posted
- task: "Email/Send@1"
name: "sendInvoiceToCustomer"
inputs:
to: "{{ AccountingTransaction.Customer.Email }}"
cc: "{{ AccountingTransaction.Customer.AccountsPayableEmail }}"
templateWorkflowId: "invoice-email-template"
templateVariables:
invoiceNumber: "{{ AccountingTransaction.TransactionNumber }}"
customerName: "{{ AccountingTransaction.Customer.Name }}"
totalAmount: "{{ AccountingTransaction.TotalAmount }}"
dueDate: "{{ AccountingTransaction.DueDate }}"
invoiceUrl: "{{ baseUrl }}/invoices/{{ AccountingTransaction.TransactionId }}"
attachments:
- type: "Invoice"
entityId: "{{ AccountingTransaction.TransactionId }}"
format: "PDF"
# Create action event for audit
- task: "ActionEvent/Create@1"
name: "logInvoicePosted"
inputs:
action:
eventName: "invoice.posted"
eventData:
transactionId: "{{ AccountingTransaction.TransactionId }}"
invoiceNumber: "{{ AccountingTransaction.TransactionNumber }}"
customerId: "{{ AccountingTransaction.CustomerId }}"
totalAmount: "{{ AccountingTransaction.TotalAmount }}"
# Sent - invoice delivered and confirmed
- name: "Sent"
stage: "InProgress"
# Partially Paid - some payment received
- name: "Partially Paid"
stage: "InProgress"
onEnter:
# Send payment received notification
- task: "Email/Send@1"
name: "sendPartialPaymentConfirmation"
inputs:
to: "{{ AccountingTransaction.Customer.Email }}"
templateWorkflowId: "partial-payment-confirmation-template"
templateVariables:
invoiceNumber: "{{ AccountingTransaction.TransactionNumber }}"
paidAmount: "{{ totalPaymentAmount }}"
totalAmount: "{{ AccountingTransaction.TotalAmount }}"
remainingBalance: "{{ AccountingTransaction.TotalAmount - totalPaymentAmount }}"
# Paid - fully paid
- name: "Paid"
stage: "Completed"
isFinal: true
onEnter:
# Send payment confirmation to customer
- task: "Email/Send@1"
name: "sendPaymentConfirmation"
inputs:
to: "{{ AccountingTransaction.Customer.Email }}"
templateWorkflowId: "payment-confirmation-template"
templateVariables:
invoiceNumber: "{{ AccountingTransaction.TransactionNumber }}"
paidAmount: "{{ AccountingTransaction.TotalAmount }}"
paymentDate: "{{ now() }}"
# Create receipt
- task: "AccountingTransaction/CreateReceipt@1"
name: "createReceipt"
inputs:
transactionId: "{{ AccountingTransaction.TransactionId }}"
customerId: "{{ AccountingTransaction.CustomerId }}"
# Log completion
- task: "ActionEvent/Create@1"
name: "logInvoicePaid"
inputs:
action:
eventName: "invoice.paid"
eventData:
transactionId: "{{ AccountingTransaction.TransactionId }}"
invoiceNumber: "{{ AccountingTransaction.TransactionNumber }}"
totalAmount: "{{ AccountingTransaction.TotalAmount }}"
# Overdue - payment past due date
- name: "Overdue"
stage: "InProgress"
onEnter:
# Send overdue notice
- task: "Email/Send@1"
name: "sendOverdueNotice"
inputs:
to: "{{ AccountingTransaction.Customer.Email }}"
templateWorkflowId: "invoice-overdue-template"
templateVariables:
invoiceNumber: "{{ AccountingTransaction.TransactionNumber }}"
totalAmount: "{{ AccountingTransaction.TotalAmount }}"
dueDate: "{{ AccountingTransaction.DueDate }}"
daysOverdue: "{{ daysBetween(AccountingTransaction.DueDate, now()) }}"
# Void - cancelled invoice
- name: "Void"
stage: "Completed"
isFinal: true
requireConfirmation: true
# Transition rules
transitions:
# === Draft to Review ===
# Draft → Pending Review (submit for approval)
- name: "draft_to_pendingReview"
from: "Draft"
to: "Pending Review"
trigger: "manual"
displayName: "Submit for Review"
priority: 50
conditions:
- expression: "[hasValidLineItems] = true"
message: "Invoice must have valid line items with amounts and descriptions"
- expression: "[AccountingTransaction.CustomerId] <> null"
message: "Invoice must have a customer assigned"
- expression: "[AccountingTransaction.TotalAmount] > 0"
message: "Invoice total must be greater than zero"
# === Review to Posted ===
# Pending Review → Posted (approve and post)
- name: "pendingReview_to_posted"
from: "Pending Review"
to: "Posted"
trigger: "manual"
displayName: "Approve and Post Invoice"
priority: 50
conditions:
- expression: "[currentUser.hasPermission('invoice.approve')]"
message: "You do not have permission to approve invoices"
- expression: "[AccountingTransaction.DueDate] <> null"
message: "Invoice must have a due date"
# Draft → Posted (direct posting for authorized users)
- name: "draft_to_posted"
from: "Draft"
to: "Posted"
trigger: "manual"
displayName: "Post Invoice"
priority: 60
conditions:
- expression: "[currentUser.hasPermission('invoice.post.direct')]"
message: "You need approval permission to post directly"
- expression: "[hasValidLineItems] = true"
message: "Invoice must have valid line items"
- expression: "[AccountingTransaction.TotalAmount] > 0"
message: "Invoice total must be greater than zero"
# === Posted State Transitions ===
# Posted → Sent (delivery confirmed)
- name: "posted_to_sent"
from: "Posted"
to: "Sent"
trigger: "auto"
priority: 50
conditions:
- expression: "[AccountingTransaction.DeliveryConfirmedDate] <> null"
# Posted/Sent → Overdue (past due date)
- name: "activeInvoice_to_overdue"
from: ["Posted", "Sent"]
to: "Overdue"
trigger: "auto"
priority: 50
conditions:
- expression: "[AccountingTransaction.DueDate] < now()"
- expression: "[hasAnyPayment] = false"
# === Payment Transitions ===
# Posted/Sent/Overdue → Partially Paid (first payment received)
- name: "activeInvoice_to_partiallyPaid"
from: ["Posted", "Sent", "Overdue"]
to: "Partially Paid"
trigger: "auto"
priority: 50
conditions:
- expression: "[hasAnyPayment] = true"
- expression: "[isFullyPaid] = false"
# Any unpaid state → Paid (full payment received)
- name: "unpaidInvoice_to_paid"
from: ["Posted", "Sent", "Overdue", "Partially Paid"]
to: "Paid"
trigger: "auto"
priority: 60 # Higher priority than partial payment
conditions:
- expression: "[isFullyPaid] = true"
# Partially Paid → Overdue (past due with outstanding balance)
- name: "partiallyPaid_to_overdue"
from: "Partially Paid"
to: "Overdue"
trigger: "auto"
priority: 40
conditions:
- expression: "[AccountingTransaction.DueDate] < now()"
- expression: "[isFullyPaid] = false"
# === Manual Void Transitions ===
# Draft/Pending Review → Void (cancel before posting)
- name: "unposted_to_void"
from: ["Draft", "Pending Review"]
to: "Void"
trigger: "manual"
displayName: "Cancel Invoice"
priority: 100
requireConfirmation: true
# Posted/Sent → Void (void posted invoice without payments)
- name: "posted_to_void"
from: ["Posted", "Sent", "Overdue"]
to: "Void"
trigger: "manual"
displayName: "Void Invoice"
priority: 100
requireConfirmation: true
conditions:
- expression: "[hasAnyPayment] = false"
message: "Cannot void invoice with applied payments. Reverse payments first."
- expression: "[currentUser.hasPermission('invoice.void')]"
message: "You do not have permission to void invoices"
# Triggers - when to re-evaluate the flow
triggers:
# Monitor AccountingTransaction changes
- type: "Entity"
entityName: "AccountingTransaction"
eventType: "Modified"
position: "After"
fields: ["status", "totalAmount", "dueDate", "deliveryConfirmedDate"]
# Monitor Payment additions
- type: "Entity"
entityName: "Payment"
eventType: "Added"
position: "After"
conditions:
- expression: "[entity.TransactionId] <> null"
# Monitor Payment status changes
- type: "Entity"
entityName: "Payment"
eventType: "Modified"
position: "After"
fields: ["status", "amount"]
conditions:
- expression: "[entity.TransactionId] <> null"
# Lifecycle events
events:
- type: "onStateEntered"
state: "*"
steps:
- task: "ActionEvent/Create@1"
inputs:
action:
eventName: "invoice.status.changed"
eventData:
transactionId: "{{ AccountingTransaction.TransactionId }}"
invoiceNumber: "{{ AccountingTransaction.TransactionNumber }}"
customerId: "{{ AccountingTransaction.CustomerId }}"
fromStatus: "{{ previousState.StatusName }}"
toStatus: "{{ currentState.StatusName }}"
transitionName: "{{ transition.name }}"
- type: "onTransitionBlocked"
steps:
- task: "Utilities/Log@1"
inputs:
level: "Warning"
message: "Invoice transition blocked - {{ transition.name }} for {{ AccountingTransaction.TransactionNumber }}: {{ condition.message }}"
Example 3: Commodity Status Tracking Flow
This example tracks individual commodity (freight item) status through the supply chain with exception handling and proof of delivery:
workflow:
name: "Commodity Status Tracking Flow"
workflowId: "d4e5f6a7-b8c9-0123-def0-456789012345"
isActive: true
workflowType: "Flow"
executionMode: "Sync"
version: "1.0"
tags:
- "flow"
- "commodity"
- "tracking"
- "logistics"
entity:
name: "Commodity"
includes:
- "commodityStatus"
- "order.customer"
- "order.orderStatus"
- "commodityEvents.trackingEvent.eventDefinition"
- "commodityDocuments"
- "warehouse"
- "deliveryLocation"
variables:
- name: "orderId"
value: "{{ Commodity.OrderId }}"
- name: "commodityNumber"
value: "{{ Commodity.CommodityNumber }}"
- name: "customerEmail"
value: "{{ Commodity.Order.Customer.Email }}"
# Aggregations for tracking and validation
aggregations:
- name: "hasTrackingEvent"
parameter: "eventCode"
expression: "any([Commodity.CommodityEvents], [each.TrackingEvent.EventDefinition.EventCode] = [eventCode])"
- name: "hasProofOfDelivery"
expression: "any([Commodity.CommodityDocuments], [each.DocumentType] = 'ProofOfDelivery' && [each.Status] = 'Verified')"
- name: "hasException"
expression: "any([Commodity.CommodityEvents], [each.TrackingEvent.EventDefinition.IsException] = true)"
- name: "daysSinceLastUpdate"
expression: "daysBetween(last([Commodity.CommodityEvents], [item.TrackingEvent.EventDate]), now())"
states:
# Pending stage - commodity booked but not yet moving
- name: "Pending"
stage: "Pending"
- name: "Booked"
parent: "Pending"
isInitial: true
- name: "Ready for Pickup"
parent: "Pending"
# InProgress stage - commodity in transit
- name: "InProgress"
stage: "InProgress"
- name: "Picked Up"
parent: "InProgress"
onEnter:
- task: "Email/Send@1"
name: "notifyPickup"
inputs:
to: "{{ customerEmail }}"
templateWorkflowId: "commodity-pickup-notification"
templateVariables:
commodityNumber: "{{ Commodity.CommodityNumber }}"
orderNumber: "{{ Commodity.Order.OrderNumber }}"
pickupDate: "{{ now() }}"
- name: "In Transit"
parent: "InProgress"
- name: "At Hub"
parent: "InProgress"
- name: "Out for Delivery"
parent: "InProgress"
onEnter:
- task: "Email/Send@1"
name: "notifyOutForDelivery"
inputs:
to: "{{ customerEmail }}"
templateWorkflowId: "commodity-out-for-delivery-notification"
templateVariables:
commodityNumber: "{{ Commodity.CommodityNumber }}"
estimatedDeliveryDate: "{{ Commodity.EstimatedDeliveryDate }}"
deliveryAddress: "{{ Commodity.DeliveryLocation.FormattedAddress }}"
# Completed stage - final states
- name: "Completed"
stage: "Completed"
- name: "Delivered"
parent: "Completed"
onEnter:
# Send delivery confirmation
- task: "Email/Send@1"
name: "sendDeliveryConfirmation"
inputs:
to: "{{ customerEmail }}"
templateWorkflowId: "commodity-delivered-notification"
templateVariables:
commodityNumber: "{{ Commodity.CommodityNumber }}"
deliveryDate: "{{ now() }}"
signedBy: "{{ Commodity.ReceivedBy }}"
# Create action event
- task: "ActionEvent/Create@1"
name: "logDelivery"
inputs:
action:
eventName: "commodity.delivered"
eventData:
commodityId: "{{ Commodity.CommodityId }}"
orderId: "{{ Commodity.OrderId }}"
deliveryDate: "{{ now() }}"
- name: "Delivered with POD"
parent: "Completed"
isFinal: true
- name: "Returned to Sender"
parent: "Completed"
isFinal: true
onEnter:
- task: "Email/Send@1"
name: "notifyReturn"
inputs:
to: "{{ customerEmail }}"
templateWorkflowId: "commodity-returned-notification"
templateVariables:
commodityNumber: "{{ Commodity.CommodityNumber }}"
returnReason: "{{ Commodity.ReturnReason }}"
# Exception states
- name: "Exception"
stage: "InProgress"
- name: "Delayed"
parent: "Exception"
onEnter:
- task: "Email/Send@1"
name: "notifyDelay"
inputs:
to: "{{ customerEmail }}"
templateWorkflowId: "commodity-delayed-notification"
templateVariables:
commodityNumber: "{{ Commodity.CommodityNumber }}"
delayReason: "{{ Commodity.ExceptionReason }}"
newEstimatedDate: "{{ Commodity.EstimatedDeliveryDate }}"
- name: "Damaged"
parent: "Exception"
requireConfirmation: true
onEnter:
# Notify customer of damage
- task: "Email/Send@1"
name: "notifyDamage"
inputs:
to: "{{ customerEmail }}"
cc: "{{ Commodity.Order.Customer.AccountManagerEmail }}"
templateWorkflowId: "commodity-damaged-notification"
templateVariables:
commodityNumber: "{{ Commodity.CommodityNumber }}"
damageDescription: "{{ Commodity.ExceptionReason }}"
# Create incident ticket
- task: "Incident/Create@1"
name: "createDamageIncident"
inputs:
title: "Commodity Damaged: {{ Commodity.CommodityNumber }}"
description: "{{ Commodity.ExceptionReason }}"
priority: "High"
category: "Damage"
relatedEntityType: "Commodity"
relatedEntityId: "{{ Commodity.CommodityId }}"
- name: "Lost"
parent: "Exception"
requireConfirmation: true
onEnter:
# Notify customer and create urgent incident
- task: "Email/Send@1"
name: "notifyLost"
inputs:
to: "{{ customerEmail }}"
cc: "{{ Commodity.Order.Customer.AccountManagerEmail }}"
templateWorkflowId: "commodity-lost-notification"
templateVariables:
commodityNumber: "{{ Commodity.CommodityNumber }}"
lastKnownLocation: "{{ Commodity.LastKnownLocation }}"
- task: "Incident/Create@1"
name: "createLostIncident"
inputs:
title: "Commodity Lost: {{ Commodity.CommodityNumber }}"
description: "Commodity lost at {{ Commodity.LastKnownLocation }}"
priority: "Urgent"
category: "Lost"
relatedEntityType: "Commodity"
relatedEntityId: "{{ Commodity.CommodityId }}"
# Transition rules
transitions:
# === Pending to InProgress ===
# Booked → Ready for Pickup (when commodity is prepared)
- name: "booked_to_readyForPickup"
from: "Booked"
to: "Ready for Pickup"
trigger: "auto"
priority: 50
conditions:
- expression: "[Commodity.IsReadyForPickup] = true"
# Ready for Pickup → Picked Up (pickup confirmed)
- name: "readyForPickup_to_pickedUp"
from: "Ready for Pickup"
to: "Picked Up"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('PICKUP') = true"
# Booked → Picked Up (direct pickup without ready state)
- name: "booked_to_pickedUp"
from: "Booked"
to: "Picked Up"
trigger: "auto"
priority: 60
conditions:
- expression: "hasTrackingEvent('PICKUP') = true"
# === InProgress Transitions ===
# Picked Up → In Transit (commodity leaves origin)
- name: "pickedUp_to_inTransit"
from: "Picked Up"
to: "In Transit"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('DEPART_ORIGIN') = true"
# In Transit → At Hub (arrives at distribution center)
- name: "inTransit_to_atHub"
from: "In Transit"
to: "At Hub"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('ARRIVE_HUB') = true"
# At Hub → In Transit (leaves hub for next leg)
- name: "atHub_to_inTransit"
from: "At Hub"
to: "In Transit"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('DEPART_HUB') = true"
# In Transit → Out for Delivery (final mile delivery)
- name: "inTransit_to_outForDelivery"
from: "In Transit"
to: "Out for Delivery"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('OUT_FOR_DELIVERY') = true"
# === Delivery Transitions ===
# Out for Delivery → Delivered (successful delivery)
- name: "outForDelivery_to_delivered"
from: "Out for Delivery"
to: "Delivered"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('DELIVERED') = true"
- expression: "[Commodity.DeliveryDate] <> null"
# Delivered → Delivered with POD (proof of delivery uploaded)
- name: "delivered_to_deliveredWithPOD"
from: "Delivered"
to: "Delivered with POD"
trigger: "auto"
priority: 50
conditions:
- expression: "[hasProofOfDelivery] = true"
# === Exception Transitions ===
# Any InProgress state → Delayed (tracking shows delay)
- name: "inProgress_to_delayed"
from: "InProgress"
to: "Delayed"
trigger: "auto"
priority: 60
conditions:
- expression: "hasTrackingEvent('DELAYED') = true"
# Delayed → In Transit (delay resolved)
- name: "delayed_to_inTransit"
from: "Delayed"
to: "In Transit"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('DELAY_RESOLVED') = true"
# Any state → Damaged (damage reported)
- name: "any_to_damaged"
from: ["In Transit", "At Hub", "Out for Delivery", "Delivered"]
to: "Damaged"
trigger: "manual"
displayName: "Report Damage"
priority: 100
requireConfirmation: true
conditions:
- expression: "[Commodity.ExceptionReason] <> null"
message: "Please provide damage description"
- expression: "[currentUser.hasPermission('commodity.reportException')]"
message: "You do not have permission to report exceptions"
# Any InProgress state → Lost (commodity cannot be located)
- name: "inProgress_to_lost"
from: "InProgress"
to: "Lost"
trigger: "manual"
displayName: "Report Lost"
priority: 100
requireConfirmation: true
conditions:
- expression: "[daysSinceLastUpdate] > 7"
message: "Commodity must be without updates for 7+ days before marking as lost"
- expression: "[currentUser.hasPermission('commodity.reportLost')]"
message: "You do not have permission to report lost commodities"
# === Return Transitions ===
# Out for Delivery → Returned to Sender (delivery refused/failed)
- name: "outForDelivery_to_returned"
from: "Out for Delivery"
to: "Returned to Sender"
trigger: "auto"
priority: 50
conditions:
- expression: "hasTrackingEvent('DELIVERY_REFUSED') = true OR hasTrackingEvent('RETURN_TO_SENDER') = true"
# Damaged → Returned to Sender (return after damage)
- name: "damaged_to_returned"
from: "Damaged"
to: "Returned to Sender"
trigger: "manual"
displayName: "Return Damaged Item"
priority: 50
# === Stale Commodity Detection ===
# Any InProgress → Delayed (automatic detection of stale tracking)
- name: "staleTracking_to_delayed"
from: ["In Transit", "At Hub"]
to: "Delayed"
trigger: "auto"
priority: 40
conditions:
- expression: "[daysSinceLastUpdate] > 3"
- expression: "[Commodity.StatusStage] <> 'Completed'"
# Triggers - when to re-evaluate the flow
triggers:
# Monitor Commodity field changes
- type: "Entity"
entityName: "Commodity"
eventType: "Modified"
position: "After"
fields:
[
"commodityStatusId",
"isReadyForPickup",
"deliveryDate",
"exceptionReason",
]
# Monitor TrackingEvent additions
- type: "Entity"
entityName: "TrackingEvent"
eventType: "Added"
position: "After"
conditions:
- expression: "[entity.CommodityId] <> null"
# Monitor CommodityDocument additions (POD uploads)
- type: "Entity"
entityName: "CommodityDocument"
eventType: "Added"
position: "After"
conditions:
- expression: "[entity.DocumentType] = 'ProofOfDelivery'"
# Monitor CommodityDocument status changes
- type: "Entity"
entityName: "CommodityDocument"
eventType: "Modified"
position: "After"
fields: ["status"]
conditions:
- expression: "[entity.DocumentType] = 'ProofOfDelivery'"
# Lifecycle events
events:
- type: "onStateEntered"
state: "*"
steps:
- task: "ActionEvent/Create@1"
inputs:
action:
eventName: "commodity.status.changed"
eventData:
commodityId: "{{ Commodity.CommodityId }}"
commodityNumber: "{{ Commodity.CommodityNumber }}"
orderId: "{{ Commodity.OrderId }}"
fromStatus: "{{ previousState.StatusName }}"
toStatus: "{{ currentState.StatusName }}"
transitionName: "{{ transition.name }}"
# When commodity is delivered, attempt to transition parent order to Delivered
- type: "onStateEntered"
state: "Delivered"
steps:
- task: "Order/Transition@1"
name: "triggerOrderDelivery"
inputs:
orderId: "{{ Commodity.OrderId }}"
transitionName: "outForDelivery_to_delivered"
continueOnError: true
- type: "onTransitionBlocked"
steps:
- task: "Utilities/Log@1"
inputs:
level: "Warning"
message: "Commodity transition blocked - {{ transition.name }} for {{ Commodity.CommodityNumber }}: {{ condition.message }}"
Hierarchical States
Flow workflows support hierarchical (nested) states where child states inherit from parent states.
How Hierarchical States Work
- State Inheritance: When an entity is in a child state, it is also considered to be in all ancestor states
- Transition Matching: Transitions from a parent state apply to all child states
- Priority: More specific (child) transitions take priority over parent transitions
Example: InProgress Parent State
states:
- name: "InProgress"
stage: "InProgress"
- name: "Picked Up"
parent: "InProgress"
- name: "In Transit"
parent: "InProgress"
- name: "Out for Delivery"
parent: "InProgress"
transitions:
# This transition applies to ALL InProgress child states
- name: "cancel_inProgress"
from: "InProgress"
to: "Cancelled"
trigger: "manual"
displayName: "Cancel Order"
priority: 10 # Low priority - child transitions checked first
When an order is in "Picked Up" state:
- It matches transitions from "Picked Up" (checked first due to specificity)
- It also matches transitions from "InProgress" (checked second)
- Priority determines which transition executes when multiple match
Transition Priority Resolution
When multiple transitions are valid (conditions met), the flow uses priority-based resolution:
- Higher priority numbers are evaluated first
- More specific states (child) take precedence over parent states at the same priority
- YAML document order breaks ties — at the same priority and specificity level, the transition defined first in the YAML file wins
- Only one transition executes per evaluation cycle (cascading may trigger subsequent transitions)
isFinalstates are excluded — entities in a state withisFinal: trueare never matched by any outgoing transition, includingfrom: "*"wildcards
transitions:
# Priority 100 - checked first
- name: "emergency_cancel"
from: "*"
to: "Cancelled"
trigger: "event"
eventName: "order.emergency.cancel"
priority: 100
# Priority 60 - checked second
- name: "delivered_to_invoiced"
from: "Delivered"
to: "Invoiced"
trigger: "auto"
priority: 60
# Priority 50 - checked third
- name: "outForDelivery_to_delivered"
from: "Out for Delivery"
to: "Delivered"
trigger: "auto"
priority: 50
Validation Rules
The Flow engine validates workflow definitions at deployment time and rejects invalid configurations:
| Rule | Description |
|---|---|
| Leaf-state targets | transitions[].to must reference a leaf state (a state with no children). Targeting a parent state is a validation error. |
| isFinal enforcement | States with isFinal: true must not appear as from in any transition (including via wildcards). |
| Unique transition names | All transitions[].name values must be unique within the workflow. |
| Valid state references | All state names referenced in from, to, and parent must exist in the states[] array. |
| Single initial state | Exactly one state must have isInitial: true (or one per parent group if using hierarchical initial states). |
| Active uniqueness | Only one active Flow (isActive: true) per entity.name + entity.type combination. |
| No self-transitions | A transition's from and to must not reference the same state. |
Best Practices
State Design
- Use meaningful state names that match your
OrderStatusentity records - Leverage hierarchical states to reduce transition duplication
- Mark terminal states with
isFinal: trueto prevent unexpected transitions - Use
requireConfirmationfor critical state transitions that need user acknowledgment - Always target leaf states in transitions — parent states are for grouping and inheritance only
Transition Design
- Keep conditions simple - complex logic should be in aggregations
- Use condition messages for manual transitions with clear, actionable error messages
- Assign appropriate priorities - higher for manual/emergency, lower for automatic
- Document transition intent with meaningful names and descriptions
- Mind YAML order when transitions share the same priority — define more important ones first
- Use
continueOnError: trueon notification steps (emails, webhooks) to prevent them from blocking subsequent steps
Data Sources & Aggregations
- Define reusable aggregations for common conditions
- Use parameterized aggregations for flexible event checking
- Consumed events are automatic —
any()andall()exclude previously consumed events, so your expressions don't need to handle this
Data Loading Optimization
- Use entity-level
queryto define default fields loaded across all states - Use state-level
queryto optimize data loading per workflow phase (e.g., load tracking data only in "In Transit" states) - Omit
queryto use system defaults when fine-grained control isn't needed - Balance performance vs. completeness — load only what's needed for conditions and steps in each state
Trigger Design
- Be specific with field monitoring - use
fields[]to avoid unnecessary evaluations - Add trigger conditions to filter irrelevant entity changes
- Use separate scheduled workflows for time-based conditions — entity triggers alone cannot detect overdue dates or stale records; create a scheduled workflow with
Order/Transition@1to re-evaluate flows on a cron schedule - Consider cascade depth — if your flow has long chains of auto-transitions, increase
maxCascadeDepthaccordingly