Skip to main content

Documentation Index

Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt

Use this file to discover all available pages before exploring further.

Event-driven Journeys turn user behavior into automated messaging. Instead of waiting on the clock or relying on broad segment membership, the Journey reacts the moment a user performs a specific action. This guide shows you how to wire Custom Events into Journeys cleanly so your automation stays accurate as you scale. What you’ll learn:
  • Where Custom Events fit inside a Journey (entry rules, Wait Until steps, and exit rules)
  • How to design event names, properties, and types that hold up in Journey logic
  • Three end-to-end patterns for translating real behavior into automation
This guide builds on Custom Events for sending events and Personalize with Custom Events for the full Liquid reference. The focus here is choosing the right event-to-Journey shape for the behavior you’re modeling.

Prerequisites


Where Custom Events fit in a Journey

A single Custom Event can drive a Journey in three different places. Picking the right slot is the first design decision.
If the event represents…Use it as…Reference
The starting moment of an experience (signup, purchase, completion)Entry ruleCustom Event entry rules
The expected next step inside an in-progress JourneyWait Until conditionWait Until
A signal the user is no longer eligible (cancelled, refunded, opted out)Exit ruleExit when Custom Event condition occurs
A repeating triggering action (cart updated, plan changed)Both entry and exit, with the same event nameSame-event entry and exit
Each Journey instance stores the events that triggered entry and matched any Wait Until step. Those events become available to message templates as journey.first_event, journey.last_event, and journey.event.EVENT_NAME. Each instance can store up to 100 event properties per user (oldest dropped). See Event property storage rules.

Design events for Journey use

The shape of your events determines how clean your Journey logic stays. A few rules of thumb pay off as automation grows.

Naming

  • Use snake_case for event names and property keys so they read predictably.
  • Use a past-tense verb phrase for the action: lesson_completed, cart_updated, payment_failed. Past tense is unambiguous; it describes something that already happened.
  • Keep event names stable. Renaming an event after Journeys depend on it silently breaks every reference. Add new event names; don’t rename old ones.
  • Prefer specific names over umbrella names. subscription_cancelled and subscription_paused produce cleaner Journey logic than a single subscription_changed event with a status property.

Property design

Include properties when you’ll actually use them, for personalization, branching, or filtering. Skip properties just because they’re available. Useful categories:
  • Identifiers for matching across events: order_id, workspace_id, course_id. These are what Wait Until’s Event Matching keys on.
  • Display values for templates: product_name, price, image_url. These render directly inside messages.
  • Branching values for filters: plan_tier, is_first_purchase, payment_method.
  • Numeric values for entry rule property filters: amount, quantity, score.

Data types

Pick the right type up front. Journey filters and Liquid behave differently across types.
TypeUse whenExample
StringCategorical or identifier values"premium", "prod_3342"
NumberValues you’ll filter or compare49.99, 12
BooleanBinary flagstrue, false
Datetime (ISO 8601)Time-of-event values used in Liquid date filters"2025-04-30T14:22:00Z"
ArrayRepeating items rendered with for-loops["push", "email"]
Sending numbers as strings is the most common Journey filter bug. Numeric comparisons like >, <, and >= require numbers. A price: "49.99" will not match price > 25. Always send numbers as numbers.

What to avoid

  • PII in properties. Names, emails, phone numbers, payment details, and sensitive health or auth data should not travel inside event properties. Use anonymized internal identifiers instead.
  • Properties you don’t reference. They eat into the 2024-byte event payload limit and clutter analytics. See Custom Event structure.
  • Inconsistent naming across events. productId in one event and product_id in another will silently break Wait Until’s Event Matching.

Pattern 1: React in real time with an entry trigger

Use this pattern when the event itself is the moment you want to message the user. The Journey enters, sends one or two personalized messages immediately, and exits. Use it for:
  • Order or appointment confirmations
  • Course or task completions
  • Account creation, plan upgrades

Example: Course completion celebration

A learning app fires course_completed whenever a user finishes a course. The Journey congratulates them and surfaces what to take next. Event payload:
JSON
{
  "name": "course_completed",
  "properties": {
    "course_id": "intro_to_python",
    "course_title": "Intro to Python",
    "completion_score": 92,
    "next_course_title": "Python for Data Analysis",
    "next_course_url": "https://example.com/courses/python-for-data-analysis"
  },
  "external_id": "user_12345",
  "timestamp": "2025-04-30T18:42:00Z"
}
1

Set the Journey entry rule

In Journey settings, under Entry rules, choose Custom events and set the event name to course_completed. Leave Filter by property unset so every completion enters the Journey. See Custom event entry rules.
2

Add a celebration push step

Reference the event payload directly with Liquid:
Liquid
You finished {{ journey.first_event.properties.course_title | default: "your course" }}! Score: {{ journey.first_event.properties.completion_score }}/100. Ready for what's next?
3

Add a follow-up email

