Skip to main content
Custom Events let you personalize Journey messages using event properties (product names, prices, URLs, arrays, etc.). Event properties become available in templates only if the event:
  • Triggers Journey entry, or
  • Matches a Wait Until condition inside the Journey
Stored event properties can be accessed using Liquid syntax.

How Custom Event personalization works

Add event properties to your Journey messages following these steps:
1

Send a Custom Event with properties

Example Custom Events payload:
JSON
{
  "name": "purchase",
  "properties": {
    "item": "Blue Sweater",
    "price": "29.99",
    "order status": "pending",
    "details": [
      {
        "manufacturer": "Company A",
        "model": "1234567890"
      },
      {
        "manufacturer": "Company B",
        "model": "9876543210"
      }
    ]
  },
  "external_id": "user_12345",
  "timestamp": "2025-10-21T19:09:32.263Z",
}
2

Reference event properties in your Journey message templates

Use Liquid syntax to access event properties.

Common Liquid access patterns

What you wantLiquidOutput
Event name{{ journey.first_event.name }}purchase
Property{{ journey.first_event.properties.item }}Blue Sweater
Nested properties{{ journey.first_event.properties.details.first.manufacturer }}Company A
Property with special characters{{ journey.last_event.properties["order status"] }}pending
Timestamp{{ journey.last_event.timestamp | date: "%B %d, %Y at %I:%M %p" }}October 21, 2025 at 07:09 PM

Nested Liquid access patterns

You can also access nested properties using dot and bracket notation:
Liquid
{{ journey.first_event.properties.details.first.manufacturer }} = Company A
{{ journey.first_event.properties.details.last.manufacturer }} = Company B
{{ journey.first_event.properties.details[0].manufacturer }} = Company A
{{ journey.first_event.properties.details[1].manufacturer }} = Company B
Invalid Liquid or missing properties render as empty strings. Messages still send. Use default filters to set a fallback.
3

Create a Journey

Setup the Journey to use the Custom Event as the entry rule and/or Wait Until condition.Add message nodes with the templates.

Event property storage rules

  • You can use multiple events in your Journey by combining entry rules and Wait Until steps.
  • Maximum: 100 stored event properties per user per Journey instance (oldest dropped).
  • Event properties are stored per user, per Journey instance.
  • Events sent before entry are not accessible.
  • Event properties are cleared when the user exits the Journey.
Events stored in one Journey cannot be accessed in another Journey.

Custom Event Liquid reference

Use these objects to access stored events inside the Journey.
journey.first_event
The first stored event for this Journey instance.
  • If using a Custom Event Entry Rule, than this is the event that caused Journey entry.
  • If not using a Custom Event Entry Rule, then this is the first event stored by matching a Wait Until condition.
Liquid
Thanks for purchasing {{ journey.first_event.properties.item | default: "your item" }}!
journey.last_event
The most recent stored event for this Journey instance.
  • If only one event is stored, first_event and last_event return the same thing.
Liquid
Your most recent purchase was {{ journey.last_event.properties.item | default: "made" }}!
journey.event.EVENT_NAME
The most recent stored event with a specific name.
  • Replace EVENT_NAME with your event name (e.g., purchase).
  • If the same event name is used multiple times, this returns the most recent instance.
Liquid
{% assign event = journey.event.purchase %}
If your event name includes spaces or special characters, use bracket notation.Example Event: "name": "order status"
Liquid
{% assign event = journey.event["order status"] %}
journey.all_events
array
All stored events for this Journey instance, in the order they were stored.
Liquid
{% for event in journey.all_events %}
{{ event.properties.item | default: "Item" }} — ${{ event.properties.price | default: "0.00" }}
{% endfor %}
  • journey.first_event is shorthand for journey.all_events[0].
  • journey.last_event is shorthand for the most recent event in the array.

Example: Abandoned cart templates using Custom Events

This example shows how to personalize abandoned cart messages using Custom Events. It builds on the Abandoned Cart tutorial. Example Custom Event set:
JSON
{
  "events": [
    {
      "name": "cart_abandoned",
      "properties": {
        "cart_url": "https://yourdomain.com/username/cart",
        "cart": [
          {
            "product_name": "24 Pack of Acorns",
            "product_image": "https://i.imgur.com/ssPCfbC.png",
            "product_price": "$12.99",
            "product_quantity": "1"
          },
          {
            "product_name": "Fancy Sweater",
            "product_image": "https://i.imgur.com/8QWTfV4.png",
            "product_price": "$9.99",
            "product_quantity": "1"
          }
        ]
      },
      "external_id": "ID_OF_THE_USER"
    }
  ]
}

Email template

This example shows how to build an email template that displays:
  • The cart item count
  • Each product with image, name, quantity, and price using a for-loop
  • A button that links to the customer’s unique cart URL
1

Create the email template

Navigate to Messages > Templates > New Email Template and open the Drag & Drop Editor.
2

