Expressions in Practice
This lesson walks through real workflow scenarios that combine template expressions, value directives, and NCalc conditions. Each scenario builds on the previous, showing how these tools work together in a complete validation and notification workflow.
The Scenario
You are building a workflow that:
- Validates that all commodities have a positive weight
- Resolves a display name for the contact
- Transforms contacts into email recipients
- Maps a shipment status to a human-readable label
- Looks up a contact record by email for notification
- Combines template expressions and NCalc in a final validation condition
The workflow receives a shipment as input and produces a validated, enriched output.
Step 1 — Condition: All Commodities Have Weight
The first guard checks that every commodity in the shipment has a weight greater than zero. A step condition using NCalc's all function handles this:
yaml1activities:2 - name: ValidateShipment3 steps:4 - name: CheckCommodityWeights5 type: Core.Validation6 conditions:7 - expression: "all([Data.GetShipment.shipment?.commodities?], [each.weight] > 0)"8 message: "All commodities must have a weight greater than zero"9 inputs:10 shipmentId: "{{ inputs.shipmentId }}"
all returns false for an empty collection, so all([], ...) fails. If shipments can legitimately have zero commodities, check that first with count([...]) = 0 OR all([...], ...).
Step 2 — Coalesce: Display Name Fallback
Contacts may have a company name, a personal name, or only an email. The coalesce directive picks the first available value:
yaml1 - name: BuildNotificationContext2 inputs:3 contactDisplayName:4 coalesce:5 - "{{ Data.GetContact.contact?.companyName? }}"6 - "{{ Data.GetContact.contact?.firstName? }}"7 - "{{ Data.GetContact.contact?.email? }}"8 - "Unknown"
Each path uses ? on every segment. The last literal "Unknown" guarantees the step always receives a non-null value even if the contact record is partially empty.
Step 3 — foreach: Transform Contacts to Email Recipients
The shipment has a list of contact entries that need to be converted into a structure suitable for an email step. Only contacts that have opted in to email notifications are included:
yaml1 emailRecipients:2 foreach: "Data.GetShipment.shipment.contacts"3 item: "contact"4 conditions: "[contact.receiveEmailUpdates] = true AND isNullOrEmpty([contact.email?]) = false"5 mapping:6 to: "{{ contact.email }}"7 name:8 coalesce:9 - "{{ contact.companyName? }}"10 - "{{ contact.email }}"11 preferredLanguage: "{{ emptyIfNull contact.languageCode? }}"
The conditions filter combines two checks in one NCalc expression: the opt-in flag must be true and the email must not be empty. Items failing either check are skipped.
Step 4 — switch: Status to Human-Readable Label
Shipment statuses are stored as enum values. The switch directive maps them to labels without an intermediate transformation step:
yaml1 statusLabel:2 switch: "{{ Data.GetShipment.shipment.status }}"3 cases:4 "Pending": "Awaiting Pickup"5 "InTransit": "In Transit"6 "Arrived": "Arrived at Warehouse"7 "Delivered": "Delivered"8 "Cancelled": "Cancelled"9 default: "Status Unknown"
Matching is case-insensitive, so "intransit" and "InTransit" both match the "InTransit" case. The default ensures that any unexpected status value still produces a usable label.
Step 5 — resolve: Contact Lookup
You need the internal contact ID from the system to attach a follow-up task. The resolve directive looks it up by email without a separate query step:
yaml1 primaryContactId:2 resolve:3 entity: "Contact"4 filter: "email:{{ luceneString Data.GetShipment.shipment.contacts[0]?.email? }}"5 field: "contactId"
The luceneString type converter escapes special characters in the email before it becomes part of the Lucene filter string. If no match is found, primaryContactId resolves to null — handle that in a subsequent condition if the ID is required.
Step 6 — Combined Template + NCalc Validation Condition
The final step checks multiple business rules at once. Template expressions resolve the values; NCalc combines them:
yaml1 - name: FinalValidation2 type: Core.Condition3 conditions:4 # All weights resolved and positive5 - expression: "all([Data.GetShipment.shipment?.commodities?], [each.weight] > 0)"6 # Due date is not in the past7 - expression: >8 isNullOrEmpty([Data.GetShipment.shipment?.dueDate?]) = false AND9 parseDate([Data.GetShipment.shipment.dueDate]) > now()10 # Total weight within allowed range11 - expression: >12 sum([Data.GetShipment.shipment.commodities], [each.weight]) <=13 convertWeight(10000, 'Lb', 'Kg')
The multi-line YAML > block keeps long expressions readable. Each condition is evaluated independently — the step fails if any one is false.
Putting It All Together
Here is the condensed workflow combining all six patterns:
yaml1name: ValidateAndNotify2triggers:3 - type: Manual4 inputs:5 - name: shipmentId6 type: string78activities:9 - name: FetchData10 steps:11 - name: GetShipment12 type: Core.Query.GetEntity13 inputs:14 entity: Shipment15 filter: "shipmentId:{{ inputs.shipmentId }}"1617 - name: GetContact18 type: Core.Query.GetEntity19 inputs:20 entity: Contact21 filter: "shipmentId:{{ inputs.shipmentId }}"2223 - name: ValidateAndBuild24 steps:25 - name: BuildPayload26 conditions:27 - expression: "all([Data.GetShipment.shipment?.commodities?], [each.weight] > 0)"28 inputs:29 contactDisplayName:30 coalesce:31 - "{{ Data.GetContact.contact?.companyName? }}"32 - "{{ Data.GetContact.contact?.email? }}"33 - "Unknown"3435 emailRecipients:36 foreach: "Data.GetShipment.shipment.contacts"37 item: "contact"38 conditions: "[contact.receiveEmailUpdates] = true"39 mapping:40 to: "{{ contact.email }}"41 name: "{{ contact.companyName? }}"4243 statusLabel:44 switch: "{{ Data.GetShipment.shipment.status }}"45 cases:46 "Pending": "Awaiting Pickup"47 "InTransit": "In Transit"48 "Delivered": "Delivered"49 default: "In Progress"5051 primaryContactId:52 resolve:53 entity: "Contact"54 filter: "email:{{ luceneString Data.GetShipment.shipment.contacts[0]?.email? }}"55 field: "contactId"
Key Takeaways
- Use
?on every uncertain path segment — it prevents runtime exceptions when traversing external data. coalescefor display values — never assume which contact field is populated; let coalesce decide.foreachwithconditions— filter inline rather than adding a separate filter step.switchfor enum mapping — cleaner than a multi-branchif()NCalc expression.resolvebatches automatically — safe to use insideforeach; repeated unique lookups are deduplicated.- Combine NCalc functions —
all,sum,parseDate,convertWeight, andisNullOrEmptycompose naturally in a single expression.