Creating truly personalised customer journeys often requires more than simple conditions. You need the power to dynamically evaluate customer data, perform complex calculations, and create sophisticated logic that adapts to real-time customer behavior.
While Journey Optimiser's simple expression mode works well for basic conditions. The Advanced Expression Editor unlocks a world of possibilities for marketers who need to create intelligent, data-driven customer experiences. This guide will show you how to leverage advanced expressions to solve real marketing challenges and create more engaging customer journeys.
The Challenge: When Simple Conditions Aren't Enough
Consider these common marketing scenarios:
Scenario 1: Abandoned Cart Recovery
You want to target customers who added items to their cart but didn't purchase, but only if they're high-value customers and it's been exactly 24 hours since abandonment.
Scenario 2: Dynamic Loyalty Offers
You need to send different offers based on customer loyalty tier, purchase history, geographic location, and seasonal preferences - all determined dynamically when the customer enters the journey.
Scenario 3: Time-Sensitive Promotions
You want to create urgency by showing different messaging based on how much time is left in a promotion, calculated in real-time for each customer's timezone.
These scenarios require more than simple "if-then" logic. They need dynamic data evaluation, complex calculations, and sophisticated decision-making capabilities.
Understanding the Expression Language Architecture
Core Language Structure
The Journey Optimizer expression language follows a functional programming paradigm with several key components:
Expression Syntax Fundamentals
Data Reference Patterns:
// Event Field Reference - Access data from triggering events
@Event{EventName.fieldPath.nestedField}
// Profile Data Reference - Access unified customer profile
#{ExperiencePlatformDataSource.ProfileFieldGroup.fieldPath}
// External Data Source Reference - Query external systems
#{DataSourceName.FieldGroupName.fieldPath}
// Journey Properties - Access journey-level metadata
#{journeyUID}
#{currentNodeName}
#{lastErrorCode}
//Audience reference
inAudience(<audience name>)
Function Call Structure:
// Basic function syntax
functionName(parameter1, parameter2, ...)
// Nested function calls (inner functions execute first)
outerFunction(innerFunction(parameter))
// Chained operations on collections
filter(collection, "field", ["value1", "value2"])
Operator Precedence (highest to lowest):
1. Parentheses: `(expression)`
2. Function Calls: `functionName()`
3. Arithmetic: `*`, `/`, `%` , `+`, `-`
4. Comparison: `==`, `!=`, `>`, `<`, `>=`, `<=`
5. Logical: `and`, `or`
1. Data References
- Events: `@event{EventName.fieldPath}` - Access incoming event data
- Data Sources: `#{DataSource.FieldGroup.fieldPath}` - Query external data
- Journey Properties: `#{<journeyProperty>}` - Access journey metadata
- Audience Reference: inAudience(<string>) - Checks if an individual belongs to a given audience
2. Function Categories
- String Functions: `concat()`, `substr()`, `contain()`, `split()`
- Math Functions: `sum()`, `avg()`, `max()`, `min()`, `round()`
- Date/Time Functions: `now()`, `toDateTime()`, `inLastDays()`
- List Functions: `filter()`, `sort()`, `listSize()`, `getListItem()`
- Conversion Functions: `toString()`, `toInteger()`, `toBool()`
3. Operators and Logical Constructs
- Boolean operators: `and`, `or`, `not`
- Comparison operators: `==`, `!=`, `>`, `<`, `>=`, `<=`
- Null checks: `is null`, `is not null`
- Type validation: `is numeric`, `is empty`
Data Types and Validation Patterns
Supported Data Types:
// String literals
"Hello World"
'Single quotes also work'
// Numeric types
42 // Integer
3.14159 // Decimal
-100 // Negative numbers
// Boolean values
true
false
// Date/Time formats
date("2024-01-15") // Date only
date("2024-01-15T10:30:00") // DateTime only
date("2024-01-15T10:30:00Z") // DateTime with timezone
date("2024-01-15T10:30:00+02:00") // DateTime with offset
// Arrays/Lists
["item1", "item2", "item3"] // String array
[1, 2, 3, 4, 5] // Integer array
[true, false, true] // Boolean array
// Null values
null
Validation and Type Checking:
// Null safety patterns
fieldName is not null and fieldName is not empty
// Type validation
fieldName is numeric // Can be converted to number
fieldName is integer // Is whole number
fieldName is decimal // Is decimal number
fieldName is empty // String is empty or null
// Combined validation example
if (#{Profile.age} is not null and #{Profile.age} is numeric)
then (toInteger(#{Profile.age}) >= 18)
else (false)
Solution: Step-by-Step Implementation Guide
Let's solve these challenges using the Advanced Expression Editor. We'll walk through each scenario with practical implementations you can use immediately.
Use Case 1: Smart Abandoned Cart Recovery
Business Goal: Target high-value customers who abandoned their cart exactly 24 hours ago.
Step 1: Set Up the Condition
In your journey's condition node, switch to Advanced Mode and enter:
// Check if customer abandoned cart exactly 24 hours ago and is high-value
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.customerValue} > 1000
and
inLastHours(@event{CartAbandonmentEvent.timestamp}, 24)
What This Does:
- Targets customers with a customer value over $1000
- Checks if current time is within the last 24 hours
Use Case 2: Dynamic Loyalty Tier Offers
Business Goal: Send different offers based on customer loyalty tier, purchase history, and location - all determined dynamically.
Step 1: Create the Condition Logic
In your condition node, implement tier-based routing:
// Route to different offer paths based on tier and spending
if (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Gold" and sum(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.purchaseHistory.totalSpent}) > 5000)
then ("premium-offer-path")
else if (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Silver")
then ("standard-offer-path")
else ("basic-offer-path")
Step 2: Configure Each Path
Now you can create different journey paths for each segment:
- Premium Path: Exclusive products, VIP support, free shipping
- Standard Path: Popular products, standard support, discount shipping
- Basic Path: Entry-level products, community support, standard shipping
Step 3: Add Location-Based Refinement
Within each path, further personalize by location:
// For customers in metropolitan areas, offer same-day delivery
contain(upper(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.homeAddress.city}), upper("New York"))
or
contain(upper(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.homeAddress.city}), upper("Los Angeles"))
Practical Tips:
- Use `upper()` to handle case variations ("NEW YORK" vs "new york")
- The `contain()` function catches abbreviations ("NYC" within "NYC Metro")
- Add more cities as needed with additional `or` statements
Use Case 3: Time-Sensitive Promotions
Business Goal: Create urgency by showing different messaging based on how much time is left in a promotion, with special handling for different dates and time periods, targeting eligible customers.
Implementation:
// Check if promotion is ending today AND customer is eligible (create urgency)
substr(toString(now()),0,10) == "2025-06-25"
and
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} in ["Gold", "Platinum"]
and
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.marketingConsent} == true
or
// Or check if we're within the last 3 days of promotion for VIP customers
inLastDays(toDateTime("2025-06-24T23:17:59.123Z"), 3)
and
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.customerValue} > 500
and
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.email} is not empty
Advanced Profile-Based Targeting:
- Loyalty Tier Filtering: `loyaltyTier in ["Gold", "Platinum"]` ensures only premium customers get last-day urgency
- Consent Validation: `marketingConsent == true` respects customer preferences
- Value-Based Segmentation: `customerValue > 500` targets high-value customers for 3-day window
- Contact Validation: `email is not empty` ensures we can actually reach the customer
Why This Pattern Works:
- Real-time Urgency: Automatically updates messaging as promotion deadline approaches
- Smart Segmentation: Different urgency windows for different customer tiers
- Compliance Ready: Includes consent and contact validation
- Personalized Timing: Premium customers get immediate urgency, others get longer window
Alternative Approaches:
// Multi-tier urgency with profile-based customization
if ((substr(toString(now()),0,10)) == "2025-06-25")
and #{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Platinum")
then ("FINAL HOURS - Platinum Exclusive!")
else if (inLastDays(toDateTime("2025-06-24T23:17:59.123Z"), 3)
and #{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Gold")
then ("3 Days Left - Gold Member Priority!")
else if (inLastDays(toDateTime("2025-06-24T23:17:59.123Z"), 7)
and #{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.customerValue} > 100)
then ("One Week Left - Don't Miss Out!")
else ("Limited time offer")
4. Smart Cart Status Evaluation with Event Counting
One of the most common challenges in e-commerce journeys is accurately tracking cart behavior by counting profile experience events. Based on community discussions, marketers need to count product add/remove events and make intelligent decisions about cart abandonment recovery.
Business Challenge: Count product add events vs. product remove events for a profile to determine if the cart is actively being filled or abandoned, then trigger appropriate messaging.
// Count recent cart events and determine cart status
count(filter(@event{ProductEvent.eventType}, "eventType", ["cart_add"])) >
count(filter(@event{ProductEvent.eventType}, "eventType", ["cart_remove"]))
and
count(filter(@event{ProductEvent.eventType}, "eventType", ["cart_add"])) > 0
and
inLastHours(@event{ProductEvent.timestamp}, 2)
Expression Breakdown:
- Add Events Count: `count(filter(..., ["cart_add"]))` counts all product addition events
- Remove Events Count: `count(filter(..., ["cart_remove"]))` counts all product removal events
- Net Positive Cart: First condition ensures more adds than removes (growing cart)
- Activity Validation: Second condition ensures at least one add event occurred
- Recency Check: `inLastHours(..., 2)` ensures events happened within last 2 hours
Advanced Cart Intelligence:
// Sophisticated cart abandonment detection with value calculation
if (count(filter(@event{CartEvent.products}, "eventType", ["add"])) > 0
and count(filter(@event{CartEvent.products}, "eventType", ["remove"])) == 0
and sum(@event{CartEvent.products.price}) > 100
and inLastHours(@event{CartEvent.timestamp}, 4))
then ("high-value-abandoned-cart")
else if (count(filter(@event{CartEvent.products}, "eventType", ["add"])) >
count(filter(@event{CartEvent.products}, "eventType", ["remove"]))
and sum(@event{CartEvent.products.price}) > 50)
then ("moderate-value-active-cart")
else ("low-engagement-cart")
Why This Pattern Works:
- Real-time Cart Analysis: Automatically evaluates cart health based on user behavior
- Value-Based Decisions: Considers cart value alongside event counts for smarter targeting
- Flexible Timeframes: Adjustable time windows for different business models
- Multiple Outcomes: Routes customers to appropriate messaging based on cart status
Pro Tips and Best Practices
1. Performance Optimisation
Cache Expensive Operations: When using the same complex expression multiple times, consider breaking it into smaller, reusable components.
Minimize Data Source Calls: Batch your data source queries and avoid redundant calls within the same condition.
// INEFFICIENT
if (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Gold" or #{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Platinum")
then (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.email} is not null
and #{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.email} is not empty)
else (false)
// BETTER: Use efficient operators
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} in ["Gold", "Platinum"]
and
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.email} is not empty
Key Optimization Principles:
- Avoid Duplicate Calls: Don't repeat the same complex expressions
- Use Efficient Operators: `in [...]` is more efficient than multiple `==` comparisons
- Combine Related Checks: Group related field validations together
- Cache Results: Structure expressions to minimize repeated calculations
2. Error Handling and Null Safety
Always implement null checks for optional fields:
// Safe string concatenation
if (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.person.name.firstName} is not null)
then (concat("Hello ", #{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.person.name.firstName}))
else ("Hello valued customer")
3. Debugging Complex Expressions
Use the built-in validation to catch syntax errors early, and break complex expressions into smaller, testable components.
Testing Strategy:
1. Start with simple expressions and build complexity gradually
2. Use the expression editor's validation feedback
3. Test with real customer data before deploying to production
4. Documentation and Maintenance
Always document your complex expressions with comments explaining the business logic:
// Business Rule: VIP customers (Gold tier + >$5000 annual spend) get exclusive offers
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Gold"
and
sum(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.annualPurchases.amount}) > 5000
Function Reference Guide
String Functions (Detailed)
Function Syntax Description Example
| | | |
concat() | concat(str1, str2, ...) | Concatenates strings | concat("Hello", " ", "World") → "Hello World" |
upper() | upper(string) | Converts to uppercase | upper("hello") → "HELLO" |
lower() | lower(string) | Converts to lowercase | lower("WORLD") → "world" |
length() | length(string) | Returns string length | length("Hello") → 5 |
substr() | substr(string, start, end) | Extracts substring | substr("Hello", 1, 3) → "el" |
indexOf() | indexOf(string, pattern) | Finds first occurrence | indexOf("Hello", "l") → 2 |
replace() | replace(string, from, to) | Replaces first match | replace("Hello", "l", "x") → "Hexlo" |
replaceAll() | replaceAll(string, from, to) | Replaces all matches | replaceAll("Hello", "l", "x") → "Hexxo" |
split() | split(string, separator) | Splits into array | split("A_B_C", "_") → ["a", "b", "c"] |
trim() | trim(string) | Removes whitespace | trim(" hello ") → "hello" |
startWith() | startWith(string, prefix) | Checks prefix | startWith("Hello", "He") → true |
endWith() | endWith(string, suffix) | Checks suffix | endWith("Hello", "lo") → true |
contain() | contain(base, search) | Checks if contains | contain("Hello", "ell") → true |
isEmpty() | isEmpty(string) | Checks if empty | isEmpty("") → true |
matchRegExp() | matchRegExp(string, pattern) | Regex matching | matchRegExp("abc123", "^[a-z]+[0-9]+$") → true |
Mathematical Functions (Detailed)
Function Syntax Description Example
| | | |
sum() | sum(num1, num2) or sum(array) | Addition | sum(1, 2, 3) → 6 |
avg() | avg(num1, num2) or avg(array) | Average | avg([1, 2, 3]) → 2 |
max() | max(num1, num2) or max(array) | Maximum | max(1, 5, 3) → 5 |
min() | min(num1, num2) or min(array) | Minimum | min([10, 2, 8]) → 2 |
round() | round(decimal) | Rounds to integer | round(3.7) → 4 |
count() | count(array) | Counts non-null items | count([1, null, 3]) → 2 |
random() | random() | Random 0–1 decimal | random() → 0.734... |
Date/Time Functions (Detailed)
Function Syntax Description Example
| | | |
now() | now() or now(timezone) | Current timestamp | now("Europe/Paris") |
nowWithDelta() | nowWithDelta(amount, unit, timezone?) | Current datetime with offset | nowWithDelta(-2, "hours", "Europe/Paris") → 2 hours ago |
toDateTime() | toDateTime(value) | Convert to datetime | toDateTime("2023-08-18T23:17:59.123Z") → 2023-08-18T23:17:59.123Z |
toDateOnly() | toDateOnly(value) | Convert to date | toDateOnly(#{ExperiencePlatform.ProfileFieldGroup.person.birthDate}) |
inLastDays() | inLastDays(datetime, days) | Within last N days | inLastDays(now(), 30) → true/false |
inLastHours() | inLastHours(datetime, hours) | Within last N hours | inLastHours(now(), 24) |
List/Array Functions (Detailed)
Function Syntax Description Example
| | | |
listSize() | listSize(array) | Array length | listSize([1,2,3]) → 3 |
getListItem() | getListItem(array, index) | Get item at index | getListItem(["a","b"], 0) → "a" |
in() | in(value, array) | Check membership | in("x", ["x","y"]) → true |
filter() | filter(array, key, values) | Filter by criteria | filter(@event{myevent.productListItems}, "type", ["online"]) |
sort() | sort(array, ascending) | Sort array | sort([3,1,2], true) → [1,2,3] |
distinct() | distinct(array) | Remove duplicates | distinct([1,1,2]) → [1,2] |
intersect() | intersect(array1, array2) | Common elements | intersect([1,2], [2,3]) → [2] |
limit() | limit(array, count, fromStart) | Returns first/last N elements of a list | limit([1,2,3,4], 2) → [1,2] |
Advanced Function Deep Dive
String Manipulation Mastery
The expression language provides robust string handling capabilities:
// Clean and standardize phone numbers
replaceAll(
replace(
replace(#{CustomerDataSource.ContactInfo.phoneNumber}, "(", ""),
")", ""
),
"-", ""
)
Nested Function Pattern:
- Inner-to-Outer Execution: Functions execute from innermost to outermost
- Step 1: `replace(..., "(", "")` removes opening parentheses
- Step 2: `replace(..., ")", "")` removes closing parentheses from Step 1 result
- Step 3: `replaceAll(..., "-", "")` removes all hyphens from Step 2 result
- Final Result: Clean numeric string (e.g., "(555) 123-4567" → "5551234567")
Alternative Approaches:
// Using regular expressions (more efficient for complex patterns)
replaceAll(#{CustomerDataSource.ContactInfo.phoneNumber}, "[^0-9]", "")
// Chain with additional validation
if (length(replaceAll(#{CustomerDataSource.ContactInfo.phoneNumber}, "[^0-9]", "")) == 10)
then (replaceAll(#{CustomerDataSource.ContactInfo.phoneNumber}, "[^0-9]", ""))
else ("INVALID_PHONE")
Mathematical Operations for Business Logic
// Calculate customer lifetime value percentile
if (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.clv} >
avg([1000, 2000, 3000, 4000, 5000]))
then ("high-value-segment")
else ("standard-segment")
List Operations for Complex Filtering
// Filter recent purchases for electronics category
filter(
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.purchases},
"category",
["electronics", "technology", "gadgets"]
)
Filter Function Mechanics:
- Parameter 1: Array/list of purchase objects to filter
- Parameter 2: `"category"` - Field name within each purchase object to evaluate
- Parameter 3: `["electronics", "technology", "gadgets"]` - Array of acceptable values for matching
- Logic: Returns only purchase objects where `purchase.category` matches any value in the filter array
- Result: Subset of original purchases array containing only electronics-related items
Common Pitfalls and How to Avoid Them
1. Data Type Mismatches
Always ensure your data types align. Convert when necessary:
// Correct: Convert string to integer for comparison
toInteger(#{DataSource.Profile.age}) > 25
// Incorrect: Direct comparison may fail
#{DataSource.Profile.age} > 25
2. Experience Event Performance
Avoid overly broad experience event queries that could impact performance:
// More efficient: Limit time range
#{ExperiencePlatformDataSource.ExperienceEventFieldGroup.experienceevent
.all(inLastDays(currentDataPackField.timestamp, 30))}
// Less efficient: No time constraints
#{ExperiencePlatformDataSource.ExperienceEventFieldGroup.experienceevent.all()}
3. Null Pointer Exceptions
Always check for null values before performing operations:
// Safe approach
#{DataSource.Profile.customField} is not null
and
length(#{DataSource.Profile.customField}) > 0
Looking Forward: Advanced Patterns
As you master the basics, consider these advanced patterns:
1. Multi-Condition Logic: Message Tracking and Profile Validation
One of the most common challenges marketers face is validating whether specific messages have been delivered to customers. here's how to solve this with multi-condition logic:
Business Challenge: Check if a specific message was delivered to a customer profile and combine it with other conditions for intelligent routing.
// Check if specific message was delivered AND customer is eligible for follow-up
in("aaa124-27bc-41a5-8c83-4a8cebd79da4",
#{ExperiencePlatform.ajo_ds_email_received.experienceevent
.all(inLastDays(currentDataPackField.timestamp, 7))
._experience.customerJourneyManagement.messageExecution.messageID})
and
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.marketingConsent} == true
and
#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} in ["Gold", "Platinum"]
2. Multi-Variant Testing with Weighted Distribution
Create sophisticated test group assignments based on customer segments and weighted distribution:
// Multi-variant testing with customer segment weighting (safe for alphanumeric IDs)
if (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Platinum")
then (
if (length(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.customerId}) % 10 <= 3)
then ("premium-variant-a")
else if (length(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.customerId}) % 10 <= 6)
then ("premium-variant-b")
else ("premium-control")
)
else if (#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.loyaltyTier} == "Gold")
then (
if (length(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.customerId}) % 2 == 0)
then ("standard-variant-a")
else ("standard-control")
)
else (
if (length(#{ExperiencePlatformDataSource.ProfileFieldGroup.Profile.customerId}) % 10 <= 1)
then ("basic-variant-a")
else ("basic-control")
)
Advanced Testing Features:
- Segment-Based Variants: Different test variants for different customer tiers
- Weighted Distribution: 40%/30%/30% split for Platinum, 50%/50% for Gold, 20%/80% for Basic
- Consistent Assignment: Same customer always gets same variant (based on customer ID length)
- Alphanumeric Safe: Uses `length() % 10` instead of `toInteger()` to avoid failures with non-numeric customer IDs
- Business Logic: Premium customers get more test exposure, basic customers get conservative approach
Key Takeaways
By leveraging the Advanced Expression Editor, you can transform your customer journeys from simple workflows into intelligent, adaptive experiences:
- Solve Complex Business Scenarios: Handle abandoned cart recovery, dynamic loyalty offers, and time-sensitive promotions automatically
- Increase Personalisation: Create highly targeted experiences without pre-building countless segments
- Improve Performance: Smart timing and dynamic content boost engagement rates
- Scale Efficiently: One journey can intelligently handle multiple customer types and scenarios
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.