Iterate over contextual data personalization-contexts
Learn how to use Handlebars iteration syntax to display dynamic lists of data from various sources in your messages, including events, custom action responses, and other contextual data.
Overview overview
Journey Optimizer provides access to contextual data from multiple sources during message personalization. You can iterate over arrays from these sources using Handlebars syntax in native channels (email, push, SMS) to display dynamic content like product lists, recommendations, or other repeating elements.
Available context sources:
- Events: Data from journey events (business events, unitary events)
- Custom action responses: Data returned from external API calls via custom actions
- Dataset lookup: Enriched data retrieved from Adobe Experience Platform datasets
- Technical properties: Journey metadata such as journey ID and supplemental identifiers
- Journey context: Other journey-related data accessible during execution
This guide shows you how to iterate over arrays from each of these sources in your messages, and how to work with arrays when configuring journey activities. Start with Handlebars iteration syntax to understand message personalization basics, or jump to Work with arrays in Journey expressions to learn how to pass array data to custom actions and dataset lookups.
Handlebars iteration syntax syntax
Handlebars provides the {{#each}} helper to iterate over arrays. The basic syntax is:
Key points:
- Replace
arrayPathwith the path to your array data - Replace
itemwith any variable name you prefer (e.g.,product,response,element) - Access properties of each item using
{{item.propertyName}} - You can nest multiple
{{#each}}blocks for multi-level arrays
Iterate over event data event-data
Event data is available when your journey is triggered by an event. This is useful for displaying data that was captured at the moment the journey started, such as cart contents, order items, or form submissions.
Context path for events
<event_ID>: The unique ID of your event as configured in the journey<fieldPath>: The path to the field or array within your event schema
Example: Cart items from an event
If your event schema includes a productListItems array (standard XDM format), you can display cart contents as detailed in the sample below.
| code language-handlebars |
|---|
|
Example: Nested arrays in events
For nested structures, use nested {{#each}} blocks.
| code language-handlebars |
|---|
|
Learn more about nesting in Best practices.
Iterate over custom action responses custom-action-responses
Custom action responses contain data returned from external API calls. This is useful for displaying real-time information from your systems, such as loyalty points, product recommendations, inventory status, or personalized offers.
Context path for custom actions
<actionName>: The name of your custom action as configured in the journey<fieldPath>: The path to the field or array within the response payload
Example: Product recommendations from an API
To iterate over an array of product recommendations returned from a custom action and display them as individual cards in your message, see the example below.
API Response:
| code language-json |
|---|
|
Message personalization:
| code language-handlebars |
|---|
|
Example: Nested arrays from custom actions
To iterate over a custom action response containing nested arrays (an array of objects, where each object contains another array), see the example below. This demonstrates using nested {{#each}} loops to access multiple levels of data.
API Response:
| code language-json |
|---|
|
Message personalization:
| code language-handlebars |
|---|
|
For more complex nesting patterns, see Best practices.
Example: Loyalty tier benefits
To display dynamic benefits based on loyalty status, see the below example.
API Response:
| code language-json |
|---|
|
Message personalization:
| code language-handlebars |
|---|
|
Iterate over dataset lookup results dataset-lookup
The Dataset Lookup activity allows you to retrieve data from Adobe Experience Platform datasets during journey runtime. The enriched data is stored as an array and can be iterated over in your messages.
Learn more about configuring the Dataset Lookup activity in this section. Dataset lookup is particularly powerful when combined with event data - see Example: Event data enriched with dataset lookup for a practical use case.
Context path for dataset lookups
<activityID>: The unique ID of your Dataset Lookup activityentities: The array of enriched data retrieved from the dataset
Example: Product details from a dataset
If you’re using a Dataset Lookup activity to retrieve product information based on SKUs, see sample below.
Dataset Lookup Configuration:
- Lookup Keys:
list(@event{purchase_event.products.sku}) - Fields to Return:
["SKU", "category", "price", "name"]
Message personalization:
| code language-handlebars |
|---|
|
Example: Filtered iteration with dataset data
To filter dataset lookup results during iteration and display only items matching specific criteria (e.g., products from a particular category), use conditional {{#if}} statements within your {{#each}} loop. See the example below.
| code language-handlebars |
|---|
|
Learn more about conditional filtering in Best practices.
Example: Calculate totals from dataset lookup
To calculate and display totals while iterating over dataset lookup results, see the example below.
| code language-handlebars |
|---|
|
Use journey technical properties technical-properties
Journey technical properties provide access to metadata about the journey execution, such as the journey ID and supplemental identifiers. These can be useful when combined with iteration patterns, especially for filtering arrays based on specific journey instances.
Available technical properties
Example: Filter array items using supplemental identifier
When using supplemental identifiers in event-triggered journeys with arrays, you can filter to show only the relevant item for the current journey instance. Learn more about supplemental identifiers in this guide.
Scenario: A journey is triggered with multiple bookings, but you want to display information only for the specific booking (identified by supplemental ID) that triggered this journey instance.
| code language-handlebars |
|---|
|
Example: Include journey ID for tracking
To include the journey ID in your message for tracking purposes, see the example below.
| code language-handlebars |
|---|
|
Combine multiple context sources combine-sources
You can combine data from different sources in the same message to create rich, personalized experiences. This section shows practical examples of using multiple context sources together.
Context sources you can combine:
Example: Cart items with real-time inventory
To combine event data (cart contents) with custom action data (inventory status), view the sample below.
| code language-handlebars |
|---|
|
Example: Event data enriched with dataset lookup
To combine event SKUs with detailed product information from a dataset lookup, view the sample below.
| code language-handlebars |
|---|
|
Example: Combine multiple sources with technical properties
To combine multiple context sources (profile data, event data, custom actions, and technical properties) in a single message, view the sample below.
| code language-handlebars |
|---|
|
Other context types other-contexts
While this guide focuses on iteration over arrays, other context types are available for personalization that typically don’t require iteration. These are accessed directly rather than looped over:
- Profile attributes (
profile.*): Individual profile fields from Adobe Experience Platform - Audiences (
inAudience()): Audience membership checks - Offer decisions: Decision management offers
- Target attributes (Orchestrated campaigns only): Attributes calculated in the campaign canvas
- Token (
context.token): Session or authentication tokens
For complete personalization syntax and examples using these sources, refer to:
Work with arrays in Journey expressions arrays-in-journeys
While the previous sections focus on iterating over arrays in message personalization using Handlebars, you also work with arrays when configuring journey activities. This section explains how to use array data from events in Journey expressions, particularly when passing data to custom actions or using arrays with dataset lookups.
first, all, and serializeList. In message content, you use Handlebars syntax with {{#each}} loops.Pass array values to custom action parameters arrays-to-custom-actions
When configuring custom actions, you often need to extract values from event arrays and pass them as parameters. This section covers common patterns.
Learn more about passing collections in Pass collections into custom action parameters.
Extract a single value from an array
Use case: Get a specific field from an event array to pass as a query parameter in a GET request.
Example scenario: Extract the first SKU with a price greater than 0 from a product list.
Event schema example:
| code language-json |
|---|
|
Custom action configuration:
- In your custom action, configure a query parameter (e.g.,
sku) with typestring - Mark it as
Variableto allow dynamic values
Journey expression in action parameter:
| code language-javascript |
|---|
|
Explanation:
@event{YourEventName}: References your journey event.first(currentEventField.condition): Returns the first array item matching the conditioncurrentEventField: Represents each item in the event array as you loop through it.SKU: Extracts the SKU field from the matched item- Result:
"SKU-1"(a string suitable for the action parameter)
Learn more about the first function in Collection management functions.
Build a list of values from an array
Use case: Create a comma-separated list of IDs to pass as a query parameter (e.g., /products?ids=sku1,sku2,sku3).
Custom action configuration:
- Configure a query parameter (e.g.,
ids) with typestring - Mark it as
Variable
Journey expression:
| code language-javascript |
|---|
|
Explanation:
-
.all(currentEventField.condition): Returns all array items matching the condition (returns a list) -
currentEventField: Represents each item in the event array as you loop through it -
.SKU: Projects the list to only include SKU values -
serializeList(list, delimiter, addQuotes): Joins the list into a string",": Use comma as delimitertrue: Add quotes around each string element
-
Result:
"SKU-1,SKU-3"(suitable for a query parameter)
Learn more about:
Collection handling for custom actions is covered in Pass collections into custom action parameters.
Pass an array of objects to a custom action
Use case: Send a complete array of objects in a request body (for POST or GET with body).
Request body example:
| code language-json |
|---|
|
Custom action configuration:
- In the request body, define
productsas typelistObject - Mark it as
Variable - Define the object fields:
id,name,price,color(each becomes mappable)
Journey canvas configuration:
-
In Advanced mode, set the collection expression:
code language-javascript @event{YourEventName.commerce.productListItems.all(currentEventField.priceTotal > 0)} -
In the collection mapping UI:
- Map
id→productListItems.SKU - Map
name→productListItems.name - Map
price→productListItems.priceTotal - Map
color→productListItems.color
- Map
Journey Optimizer constructs the array of objects matching your action payload structure.
| note note |
|---|
| NOTE |
When working with event arrays, use currentEventField to reference each item. For data source collections (Adobe Experience Platform), use currentDataPackField. For custom action response collections, use currentActionField. |
Learn more in Pass collections into custom action parameters.
Use arrays with dataset lookups arrays-with-dataset-lookup
When using the Dataset Lookup activity, you can pass an array of values as lookup keys to retrieve enriched data.
Example: Look up product details for all SKUs in an event array.
Dataset Lookup configuration:
In the lookup keys field, use list() to convert an array path to a list:
| code language-javascript |
|---|
|
This creates a list of all SKU values to look up in the dataset. The results are available as an array at context.journey.datasetLookup.<activityID>.entities that you can iterate over in your message (see Iterate over dataset lookup results).
Limitations and patterns array-limitations
Be aware of these limitations when working with arrays in journeys:
No dynamic looping over arrays in journey flow
Journeys cannot create dynamic loops where one action node is executed multiple times for each item in an array. This is intentional to prevent runaway performance issues.
What you cannot do:
- Execute a custom action once per array item dynamically
- Create multiple journey branches based on array length
Recommended patterns instead:
-
Send all items at once: Pass the entire array or a serialized list to a single custom action that processes all items. See Build a list of values from an array.
-
Use external aggregation: Have your external API accept multiple IDs and return combined results in a single call.
-
Pre-compute in AEP: Use computed attributes to pre-calculate values from arrays at the profile level.
-
Single value extraction: If you only need one value, extract it using
firstorhead. See Extract a single value from an array.
Learn more in Guardrails and limitations.
Array size considerations
Large arrays can impact journey performance:
- Event arrays: Keep event payloads under 50KB total
- Custom action responses: Response payloads should be under 100KB
- Dataset lookup results: Limit the number of lookup keys and returned entities
Complete example: Event array to custom action complete-example
Here’s a complete workflow showing how to use an event array with a custom action.
Scenario: When a user abandons their cart, send cart data to an external recommendation API to get personalized suggestions, then display them in an email.
Step 1: Configure the custom action
Create a custom action “GetCartRecommendations”:
- Method: POST
- URL:
https://api.example.com/recommendations - Request body:
| code language-json |
|---|
|
- Mark
cartItemsas typelistObjectandVariable - Define fields:
sku(string),price(number),quantity(integer)
Learn more in Configure a custom action.
Step 2: Configure response payload
In the custom action, configure the response:
| code language-json |
|---|
|
Learn more in Use API call responses.
Step 3: Wire the action in the journey
-
After your cart abandonment event, add the custom action
-
In Advanced mode for the
cartItemscollection:code language-javascript @event{cartAbandonment.commerce.productListItems.all(currentEventField.quantity > 0)} -
Map the collection fields:
sku→productListItems.SKUprice→productListItems.priceTotalquantity→productListItems.quantity
Step 4: Use the response in your email
In your email content, iterate over the recommendations:
| code language-handlebars |
|---|
|
Step 5: Test your configuration
Before running a live journey, test the custom action using the “Send test request” feature in the action configuration to verify the built request and values.
- Use journey test mode
- Trigger with sample event data including a
productListItemsarray - Verify the custom action receives the correct array structure
- Check the action test logs
- Preview the email to confirm both arrays display correctly
Learn more in Troubleshoot your custom actions.
Best practices best-practices
Follow these best practices when iterating over contextual data to create maintainable, performant personalization.
Use descriptive variable names
Choose variable names that clearly indicate what you’re iterating over. This makes your code more readable and easier to maintain. Learn more about personalization syntax:
| code language-handlebars |
|---|
|
Expression fragments in loops
When using expression fragments within {{#each}} loops, be aware that you cannot pass loop-scoped variables as fragment parameters. However, fragments can access global variables that are defined in your message content outside of the fragment.
Supported pattern - Use global variables:
| code language-handlebars |
|---|
|
The fragment can reference globalDiscount because it’s defined globally in the message.
Not supported - Passing loop variables:
| code language-handlebars |
|---|
|
Workaround: Include the personalization logic directly in your loop instead of using a fragment, or call the fragment outside of the loop.
Learn more about using expression fragments inside loops, including detailed examples and additional workarounds.
Handle empty arrays
Use the {{else}} clause to provide fallback content when an array is empty. Learn more about helper functions:
| code language-handlebars |
|---|
|
Combine with conditional helpers
Use {{#if}} within loops for conditional content. Learn more about conditional rules and see examples in Custom action responses and Dataset lookup sections.
| code language-handlebars |
|---|
|
Limit iteration for performance
For large arrays, consider limiting the number of iterations:
| code language-handlebars |
|---|
|
Access array metadata
Handlebars provides special variables within loops that help with advanced iteration patterns:
@index: Current iteration index (0-based)@first: True for the first iteration@last: True for the last iteration
| code language-handlebars |
|---|
|
@index, @first, @last) are only available within {{#each}} loops in message personalization. For working with arrays in Journey expressions (such as getting the first item from an array before passing to a custom action), use array functions like head, first, or all. See Work with arrays in Journey expressions for more details.Troubleshooting troubleshooting
Having issues with iteration? This section covers common problems and solutions.
Array not displaying
Issue: Your array iteration isn’t showing any content.
Possible causes and solutions:
-
Incorrect path: Verify the exact path to your array based on the context source:
- For events:
context.journey.events.<event_ID>.<fieldPath> - For custom actions:
context.journey.actions.<actionName>.<fieldPath> - For dataset lookups:
context.journey.datasetLookup.<activityID>.entities
- For events:
-
Array is empty: Add an
{{else}}clause to check if the array has no data. See Best practices for examples. -
Data not available yet: Ensure the custom action, event, or dataset lookup activity has been executed before the message activity in your journey flow.
Syntax errors
Issue: Expression validation fails or message doesn’t render.
Common mistakes:
- Missing closing tags: Every
{{#each}}must have a{{/each}}. Review Handlebars iteration syntax for proper structure. - Incorrect variable name: Ensure consistent use of variable name throughout the block. See Best practices for naming conventions.
- Incorrect path separators: Use dots (
.) not slashes or other characters
Expression fragments not working in loops
Issue: An expression fragment doesn’t display expected content when used inside an {{#each}} loop, or shows empty/unexpected output.
Possible causes and solutions:
-
Trying to pass loop variables as parameters: Expression fragments cannot receive loop-scoped variables (like the current iteration item) as parameters. This is a known limitation.
Solution: Use one of these workarounds:
- Define global variables in your message that the fragment can access
- Include the personalization logic directly in your loop instead of using a fragment
- Call the fragment outside of the loop if it doesn’t need loop-specific data
-
Fragment expects a parameter that isn’t available: If your fragment was designed to receive specific input parameters, it won’t work correctly when those parameters can’t be passed from within a loop.
Solution: Restructure your approach to use global variables that the fragment can access. See Best practices - Expression fragments in loops for examples.
-
Incorrect variable scope: The fragment might be trying to reference a variable that only exists within the loop scope.
Solution: Define any variables the fragment needs at the message level (outside the loop) so they’re globally accessible.
Learn more about using expression fragments inside loops, including detailed explanations, examples, and recommended patterns.
Testing your iterations
Use journey test mode to verify your iterations. This is especially important when using custom actions or dataset lookups:
- Start your journey in test mode
- Trigger the event or custom action with sample data
- Check the message preview to verify the iteration displays correctly
- Review test mode logs for any errors (see Custom action test mode logs)
Related topics related-topics
Personalization fundamentals: Get started with personalization | Add personalization | Personalization syntax | Helper functions | Create conditional rules
Journey configuration: About events | Configure custom actions | Pass collections into custom action parameters | Use API call responses in custom actions | Troubleshoot your custom actions | Use Adobe Experience Platform data in journeys | Use supplemental identifiers in journeys | Guardrails and limitations | Test your journey
Journey expression functions: Advanced expression editor | Collection management functions (first, all, last) | List functions (serializeList, filter, sort) | Array functions (head, tail)
Personalization use cases: Cart abandonment email | Order status notification
Message design: Get started with email design | Create push notifications | Create SMS messages | Preview and test your content