Recommend the next course using the event’s recommendation properties.Subject: Up next: {{ journey.first_event.properties.next_course_title }}Body excerpt:
Liquid
You earned {{ journey.first_event.properties.completion_score }}/100 in {{ journey.first_event.properties.course_title }}. Keep your streak going. Start {{ journey.first_event.properties.next_course_title }} now.
Set the call-to-action button URL to {{ journey.first_event.properties.next_course_url }}.
Why this works. The event payload carries everything the messages need. There’s no segment lookup, no follow-up event, and no branching. The Journey is a personalization wrapper around a single triggering moment.

Pattern 2: Wait for a follow-up event with Event Matching

Use this pattern when the entry event sets the stage but the actual decision depends on whether a second event fires within a window. Event Matching ties the wait event to the entry event so concurrent Journey instances stay scoped to the same workflow (the same order, the same workspace, the same conversation). Use it for:
  • Order tracking with a delivery SLA
  • Trial-to-feature-activation funnels
  • Quote-to-acceptance windows
  • Any “did the expected next step happen?” question

Example: Food delivery order tracking

A food delivery app enters users into an order Journey when order_placed fires. If order_delivered fires within 90 minutes for the same order_id, the Journey sends a review prompt. Otherwise, it sends a delay-handling support message. Entry event (sent when the user places the order):
JSON
{
  "name": "order_placed",
  "properties": {
    "order_id": "ord_88412",
    "restaurant_name": "Marcello's Pizzeria",
    "estimated_delivery_minutes": 45
  },
  "external_id": "user_12345"
}
Wait Until event (sent by the courier system when delivery is confirmed):
JSON
{
  "name": "order_delivered",
  "properties": {
    "order_id": "ord_88412"
  },
  "external_id": "user_12345"
}
1

Set the entry rule

Custom events entry rule on order_placed. No property filter, since every order should enter the Journey.
2

Add a Wait Until on the delivery event

Add a Wait Until step:
  • Condition: Custom Event order_delivered
  • Event Matching:
    • Trigger Event Property: order_id
    • Wait Event Property: order_id
  • Expiration: 90 minutes
The Event Matching binding ensures that if the same user places two concurrent orders, each order’s Journey instance only advances when that order’s delivery event fires.
3

Configure the event branch

On the event branch (delivery happened in time), send a review prompt:
Liquid
How was your order from {{ journey.first_event.properties.restaurant_name }}? Tap to leave a review.
You can use journey.last_event here if you want to reference properties on the matched order_delivered event. The full reference is in Custom Event Liquid reference.
4

Configure the expiration branch

On the expiration branch (90 minutes elapsed without order_delivered), send a delay-handling message:
Liquid
Your order from {{ journey.first_event.properties.restaurant_name | default: "the restaurant" }} is taking longer than expected. Tap to track it or reach support.
This branch references journey.first_event because the wait event never fired. journey.last_event would point to the same order_placed record in this case.
Why Event Matching matters. Without it, the Wait Until would advance on any order_delivered event for that user. If the user has two open orders, the first delivery would prematurely complete both Journey instances. Binding to a shared order_id keeps each instance scoped to its own context.

Pattern 3: Wait for a threshold or milestone event

Use this pattern when the question is “did the user accumulate enough behavior?” Total spend, total sessions, total invitations sent. Wait Until matches discrete events, not aggregates, so the right way to model thresholds is to fire a discrete event from your backend the moment a threshold is crossed. Use it for:
  • Lifetime value tiers (silver, gold, platinum)
  • Engagement milestones (10 workouts, 50 lessons, 100 transactions)
  • Funded-account thresholds
  • Streak or consistency achievements

Example: 30-day VIP loyalty Journey

An ecommerce app enters new users into a 30-day loyalty Journey on signup_completed. When cumulative spending crosses a threshold, the backend fires a derived loyalty_milestone_reached event. The Journey waits up to 30 days for that event to land and routes accordingly. Backend-fired milestone event (sent the first time the user crosses the threshold):
JSON
{
  "name": "loyalty_milestone_reached",
  "properties": {
    "tier": "vip",
    "amount_spent_cents": 25000,
    "currency": "USD"
  },
  "external_id": "user_12345"
}
1

Send the milestone event from your backend

Track cumulative spend in your own data store. Each time a purchase succeeds, recompute the running total. The first time it crosses the threshold, fire loyalty_milestone_reached to OneSignal.Fire this event once per user per milestone. Use the idempotency_key field to deduplicate if your retry logic could re-fire it.
2

Set the entry rule

Custom events entry rule on signup_completed. Every new user enters the Journey at signup; the wait then determines what they receive.
3

Add a Wait Until on the milestone

Wait Until step:
  • Condition: Custom Event loyalty_milestone_reached
  • Expiration: 30 days
  • No Event Matching needed. The milestone fires once per user per signup, so there is no instance ambiguity to resolve.
4

Configure the event branch

On the event branch, send a “Welcome to VIP” email and a celebratory push using the milestone event’s properties:
Liquid
You just unlocked {{ journey.last_event.properties.tier | upcase | default: "VIP" }} status. Your perks are live now.
5

Configure the expiration branch

