Skip to main content
Send the right message on the right channel when a user does something in your product, then stop the moment they convert. Each lifecycle moment becomes one Journey. Your app sends one event, and Courier runs the waits, the checks, and the channel choices.

See what you’ll build

A trial lifecycle that starts when a user starts a trial and ends when they upgrade: The same pattern covers most lifecycle messages. Map each product event to its own Journey: Keep the decision logic in the Journey, not in your app. A product manager can then change timing and copy without a deploy. For the email-only version of this pattern, see Build an onboarding sequence.

Prerequisites

Build the Journey

You don’t need every channel to follow along. Leave out the send node for any channel you haven’t connected, and the rest of the Journey still runs. If you leave out push, drop the one-hour delay, the fetch after it, and the SMS node’s send_status condition too, since that condition reads the push node’s status.
1

Create the Journey and its trigger

Open Journeys, select New Journey, name it trial-lifecycle, and choose the API Invoke trigger.Add three fields to the trigger schema. Every node downstream can read them:
  • plan: the plan the user is trialing
  • reminder_at: when to send the reminder, as an ISO 8601 datetime. Your app computes it, for example three days before the trial ends
  • trial_ends_at: when the trial ends, as an ISO 8601 datetime
Keep reminder_at in the future and at least two hours before trial_ends_at. That leaves room for the push, the one-hour SMS fallback, and the last-day email to go out in order. For a trial too short for that, move reminder_at closer to the start.If you already send events through Segment, pick the Segment trigger instead, and the event’s properties become the run’s data. See Start a journey from Segment. Segment can deliver an event more than once, and a Segment-triggered run has no idempotency key, so a repeat starts a second run. If duplicates matter for this flow, invoke the Journey from your backend with an idempotency key instead.
2

Welcome them in the app and by email

Add a send node, choose In-app / Inbox, and select + Create to write the message inline. Reference the run’s data with variables such as {{plan}}.Add a second send node on the Email channel with the same welcome and a link to your most important setup step.Each send node owns a template scoped to this Journey. A run sends each template’s published version, so publish every template before you publish the Journey.
3

Wait until the reminder time

Add a delay node, set the interval type to Until, and turn on Dynamic Interval. Select data.reminder_at as the field.Each user’s run now waits until their own reminder time, so your code holds no timers. For a gap longer than one delay covers, chain delays.
4

Check whether they already upgraded

A run works from a snapshot of the data it started with, so data.plan still says the trial plan a week later. To read the user’s current state, add a fetch data node that calls your own API, such as https://api.example.com/users/{{recipient}}/subscription. The response merges into the run’s data.Return the status in a field the trigger doesn’t set, such as subscription_status. The default Soft Merge never overwrites a field the run already has, so a response field named plan would leave the stale value in place.Protect the endpoint. Turn on Headers in the fetch node and send a secret, such as Authorization: Bearer <your token>, that your endpoint checks.A failed fetch ends the run. A non-2xx response, a redirect, or no answer within 10 seconds stops the run with an error, and that user gets no reminders. Have the endpoint return 200 for every user in a trial, including ones it doesn’t know yet.Then add a branch node. Give the first path the condition data.subscription_status is equal active, and wire it to an exit. The default path continues to the reminders.
5

Remind them on push, and fall back to SMS

On the default path, add a send node on the Push channel with a short reminder. A user with no device tokens gets no push, and their message ends as UNROUTABLE. Any status short of DELIVERED passes the SMS condition below, so they get the SMS.Add a delay of one hour after the push node. Your app reports delivery after the device receives the push, so a check made right after the send would find most pushes not yet delivered.After the delay, add a fetch data node that calls the same subscription endpoint with merge strategy Overwrite, so an upgrade during that hour is seen.Then add an SMS send node with the same reminder and two conditions in one group, so both must hold: send_status.<push node ID> was not DELIVERED, and data.subscription_status is not equal active. The node ID is the push node’s id. Over the API, it comes back in the response when you create or replace the Journey, and it stays the same when you send the nodes back with their IDs. GET /journeys/{templateId} returns it once the Journey is published. Users still on the trial whose push didn’t arrive within the hour, including users without your app, get the SMS. Everyone else skips it.This step depends on push delivery tracking. A push reaches DELIVERED only when your app reports it, through a Courier mobile SDK or the payload’s trackingUrl. Without that, every push stays at SENT and every user also gets the SMS.To keep reminders in business hours, compute reminder_at in the user’s daytime rather than adding a send window. A send window holds delivery until it opens, so a push held overnight would still be undelivered when the one-hour check runs.
6

