Skip to main content

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: 5 cascading 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 continueOnError is false (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: true on 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 requireConfirmation per 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, or event
  • 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:

FunctionDescriptionExample
all()True if all items match the conditionall([Order.OrderCommodities], [each.Status] = 'Delivered')
any()True if any item matches the conditionany([Order.OrderEvents], [each.EventCode] = 'PICKUP')
sum()Sum of numeric valuessum([Order.OrderCharges], [each.Amount])
count()Number of items in collectioncount([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) or After (post-save, default)
  • Field filtering: Optionally limit Modified triggers 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

AttributeTypeRequiredDescriptionExample
workflow.workflowIdstringYesUnique identifier (UUID format)"a1b2c3d4-..."
workflow.namestringYesDisplay name of the flow"Order Status State Machine"
workflow.isActivebooleanYesWhether the flow is active. Only one active Flow per entity+type is allowed.true
workflow.workflowTypestringYesMust be "Flow""Flow"
workflow.executionModestringYesShould be "Sync" for immediate transitions"Sync"
workflow.versionstringYesVersion of the workflow definition"1.0"
workflow.maxCascadeDepthnumberNoMax cascading transitions per trigger. Default: 510
workflow.tags[]arrayRecommendedTags for categorization["stateMachine", "order"]

Entity Configuration

AttributeTypeRequiredDescriptionExample
entity.namestringYesEntity type this flow manages"Order"
entity.typestringConditionalEntity subtype filter. Required for Order (e.g., "ParcelShipment") and AccountingTransaction flows."ParcelShipment"
entity.includes[]arrayNoRelated data to load (navigation paths). If omitted, loads common relations.["orderCommodities.commodity"]
entity.querystringNoGraphQL-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: true can target a given entity.name + entity.type combination.
    • Example: One active Flow for entity.name: "Order" with entity.type: "ParcelShipment", AND a separate active Flow for entity.name: "Order" with entity.type: "AirShipmentOrder".
    • You may still have another active Flow for entity.name: "Commodity" at the same time.
  • 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 per entity.name only.

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
AttributeTypeRequiredDescriptionExample
aggregations[].namestringYesUnique identifier for this aggregation"allCommoditiesDelivered"
aggregations[].expressionstringYesNCalc expression using aggregation functions"all([Order.OrderCommodities], [each.Commodity.CommodityStatus.StatusStage] = 'Completed')"
aggregations[].parameterstringNoDynamic parameter name for parameterized aggregations"eventCode"

NCalc Aggregation Functions

The expression engine supports these built-in aggregation functions:

FunctionDescriptionExample
all(collection, condition)True if all items match the conditionall([Order.OrderCommodities], [each.Status] = 'Delivered')
any(collection, condition)True if any item matches the conditionany([Order.OrderEvents], [each.EventCode] = 'PICKUP')
sum(collection, expression)Sum of numeric values from expressionsum([Order.OrderCharges], [each.Amount])
count(collection)Number of items in collectioncount([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 collectiondistinct([Order.OrderCommodities], [each.WarehouseId])
groupBy(collection, keyExpression)Group items by keygroupBy([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)
AttributeTypeRequiredDescriptionExample
variables[].namestringYesVariable name (unique within the workflow)"customerId"
variables[].valuestringYesTemplate/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.

AttributeTypeRequiredDescriptionExample
states[].namestringYesState name (references OrderStatus)"Scheduled"
states[].stagestringNoStatus stage: Pending, InProgress, Completed"InProgress"
states[].parentstringNoParent state name for hierarchical states"InProgress"
states[].isInitialbooleanNoWhether this is an initial statetrue
states[].isFinalbooleanNoFinal state: no outgoing transitions allowed (including from: "*" wildcards)true
states[].requireConfirmationbooleanNoWhether transitions to this state require user confirmationfalse
states[].querystringNoGraphQL-style query overriding the entity's default query for this state"{ commodities, charges }"
states[].onEnterarrayNoSteps to execute when entering this state (after transition)[...]
states[].onExitarrayNoSteps 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 CaseWhere to Define
Action specific to one transitiontransitions[].steps
Action for ANY entry to a statestates[].onEnter
Action for ANY exit from a statestates[].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 PhaseQuery FocusExample
Draft/InitialCustomer and basic info"{ customer { name, contact } }"
In TransitTracking and logistics"{ commodities { tracking }, stops { eta } }"
BillingFinancial data"{ charges, invoices, payments }"
CompletedMinimal 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.

AttributeTypeRequiredDescriptionExample
transitions[].namestringYesUnique identifier for this transition"schedule_to_dcOutbound"
transitions[].fromstring or arrayYesSource state(s). Use "*" for any state"Scheduled" or ["A", "B"]
transitions[].tostringYesTarget state (must be a leaf state, not a parent)"DC Outbound"
transitions[].triggerstringYesTrigger type: auto, manual, event"auto"
transitions[].prioritynumberNoPriority (higher = checked first). Default: 50100
transitions[].displayNamestringNoUI label for manual transitions"Move to DC Outbound"
transitions[].conditions[]arrayNoConditions that must all be true for transition[...]
transitions[].steps[]arrayNoTasks to execute after the transition completes[...]
transitions[].eventNamestringNoEvent name for event trigger type"order.exception.raised"
transitions[].requireConfirmationbooleanNoRequire 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.

AttributeTypeRequiredDescription
steps[].taskstringYesTask type and version (e.g., "Email/Send@1")
steps[].namestringNoStep name for output reference
steps[].inputsobjectNoInput parameters for the task
steps[].conditionsstringNoCondition to execute this step
steps[].continueOnErrorbooleanNoContinue 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.

AttributeTypeRequiredDescription
conditions[].expressionstringYesNCalc expression that must evaluate to true
conditions[].messagestringNoError message shown to user when condition fails (for manual transitions)

Behavior based on trigger type:

Trigger TypeCondition FailsUser Experience
autoTransition silently skippedNo feedback - tries next transition
manualTransition blockedError message shown to user
eventTransition silently skippedNo 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.

AttributeTypeRequiredDescriptionExample
triggers[].typestringYesMust be "Entity" for Flow workflows"Entity"
triggers[].entityNamestringYesEntity type to monitor (e.g., Order, Commodity, TrackingEvent)"TrackingEvent"
triggers[].eventTypestringYesEvent type: Added, Modified, or Deleted"Added"
triggers[].positionstringNoWhen to fire: Before (pre-save) or After (post-save). Default: After"After"
triggers[].fields[]arrayNoSpecific fields to monitor for Modified events (improves performance)["commodityStatusId"]
triggers[].conditions[]arrayNoNCalc 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.

AttributeTypeRequiredDescription
events[].typestringYesEvent type (see below)
events[].statestringNoSpecific state or "*" for all
events[].steps[]arrayYesSteps to execute

Event Types:

  • onStateEntered: Fires after entering a state
  • onStateExited: Fires before exiting a state
  • onTransitionStarted: Fires before a transition executes
  • onTransitionCompleted: Fires after a transition completes
  • onTransitionBlocked: Fires when a condition blocks a manual transition
  • onEvaluationCompleted: 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

  1. State Inheritance: When an entity is in a child state, it is also considered to be in all ancestor states
  2. Transition Matching: Transitions from a parent state apply to all child states
  3. 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:

  1. Higher priority numbers are evaluated first
  2. More specific states (child) take precedence over parent states at the same priority
  3. YAML document order breaks ties — at the same priority and specificity level, the transition defined first in the YAML file wins
  4. Only one transition executes per evaluation cycle (cascading may trigger subsequent transitions)
  5. isFinal states are excluded — entities in a state with isFinal: true are never matched by any outgoing transition, including from: "*" 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:

RuleDescription
Leaf-state targetstransitions[].to must reference a leaf state (a state with no children). Targeting a parent state is a validation error.
isFinal enforcementStates with isFinal: true must not appear as from in any transition (including via wildcards).
Unique transition namesAll transitions[].name values must be unique within the workflow.
Valid state referencesAll state names referenced in from, to, and parent must exist in the states[] array.
Single initial stateExactly one state must have isInitial: true (or one per parent group if using hierarchical initial states).
Active uniquenessOnly one active Flow (isActive: true) per entity.name + entity.type combination.
No self-transitionsA transition's from and to must not reference the same state.

Best Practices

State Design

  • Use meaningful state names that match your OrderStatus entity records
  • Leverage hierarchical states to reduce transition duplication
  • Mark terminal states with isFinal: true to prevent unexpected transitions
  • Use requireConfirmation for 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: true on 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 automaticany() and all() exclude previously consumed events, so your expressions don't need to handle this

Data Loading Optimization

  • Use entity-level query to define default fields loaded across all states
  • Use state-level query to optimize data loading per workflow phase (e.g., load tracking data only in "In Transit" states)
  • Omit query to 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@1 to re-evaluate flows on a cron schedule
  • Consider cascade depth — if your flow has long chains of auto-transitions, increase maxCascadeDepth accordingly