Chapter 2
Build a customer journey in Courier: API and Segment triggers, send nodes, branching, delays, fetches, throttling, the AI node, experiments, and run inspection.

Last updated: July 2026
Journey design happens on a visual canvas. You drag nodes from the palette onto the graph, connect them, and configure each one in a side panel. Execution runs from the trigger downward: a node runs, then hands off to the next. Because the whole flow (branches, delays, copy, channels) lives on the canvas, product and lifecycle teams change it without a deploy, and engineers keep the trigger as the only integration point.

A journey starts one of two ways, and most products use both.
API triggers invoke a journey directly from your backend, for flows tied to your own application logic. Trigger it with the Courier SDK:
import { CourierClient } from "@trycourier/courier";const courier = new CourierClient({ authorizationToken: process.env.COURIER_API_KEY });const { runId } = await courier.journeys.invoke("trial-onboarding", {user_id: "user_123",data: { plan: "enterprise", trial_days: 14 },});
The call returns a runId and a 202. Courier validates data against the trigger's schema and walks the graph asynchronously. SDKs are available for Node.js, Python, Go, Ruby, Java, PHP, and .NET; see the SDK overview.
Segment triggers start a journey when a matching Segment event arrives, so you instrument your product once and route behavioral events to journeys without writing new backend calls. Use API triggers for transactional flows, Segment triggers for behavioral lifecycle journeys.
Add Courier as a Segment destination:
Your product's existing calls then flow to Courier:
analytics.track("trial_started", {plan: "enterprise",trial_days: 14,});
The event name (trial_started) becomes a trigger you can select, and the properties become data your journey branches on and personalizes with. identify calls keep profile traits current; group calls associate users with organizations for account-based journeys. RudderStack works the same way through its Courier destination.
A send node delivers one message on one channel. When you add it, you pick the channel (email, SMS, push, in-app, Slack, Teams) and build the template right there. Each channel gets the controls it needs: email has subject lines and HTML, SMS counts characters, Slack uses Block Kit. Omnichannel delivery works by combining send nodes with a branch that falls back to whichever channel a user has: Slack if a profile.slack_token exists, else push, else email.
Templates use Handlebars to pull from trigger data and the user profile, so Hi {{profile.first_name}}, your {{data.plan}} trial has {{data.trial_days}} days left renders per recipient. Conditionals let one template serve many segments:
{{#if (condition (var "profile.plan") "==" "enterprise")}}Your enterprise features are active.{{else}}Upgrade to unlock enterprise features.{{/if}}
A branch node evaluates conditions and routes each user down the first matching path, with a default path for everyone else. Conditions read from three sources: trigger data (data.setup_completed), profile attributes (profile.company_size), and results from earlier sends (refs.welcome_email.status).
| Condition type | Operators |
|---|---|
| Equality | is equal, is not equal |
| Text | contains, does not contain, starts with, ends with |
| Number | greater than, greater than or equal, less than, less than or equal |
| Presence | exists, does not exist |
Group conditions with AND or OR, and nest groups for more complex logic. A common pattern: after a welcome email and a three-day delay, branch on setup completion. Setup done routes to advanced tips; not done routes to setup help. Step references keep you from re-nudging someone who already engaged, so a branch on refs.welcome_email.status equal to CLICKED skips the reminder.
A delay node pauses a run in one of two modes:
PT1H for one hour, P3D for three days).Courier uses profile.timezone for timezone-aware delays and defaults to UTC when none is set. Pair delays with quiet hours so routine messages land during business hours and only critical ones override.

A fetch data node makes an HTTP request mid-run and merges the response into the run's data, so a branch downstream decides on live state instead of stale trigger data. Configure the URL (with interpolation like https://api.yourapp.com/accounts/{{profile.account_id}}/health), the method (GET, POST, PUT, or DELETE), and auth. How the response combines with existing run data depends on the merge strategy:
| Strategy | Behavior |
|---|---|
overwrite | Deep-merges the response into run state; response values win on conflict. |
soft-merge | Adds only fields that don't already exist; keeps existing values. |
replace | Replaces the entire run state with the response. |
none | Discards the body (fire-and-forget). |
Add a branch after the fetch to handle failures: route users whose data.health_score exists one way and failed fetches another.
A throttle node limits how often a run passes a point within a window. Scope it per user (cap one user's sends), global (cap total volume across users), or dynamic (cap by a key you pass, like account ID). Set a max_allowed and a period, and a burst that exceeds the cap is suppressed.
Batching and digests roll a burst of events into one message: instead of 20 comment alerts in an hour, a user gets a single summary.
The AI node runs a frontier model inside the journey and returns structured data the rest of the flow acts on. Pick a model (GPT-5.5, Claude Opus 4.8, Claude Fable 5, and other OpenAI and Anthropic models), write a prompt with {{variable}} interpolation from journey data, and define an output schema (form mode for simple fields, JSON mode for complex shapes). The response is parsed to that schema and merged into the run.

Four things it does without an external service:
Web search is available for Anthropic models; it adds two credits per run and counts toward input token usage. Test the node in the editor with sample inputs, and run inspection shows the full prompt, the raw response, and the parsed output.
A send node can run an experiment instead of a single template: it holds 2 to 10 template variants and splits traffic across them, so you can compare subject lines, copy, or layouts on one send without branching the journey. Each variant is a full template you edit inline, and each carries a relative weight, so weights of 60/30/10 keep most traffic on your current template while testing two alternatives.
Courier assigns recipients with a bucketing key you pass in your data, like a user or workspace ID: it hashes the key so the same recipient always lands in the same variant. Assignments stay sticky when you change weights, and only re-bucket if you add or remove a variant or change the key.
The Results and Metrics views break delivery, open, and click data out per variant. When you've seen enough, you promote a variant: its template becomes the only one on the node and the experiment ends. Promotion is a manual decision; Courier doesn't pick a variant for you.
Before publishing, use test mode: provide sample event data and watch each node execute, so you can confirm a branch routes a user who finished setup differently from one who didn't. After publishing, run inspection steps through a real user's run node by node, showing the data at each step and where it errored.

Message logs show delivery status, channels attempted, and errors, searchable by user, template, or journey. The common failures:
Analytics roll up completion rates, drop-off points, branch usage, and per-channel engagement, so you can see where a journey loses people. Chapter 4 turns these mechanics into full lifecycle blueprints; Chapter 3 covers what to look for in a platform.
Yes. The Journeys API lets you create a journey, define its templates, wire the node graph, publish it, and invoke runs from your backend, and the canvas and API act on the same journey. Teams often design on the canvas and invoke from code, or manage the whole lifecycle programmatically when journeys are generated or kept in version control.
Configure Courier as a destination in your Segment workspace, and Segment forwards track, identify, and group calls automatically. Track events appear as triggers you can map to a journey, with no webhook code on your side. The Segment integration guide covers the full setup.
Cancel the run. Configure the journey to cancel when a specific event fires, for example trial_converted, and Courier stops all pending steps for that user immediately. This keeps you from messaging someone who already did the thing you were driving.
Open run inspection and find the user's run; each node shows whether it's pending, completed, or errored, along with the data it saw. The usual causes are a profile with no contact info for the target channel, a user who opted out of the topic, or a branch condition that routed them away from the send node.
Yes. Each user has an independent run with its own state, so one user on day 3 has no effect on another starting day 1. Throttle nodes and cross-journey frequency caps control how often any single user is reached.
Previous chapter
Customer Journey Management Explained
Customer journeys trigger messages from user behavior, not a schedule. Learn how they differ from campaigns, why they matter, and the nodes that build them.
Next chapter
Choosing Your Journey Management Platform
What to look for in a customer journey platform: real-time triggers, a visual builder the whole team can use, channel breadth, and provider flexibility.
© 2026 Courier. All rights reserved.