On the expiration branch (30 days elapsed without the milestone), send a motivating nudge that does not commit to a specific dollar amount, since the cumulative spend lives in your backend rather than the Journey instance:
Liquid
You're closer to VIP than you think. See what's waiting for you.
If you want the message to show exactly how much further the user has to go, fire a second derived event like loyalty_progress_check with a cents_to_next_tier property and add another Wait Until earlier in the Journey to capture it.
Why a derived event beats raw event filtering. You could try to model thresholds with raw purchase events and an entry rule property filter, but that only catches single purchases above a threshold, not cumulative spending. Cumulative state lives in your backend. Fire a discrete event the moment the state crosses a meaningful boundary, and let the Journey react to that boundary event.

Personalize messages with event properties

Every Journey instance stores the events that drove its entry and any Wait Until matches. Reference them in templates with Liquid:
Liquid
{{ journey.first_event.properties.product_name }}
{{ journey.last_event.properties.amount }}
{{ journey.event.signup_completed.properties.plan }}
  • journey.first_event is the entry-triggering event.
  • journey.last_event is the most recently matched event (entry or any Wait Until).
  • journey.event.EVENT_NAME returns the most recent matched event with a specific name.
For nested property access, default fallbacks, arrays, and the full reference, see the canonical Custom Event Liquid reference.

Personalize with Custom Events

Full Liquid reference, nested property access, for-loops over event arrays, and worked examples.

Optimization checklist

Work through this list as you ship more event-driven Journeys.
  • Use Event Matching wherever the same user can have concurrent instances. Orders, conversations, surveys, support tickets. Bind Wait Until events to a shared identifier so each instance advances independently. See Event Matching.
  • Always pair Wait Until with a meaningful expiration. A wait without a thoughtful expiration leaves users in limbo. Decide what message, or no message, should fire if the expected event never arrives.
  • Reuse the same event for entry and exit on repeating signals. Cart updates, address changes, plan changes. When a triggering event repeats, set the same event name on the entry rule and the exit rule. The user exits the current instance and re-enters with the latest properties. See Exit when Custom Event condition occurs.
  • Watch the 100-property storage cap. Each Journey instance stores at most 100 event properties across all matched events. Long-running Journeys with many Wait Until matches can hit the cap.
  • Stay under the 2024-byte event payload limit. Strip properties you won’t reference. Move large content like HTML, base64 images, or full carts behind a deep link or a backend lookup.
  • Verify events with the Custom Events Activity feed before activating. The dashboard’s Event Activity tab shows events as they land. Use it to confirm property names, types, and values match your Journey configuration before turning the Journey live.

FAQ

When should I use a Custom Event entry rule vs. a segment-based entry rule?

Use a Custom Event entry rule when the trigger is a discrete user action (the user just placed an order). Use a segment-based entry rule when the trigger is a state (the user has been inactive for 7 days). You cannot combine the two on the same Journey, but you can still reference Custom Events inside a segment-based Journey via Wait Until steps.

Can a single user be in the same Journey multiple times?

Yes. Custom Event entry rules allow concurrent instances by default. Each order_placed for the same user starts its own instance with its own stored event. To prevent overlap, either filter the entry rule by a property or set the same event name as both the entry rule and the exit rule, which exits the current instance before the new one begins. See the Custom Event entry rule warning.

Why is my Wait Until step matching the wrong event?

A Wait Until without Event Matching advances on any event of the configured name for that user. If the user has multiple concurrent Journey instances or fires the event from an unrelated workflow, the Wait Until completes prematurely. Bind it to a shared property like order_id or workspace_id so each instance scopes to its own context.

How do I model thresholds like “spent more than $100”?

For per-event thresholds (this single purchase was above 100),useapropertyfilterontheentryrule.Forcumulativethresholds(lifetimespendcrossed100), use a property filter on the entry rule. For cumulative thresholds (lifetime spend crossed 100), compute the cumulative state in your backend and fire a derived event the moment the threshold is crossed. Wait Until matches discrete events, not aggregates.

What happens to the stored event when a user exits a Journey?

The stored event is cleared. If the user re-enters the same Journey, a fresh journey.first_event is captured from the new entry trigger. See Event property storage rules.

Do I need to send Custom Events from a mobile or web SDK?

No. You can send Custom Events from any source: SDKs, the Create Custom Events API, or one of OneSignal’s data integrations. All sources are treated identically inside Journeys.

Custom Events

Send events from your SDK or REST API, configure retention, and verify events in the dashboard.

Personalize with Custom Events

Full Liquid reference for journey.first_event, journey.last_event, and journey.event.EVENT_NAME.

Journey actions

Wait, Wait Until, Time Window, Yes/No branch, Split Branch, and Tag User reference.

Journey settings

Configure entry rules, exit rules, re-entry, and scheduling.

Welcome Journey: Mobile gaming

Worked example of a 7-day onboarding Journey using a daily_session Wait Until.

Abandoned cart tutorial

End-to-end ecommerce Journey using cart events for entry, exit, and personalization.