Add the layout structure

Create five rows:
  • Rows 1, 2, and 4: one column with a Paragraph block
  • Row 3: four columns with HTML | Paragraph | Paragraph | Paragraph
  • Row 5: one column with a Button block
3

Display the item count

In row 1, add:
Liquid
We're holding onto {{journey.first_event.properties.cart.size}} items in your cart, but don't wait too long!
For better grammar, you could use a conditional to say “1 item” vs “2 items”, but for abandoned cart emails, plural is usually acceptable.
Liquid
{% assign cart = message.custom_data.cart %}
{% assign item_count = cart.size | plus: 0 %}
{% if item_count == 1 %}
We're holding onto {{item_count}} item in your cart, but don't wait too long, other squirrels are getting ahead!
{% endif %}
{% if item_count > 1 %}
We're holding onto {{item_count}} items in your cart, but don't wait too long, other squirrels are getting ahead!
{% endif %}
4

Start the for-loop

Use a for-loop to repeat the product display row for each cart item.In row 2 (loop start), add:
Liquid
{% for product in journey.first_event.properties.cart %}
What this does:
  • Begins a loop that iterates over each object in the cart array
  • Creates a temporary variable product representing the current item
  • Everything between {% for %} and {% endfor %} repeats once per cart item
  • You can name product anything (e.g., item, cartItem)—just stay consistent
For-loop placement: Make sure the {% for %} syntax is in its own Text block row. Don’t put it inside a multi-column row with other content, as this can break email rendering in some clients.
5

Display product details

This 4-column row shows image, name, quantity, and price. Because it’s inside the loop, it repeats for every cart item.In row 3 (product details), configure:Column 1 - HTML block (product image):
<img src="{{product.product_image}}" alt="Product image" style="max-width:100%;" />
Columns 2–4 - Text blocks (product name, quantity, price):
  • Column 2: {{product.product_name}}
  • Column 3: {{product.product_quantity}}
  • Column 4: {{product.product_price}}
How the loop works:
  • On first iteration, product = first object in cart array
  • {{product.product_image}} gets the first item’s image
  • On second iteration, product = second object
  • Row repeats automatically for all cart items
Field name matching: Keys like product_image must exactly match your event payload (case-sensitive). Mismatches render as empty strings.
6

End the for-loop

Close the loop to mark where repetition stops.In row 4 (loop end), add:
Liquid
{% endfor %}
Every {% for %} must have a matching {% endfor %}. Missing this will break email rendering.
7

Add the cart URL button

In the row 5 Button block, set the Action URL to:
Liquid
{{journey.first_event.properties.cart_url}}
8

Test the template

  • Add the template to a blank Journey and set the entry rule to a Custom Event.
  • Enabling the Journey and entering yourself into it via the Custom Event API.
  • Verify the data displays correctly.
Success! Now you can apply your own styling to the template. See Design emails with drag-and-drop.

Push template

Push notifications have limited space, so display one item and mention the total count. Message field: Display item and count with correct grammar using conditional statements.
Liquid
{% assign cart = journey.first_event.properties.cart %}
{% assign item_count = cart.size | plus: 0 %}
{% if item_count == 1 %}
You left {{cart.first.product_name}} in your cart.
{% endif %}
{% if item_count == 2 %}
You left {{cart.first.product_name}} and {{item_count | minus: 1}} more item in your cart.
{% endif %}
{% if item_count > 2 %}
You left {{cart.first.product_name}} and {{item_count | minus: 1}} more items in your cart.
{% endif %}
Image field:
Liquid
{{journey.first_event.properties.cart.first.product_image | default: "https://i.imgur.com/ssPCfbC.png"}}
Launch URL field:
Liquid
{{journey.first_event.properties.cart_url | default: "https://yourdomain.com/cart"}}
Send yourself a test push notification by adding it to the test Journey and entering yourself into it via another Custom Event API call. You should see a notification similar to this appear:
Success! You can now create more templates and use them in the Abandoned Cart Journey.

Troubleshooting & best practices

Common mistakes:
MistakeWhy it failsCorrect syntax
{{ journey.first_event.item }}Missing .properties{{ journey.first_event.properties.item }}
{{ journey.event.purchase.item }}Missing .properties{{ journey.event.purchase.properties.item }}
{{ journey.first_event.properties.Item }}Wrong case (should be item){{ journey.first_event.properties.item }}
{{ event.properties.item }}Missing journey. prefix{{ journey.first_event.properties.item }}
Best practices:
  • Always test templates before going live
  • Use default filters for optional properties
  • Validate event schema matches template expectations

Need help?Chat with our Support team or email [email protected]Please include:
  • Details of the issue you’re experiencing and steps to reproduce if available
  • Your OneSignal App ID
  • The External ID or Subscription ID if applicable
  • The URL to the message you tested in the OneSignal Dashboard if applicable
  • Any relevant logs or error messages
We’re happy to help!