Send the last-day email

Add a delay set to Until with Dynamic Interval on data.trial_ends_at.Check the subscription again, since the user may have upgraded after the reminder. Add a second fetch data node that calls the same endpoint, and set its merge strategy to Overwrite. The default Soft Merge would keep the status the first fetch stored. Then add a branch on data.subscription_status is equal active that exits, as before.On the default path, add an Email send node that tells the user what they keep if they upgrade today.To learn which version converts better, turn this node into an experiment and compare opens and clicks per variant.
7

Set a cancelation token and publish

In the Journey’s settings, set the cancelation token to trial-{{recipient}}. Each user’s run carries its own token, so you can stop one user’s messages without touching anyone else’s.Select Publish, then copy the Journey ID from the editor. New runs use the published version, and runs already in flight finish on the version they started with.

Trigger it from your app

1

Invoke the Journey when the trial starts

Call POST /journeys/{templateId}/invoke from the code that starts the trial, with the user and the trigger data. Replace trial-lifecycle with the Journey ID you copied, or with the Journey’s alias if you set one.Compute reminder_at and trial_ends_at from the user’s real trial as ISO 8601 strings. The samples use four and seven days from now. In production, set the reminder to a daytime hour in the user’s time zone. Keep reminder_at within one delay’s limit of the invoke, and trial_ends_at within that limit of reminder_at.
The idempotency key makes a retried request return the same run instead of starting a second sequence. Build it from the trial’s ID, not the user’s, so a user who starts a new trial later gets a new run. Courier keeps a key for 25 hours by default. See Idempotency to hold it longer. The response carries a runId. Store it to inspect or cancel this exact run later.
2

Stop the messages when they upgrade

In your upgrade handler, call POST /journeys/cancel with the user’s token. Pending steps never fire, and anything already delivered stays delivered.
If this call is missed, a subscription check runs before the push, the SMS, and the last-day email, so a user who upgraded gets none of them.

Verify

1

Run it on a short clock

Duplicate the Journey for testing, set the copy’s delay after the push node to 2 minutes, and change the copy’s cancelation token to trial-test-{{recipient}} so a test cancel can’t stop a real run. Publish the copy’s templates and the copy, then invoke the copy’s Journey ID, not the original’s, for a test user with reminder_at a few minutes from now and trial_ends_at a few minutes after that. The whole lifecycle plays out in about ten minutes instead of two weeks.
2

Follow the run

Open the Journey’s Logs tab and select the run. Each node’s outcome is drawn on the graph. Click a node to see its input data, the profile it read, the conditions it evaluated, and its output. Over the API, GET /journeys/runs/{run_id}/steps returns the same steps.
3

Check each path

Run it for a test user on a device that reports delivery and confirm the SMS node is skipped. Run it for a user with no device tokens and confirm the SMS goes out. Then have your test endpoint return subscription_status: active and confirm the run exits at the branch.
4

Confirm a cancel stops the run

Invoke it once more, then cancel with the token trial-test-<user ID> while the run waits at a delay. Open the run in the Logs tab and confirm it reads Canceled. A cancel by token answers the same way whether or not it matched a run, so the run’s status is the proof.

Measure what’s working

  • Per message: Analytics shows delivery, open, and click rates for each Journey.
  • Per path: in the run logs, compare how many runs exit at either upgrade branch with how many reach the last-day email.
  • Per variant: an experiment on any send node compares versions of one message.
  • In your own systems: an outbound webhook forwards delivery events so you can join them to conversion data.

Fix common problems

Only a published Journey has a version to run. Publish each send node’s template, then publish the Journey, and invoke again with its Journey ID.
A run’s data is a snapshot taken at invoke, so later edits to the user’s profile don’t reach it. Read anything that changes during a wait with a fetch data node right before the branch.
The fetch got a non-2xx response, a redirect, or no answer within 10 seconds. The step’s error message in the run logs names the cause. Return 200 for every user the Journey can reach.
A node whose conditions aren’t met is skipped, and the run logs show which condition failed.
A token that references a variable missing at run start leaves the run untagged. That run is still cancelable by run_id.
The push never reached DELIVERED, so the SMS condition passed. Check the push in Logs. A push stuck at SENT means your app isn’t reporting delivery. See push tracking.
Each invoke without an idempotency key starts a new run. Key the request on the trial, such as trial-started-trial_456, so a retry returns the existing run and a later trial still starts its own.
Every node type, and what each one accepts, is in the nodes reference.