Expand my Community achievements bar.

Join expert-led sessions on Real-Time CDP & Journey Optimizer designed to boost your impact.

Unlock Advanced Personalization: Mastering Journey Optimizer Expression Editor for Dynamic Customer Experiences

Avatar

Employee

6/25/25

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

Function
Syntax
Description
Example
concat()concat(str1, str2, ...)Concatenates stringsconcat("Hello", " ", "World") → "Hello World"
upper()upper(string)Converts to uppercaseupper("hello") → "HELLO"
lower()lower(string)Converts to lowercaselower("WORLD") → "world"
length()length(string)Returns string lengthlength("Hello") → 5
substr()substr(string, start, end)Extracts substringsubstr("Hello", 1, 3) → "el"
indexOf()indexOf(string, pattern)Finds first occurrenceindexOf("Hello", "l") → 2
replace()replace(string, from, to)Replaces first matchreplace("Hello", "l", "x") → "Hexlo"
replaceAll()replaceAll(string, from, to)Replaces all matchesreplaceAll("Hello", "l", "x") → "Hexxo"
split()split(string, separator)Splits into arraysplit("A_B_C", "_") → ["a", "b", "c"]
trim()trim(string)Removes whitespacetrim(" hello ") → "hello"
startWith()startWith(string, prefix)Checks prefixstartWith("Hello", "He") → true
endWith()endWith(string, suffix)Checks suffixendWith("Hello", "lo") → true
contain()contain(base, search)Checks if containscontain("Hello", "ell") → true
isEmpty()isEmpty(string)Checks if emptyisEmpty("") → true
matchRegExp()matchRegExp(string, pattern)Regex matchingmatchRegExp("abc123", "^[a-z]+[0-9]+$") → true

Mathematical Functions (Detailed)

Function Syntax Description Example

Function
Syntax
Description
Example
sum()sum(num1, num2) or sum(array)Additionsum(1, 2, 3) → 6
avg()avg(num1, num2) or avg(array)Averageavg([1, 2, 3]) → 2
max()max(num1, num2) or max(array)Maximummax(1, 5, 3) → 5
min()min(num1, num2) or min(array)Minimummin([10, 2, 8]) → 2
round()round(decimal)Rounds to integerround(3.7) → 4
count()count(array)Counts non-null itemscount([1, null, 3]) → 2
random()random()Random 0–1 decimalrandom() → 0.734...

Date/Time Functions (Detailed)

Function Syntax Description Example

Function
Syntax
Description
Example
now()now() or now(timezone)Current timestampnow("Europe/Paris")
nowWithDelta()nowWithDelta(amount, unit, timezone?)Current datetime with offsetnowWithDelta(-2, "hours", "Europe/Paris") → 2 hours ago
toDateTime()toDateTime(value)Convert to datetimetoDateTime("2023-08-18T23:17:59.123Z") → 2023-08-18T23:17:59.123Z
toDateOnly()toDateOnly(value)Convert to datetoDateOnly(#{ExperiencePlatform.ProfileFieldGroup.person.birthDate})
inLastDays()inLastDays(datetime, days)Within last N daysinLastDays(now(), 30) → true/false
inLastHours()inLastHours(datetime, hours)Within last N hoursinLastHours(now(), 24)

List/Array Functions (Detailed)

Function Syntax Description Example

Function
Syntax
Description
Example
listSize()listSize(array)Array lengthlistSize([1,2,3]) → 3
getListItem()getListItem(array, index)Get item at indexgetListItem(["a","b"], 0) → "a"
in()in(value, array)Check membershipin("x", ["x","y"]) → true
filter()filter(array, key, values)Filter by criteriafilter(@event{myevent.productListItems}, "type", ["online"])
sort()sort(array, ascending)Sort arraysort([3,1,2], true) → [1,2,3]
distinct()distinct(array)Remove duplicatesdistinct([1,1,2]) → [1,2]
intersect()intersect(array1, array2)Common elementsintersect([1,2], [2,3]) → [2]
limit()limit(array, count, fromStart)Returns first/last N elements of a listlimit([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