Workflow: Flow Type
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
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:
| Function | Description | Example |
|---|---|---|
All | True if all items match the aggregation expression | All commodities delivered |
Any | True if any item matches the aggregation expression | Has tracking event |
None | True if no items match the aggregation expression | No charges are void |
Triggers
Triggers define when the flow should re-evaluate conditions:
- Entity triggers: Evaluate when Order or related entities change
- Event triggers: Evaluate when specific domain events occur
- Schedule triggers: Periodic evaluation (for time-based transitions)
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"
tags:
- "flow"
- "orderStatus"
# Entity configuration (what to manage and what data to load)
entity:
name: "Order"
includes: # Related data to load for condition evaluation
- "orderCommodities.commodity.commodityStatus"
- "orderEvents.trackingEvent.eventDefinition"
- "orderCharges.charge"
- "accountingTransactions"
# 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.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.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.includes[] | array | No | Related data to load (navigation paths). If omitted, loads common relations. | ["orderCommodities.commodity"] |
Active Flow Uniqueness (Per Entity)
For Flow workflows, CargoXplorer supports only one active Flow workflow per entity at a time.
- Rule: At most one Flow workflow with
workflow.isActive: truecan target a givenentity.name.- Example: Only one active Flow for
entity.name: "Order". - You may still have another active Flow for
entity.name: "Commodity"at the same time.
- Example: Only one active Flow for
- Why: Multiple active Flows targeting the same entity would compete to transition the same record, resulting in ambiguous and potentially conflicting state changes.
If you need different behavior for a subset of records, implement that variation inside the same Flow using conditions (e.g., OrderType, custom fields) rather than creating multiple active Flows for the same entity.name.
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.
Aggregations
Aggregations define reusable, named 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[].source | string | Yes | Path to collection on the entity | "[Order.OrderCommodities]" |
aggregations[].function | string | Yes | Aggregation function. Common: All, Any, None, Sum, Custom | "All" |
aggregations[].expression | string | Yes | NCalc expression to evaluate for each item | "[each.Commodity.CommodityStatus.StatusStage] = 'Completed'" |
aggregations[].parameter | string | No | Dynamic parameter name for parameterized aggregations | "eventCode" |
aggregations[].filter | string | No | Optional NCalc filter expression to pre-filter items in source | "[each.Status] = 'Applied'" |
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
- name: "allCommoditiesDelivered"
source: "[Order.OrderCommodities]"
function: "All"
expression: "[each.Commodity.CommodityStatus.StatusStage] = 'Completed'"
# Parameterized aggregation - reusable with different event codes
- name: "hasTrackingEvent"
source: "[Order.OrderEvents]"
function: "Any"
parameter: "eventCode"
expression: "[each.TrackingEvent.EventDefinition.EventCode] = [eventCode]"
# Used in transitions as:
# - expression: "[allCommoditiesDelivered] = true"
# - expression: "hasTrackingEvent('DEPART_DC') = true"
# - expression: "hasTrackingEvent('ARRIVE_PP') = true"
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 | Whether this is a final state (no outgoing transitions) | true |
states[].requireConfirmation | boolean | No | Whether transitions to this state require user confirmation | false |
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. Execute FROM state's onExit steps
3. Update entity status (backend-defined status field for the entity)
4. Execute transition steps
5. Execute TO state's onEnter steps
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 |
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 | "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 (backend-defined status field for the entity)
3. Execute transition steps (sequentially)
4. Fire onStateEntered events
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.
| Attribute | Type | Required | Description | Example |
|---|---|---|---|---|
triggers[].type | string | Yes | Trigger type: Entity, Schedule, Event | "Entity" |
triggers[].entityName | string | Conditional | Entity name for Entity triggers | "TrackingEvent" |
triggers[].eventType | string | Conditional | Event type: Added, Modified, Deleted | "Added" |
triggers[].position | string | No | Position: Before, After. Default: After | "After" |
triggers[].fields[] | array | No | Specific fields to monitor for Modified events | ["commodityStatusId"] |
triggers[].conditions[] | array | No | Conditions for trigger activation | [...] |
triggers[].schedule | string | Conditional | Cron expression for Schedule triggers | "0 9 * * *" |
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"
tags:
- "flow"
- "parcel"
- "delivery"
- "returns"
entity:
name: "Order"
# 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"
source: "[Order.OrderCommodities]"
function: "All"
expression: "[each.Commodity.CommodityStatus.StatusStage] = 'Completed'"
- name: "hasTrackingEvent"
source: "[Order.OrderEvents]"
function: "Any"
parameter: "eventCode"
expression: "[each.TrackingEvent.EventDefinition.EventCode] = [eventCode]"
- name: "allChargesPaid"
source: "[Order.OrderCharges]"
function: "All"
expression: "[each.Charge.ChargeStatus] = 'Paid' || [each.Charge.ChargeStatus] = 'Void'"
- name: "hasPostedInvoice"
source: "[Order.AccountingTransactions]"
function: "Any"
expression: "[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"
source: "[AccountingTransaction.Payments]"
function: "Sum"
expression: "[each.Amount]"
filter: "[each.Status] = 'Applied'"
- name: "hasAnyPayment"
source: "[AccountingTransaction.Payments]"
function: "Any"
expression: "[each.Status] = 'Applied' && [each.Amount] > 0"
- name: "isFullyPaid"
source: "[AccountingTransaction.Payments]"
function: "Custom"
expression: "[totalPaymentAmount] >= [AccountingTransaction.TotalAmount]"
- name: "hasValidLineItems"
source: "[AccountingTransaction.TransactionLines]"
function: "All"
expression: "[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: "{{ Transaction.TransactionId }}"
invoiceNumber: "{{ Transaction.TransactionNumber }}"
totalAmount: "{{ Transaction.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"
# Schedule trigger to check for overdue invoices daily
- type: "Schedule"
schedule: "0 9 * * *" # Daily at 9 AM
conditions:
- expression: "[AccountingTransaction.Status] = 'Posted' || [AccountingTransaction.Status] = 'Sent' || [AccountingTransaction.Status] = 'Partially Paid'"
# 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"
source: "[Commodity.CommodityEvents]"
function: "Any"
parameter: "eventCode"
expression: "[each.TrackingEvent.EventDefinition.EventCode] = [eventCode]"
- name: "hasProofOfDelivery"
source: "[Commodity.CommodityDocuments]"
function: "Any"
expression: "[each.DocumentType] = 'ProofOfDelivery' && [each.Status] = 'Verified'"
- name: "hasException"
source: "[Commodity.CommodityEvents]"
function: "Any"
expression: "[each.TrackingEvent.EventDefinition.IsException] = true"
- name: "daysSinceLastUpdate"
source: "[Commodity.CommodityEvents]"
function: "Custom"
expression: "daysBetween(Max([each.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'"
# Schedule trigger to detect stale commodities
- type: "Schedule"
schedule: "0 */6 * * *" # Every 6 hours
conditions:
- expression: "[Commodity.StatusStage] = 'InProgress'"
# 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 }}"
# Sync commodity status changes to parent order
- type: "onStateEntered"
state: "*"
steps:
- task: "Workflow/Trigger@1"
name: "triggerOrderFlowEvaluation"
inputs:
entityType: "Order"
entityId: "{{ Commodity.OrderId }}"
reason: "Commodity status changed"
- 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
- First matching transition at the highest priority level executes
- Only one transition executes per evaluation cycle
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
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
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
- Keep one active Flow per entity - if you need variants, implement them with conditions within a single Flow
Data Sources & Aggregations
- Define reusable aggregations for common conditions
- Use parameterized aggregations for flexible event checking
- Filter data sources to improve performance with large collections
Trigger Design
- Be specific with field monitoring - use
fields[]to avoid unnecessary evaluations - Add trigger conditions to filter irrelevant entity changes
- Consider performance - limit triggers to essential entity modifications