Skip to main content

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 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:

FunctionDescriptionExample
AllTrue if all items match the aggregation expressionAll commodities delivered
AnyTrue if any item matches the aggregation expressionHas tracking event
NoneTrue if no items match the aggregation expressionNo 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

AttributeTypeRequiredDescriptionExample
workflow.workflowIdstringYesUnique identifier (UUID format)"a1b2c3d4-..."
workflow.namestringYesDisplay name of the flow"Order Status State Machine"
workflow.workflowTypestringYesMust be "Flow""Flow"
workflow.executionModestringYesShould be "Sync" for immediate transitions"Sync"
workflow.versionstringYesVersion of the workflow definition"1.0"
workflow.tags[]arrayRecommendedTags for categorization["stateMachine", "order"]

Entity Configuration

AttributeTypeRequiredDescriptionExample
entity.namestringYesEntity type this flow manages"Order"
entity.includes[]arrayNoRelated 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: true can target a given entity.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.
  • 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
AttributeTypeRequiredDescriptionExample
aggregations[].namestringYesUnique identifier for this aggregation"allCommoditiesDelivered"
aggregations[].sourcestringYesPath to collection on the entity"[Order.OrderCommodities]"
aggregations[].functionstringYesAggregation function. Common: All, Any, None, Sum, Custom"All"
aggregations[].expressionstringYesNCalc expression to evaluate for each item"[each.Commodity.CommodityStatus.StatusStage] = 'Completed'"
aggregations[].parameterstringNoDynamic parameter name for parameterized aggregations"eventCode"
aggregations[].filterstringNoOptional 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)
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
- 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.

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[].isFinalbooleanNoWhether this is a final state (no outgoing transitions)true
states[].requireConfirmationbooleanNoWhether transitions to this state require user confirmationfalse
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. 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 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

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"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 (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.

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.

AttributeTypeRequiredDescriptionExample
triggers[].typestringYesTrigger type: Entity, Schedule, Event"Entity"
triggers[].entityNamestringConditionalEntity name for Entity triggers"TrackingEvent"
triggers[].eventTypestringConditionalEvent type: Added, Modified, Deleted"Added"
triggers[].positionstringNoPosition: Before, After. Default: After"After"
triggers[].fields[]arrayNoSpecific fields to monitor for Modified events["commodityStatusId"]
triggers[].conditions[]arrayNoConditions for trigger activation[...]
triggers[].schedulestringConditionalCron expression for Schedule triggers"0 9 * * *"

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"
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

  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. First matching transition at the highest priority level executes
  4. 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 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

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