10 minlesson

Expressions in Practice

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:

  1. Validates that all commodities have a positive weight
  2. Resolves a display name for the contact
  3. Transforms contacts into email recipients
  4. Maps a shipment status to a human-readable label
  5. Looks up a contact record by email for notification
  6. 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:

yaml
1activities:
2 - name: ValidateShipment
3 steps:
4 - name: CheckCommodityWeights
5 type: Core.Validation
6 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:

yaml
1 - name: BuildNotificationContext
2 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:

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

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

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

yaml
1 - name: FinalValidation
2 type: Core.Condition
3 conditions:
4 # All weights resolved and positive
5 - expression: "all([Data.GetShipment.shipment?.commodities?], [each.weight] > 0)"
6 # Due date is not in the past
7 - expression: >
8 isNullOrEmpty([Data.GetShipment.shipment?.dueDate?]) = false AND
9 parseDate([Data.GetShipment.shipment.dueDate]) > now()
10 # Total weight within allowed range
11 - 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:

yaml
1name: ValidateAndNotify
2triggers:
3 - type: Manual
4 inputs:
5 - name: shipmentId
6 type: string
7
8activities:
9 - name: FetchData
10 steps:
11 - name: GetShipment
12 type: Core.Query.GetEntity
13 inputs:
14 entity: Shipment
15 filter: "shipmentId:{{ inputs.shipmentId }}"
16
17 - name: GetContact
18 type: Core.Query.GetEntity
19 inputs:
20 entity: Contact
21 filter: "shipmentId:{{ inputs.shipmentId }}"
22
23 - name: ValidateAndBuild
24 steps:
25 - name: BuildPayload
26 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"
34
35 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? }}"
42
43 statusLabel:
44 switch: "{{ Data.GetShipment.shipment.status }}"
45 cases:
46 "Pending": "Awaiting Pickup"
47 "InTransit": "In Transit"
48 "Delivered": "Delivered"
49 default: "In Progress"
50
51 primaryContactId:
52 resolve:
53 entity: "Contact"
54 filter: "email:{{ luceneString Data.GetShipment.shipment.contacts[0]?.email? }}"
55 field: "contactId"

Key Takeaways

  1. Use ? on every uncertain path segment — it prevents runtime exceptions when traversing external data.
  2. coalesce for display values — never assume which contact field is populated; let coalesce decide.
  3. foreach with conditions — filter inline rather than adding a separate filter step.
  4. switch for enum mapping — cleaner than a multi-branch if() NCalc expression.
  5. resolve batches automatically — safe to use inside foreach; repeated unique lookups are deduplicated.
  6. Combine NCalc functionsall, sum, parseDate, convertWeight, and isNullOrEmpty compose naturally in a single expression.
Expressions in Practice - Anko Academy