Chapter 7
Most product messaging is a small number of shapes repeated with different content. Here are the common ones, each as a prompt and the flow it produces.

Last updated: September 2026
Most product messaging is a small number of shapes repeated with different content. Here are the common ones, each as a prompt and the flow it produces.
Transactional messages are triggered by something the user did and expected. A password reset, a receipt, a shipping notice. They're not subject to marketing opt-out, and they're urgent in a way that shapes the delivery rules.
Lifecycle messages are ones you decided to send. Onboarding nudges, feature announcements, win-backs. They're opt-in, and cadence matters because there's a real cost to sending too many.
The practical difference is that preferences are load-bearing on the lifecycle side and mostly not on the transactional side. Nobody opts out of password resets.
Four, and an agent will miss at least two unless you say so.
Always pass an idempotency key. Your code will retry on a network blip, and without a key the user gets two codes.
Use "single" routing with fallbacks, not "all". For an OTP, "all" means email and SMS and push simultaneously. Fallback order means try SMS, and if there's no phone number, use email.
Phone numbers in E.164. +15551234567, not (555) 123-4567.
Never batch or delay them. A password reset in a digest is not a password reset.
Password reset. The one to build first.
Password reset notification. SMS first with email fallback, single routing. Include a 10-minute expiry in the copy. Data is
reset_urlandfirst_name. Wire it to the reset handler and send a test to user_test.
One template, two channels, no journey needed. What agents get wrong: routing to "all", and putting the token in the subject line where it shows up in a notification preview.
One-time passcode. Same shape, tighter. Keep the code out of the subject, keep the SMS under one segment, and set a short expiry. Digits only in the SMS body, because some clients linkify anything that looks like a number.
Order confirmation. The first one that needs a loop:
Order confirmation template. Email with the order number, a line-item list, the total, and a tracking button. SMS is one line with the order number and tracking link. Data is
order_number,itemsas an array withname,quantity, andprice, plustotalandtracking_url.
The items array is where loop: earns its keep. Ask to see the rendered output on a three-item order, because line-item spacing is the thing that looks wrong first.
Receipt. Like a confirmation, but people keep it. Plain text needs to be readable on its own, because receipts get forwarded to accounting.
Dunning and failed payment. Transactional, but it's a sequence, so it's a journey: notify, wait two days, check whether the payment went through with a fetch node, escalate if not. This is where cancel nodes matter. When the invoice gets paid, the remaining runs should stop.
Appointment reminder. A journey anchored to a future timestamp rather than a delay from now. Use mode: "until" against the appointment time, minus your lead time.
Trial ending. Send a few days before, branch on whether they've converted. Chapter 6's AI node example is a richer version of this.
Security alert. New device, new location, password changed. Urgent, single routing, and the copy needs a clear "this wasn't me" path.
Account change. Email or phone updated. Send to the old address as well, because that's the one that catches an account takeover.
Usage threshold. "You've used 80% of your API calls." Triggered by your system, not the user, so guard against sending it repeatedly as usage hovers at the threshold.
Back-in-stock. Sits on the line. Requested by the user, so it's expected, but it's promotional enough that preferences should apply.
Onboarding sequence. The canonical one:
Build a journey called "new-signup-onboarding". Send the welcome template immediately, wait one day, then send a setup reminder only if the user's profile has
setup_complete = false. Publish it as a draft and add a function to this project that triggers it for auser_id.
Three nodes and a branch. Extend it by branching on whether the first email was opened, using the send_status conditions from Chapter 6.
Feature announcement. One-off to a segment. An audience rather than a list, so membership stays current.
Activity notification. "Sara commented on your design." Individually these are fine. At volume they're the reason people mute your app, which leads directly to:
Activity digest. Instead of twelve notifications, one summary:
Build a weekly digest journey: collect
comment_addedevents per user and send one email every Monday at 9am in the user's timezone.
Use a digest node when the recipient controls the schedule. Chapter 6's AI node summarizer pairs well here: the digest collects the events, the model writes the sentence that makes them readable.
Aggregated activity. The burst version. Someone gets eight likes in a minute and should get one notification. That's a batch node, releasing on volume or on a quiet period.
Re-engagement.
Re-engagement journey for users with no login in 14 days: an email, wait 3 days, then a push if the user has a token, then stop.
Note the "if the user has a token" branch. Without it you generate failed sends for everyone on web.
Referral. Two-sided, which means two flows: the invite, and the "your friend joined" confirmation. Easy to build one and forget the other.
Promotional campaign. A one-off broadcast to an audience. The preference check matters most here.
Preference cadence. Not a message. A page where people choose what they get. Worth building early, because the alternative to a preference center is an unsubscribe.
One-off broadcast. Announcements, incidents, policy changes. Keep the template reusable so the next one is a data change.
Courier has no global per-user frequency cap. There's no setting that limits a person to N messages a week across every journey and send.
What exists is consolidation and consent:
Those cover most real fatigue problems, because most fatigue comes from one noisy source rather than a broad total. But if you need a hard cap across everything, you'd build it yourself against the message log. Don't assume the platform is doing it.
Both aggregate. They release differently.
Use a digest when the schedule belongs to the recipient. Weekly on Monday, daily at 9am, whatever they picked. The subscription topic holds the schedule and the template, not the journey.
Use a batch when the release belongs to the journey. Hold events until there are ten, or until nothing new has arrived for an hour.
Two gotchas:
wait_period shorter than max_wait_period. The first is the quiet window, the second is the ceiling from the first event.Lists are static. You add and remove people.
Audiences are rules. "Trial users who logged in this week" maintains itself.
One nuance worth knowing, because it produces a confusing bug: audience membership is pre-computed, not evaluated against every profile at send time. After you change a filter, a rebuild can take fifteen to twenty minutes on a large workspace, and sends during a rebuild can read partially updated membership. If you change a filter and immediately send, you may not get who you expect.
Everything above is an API object. So is everything a marketer builds by clicking.
That means a marketer can build a campaign in the dashboard, an agent can adjust it by API, and both are working on one object with one version history. No export, no rebuild, no "the real one is in the other tool."
This is the practical payoff of the API parity from Chapter 1. It's not that agents replace the UI. It's that nobody has to choose.
Per-template delivery metrics come back as a time series: sent, delivered, opened, clicked, errors, undeliverable, broken out by provider and channel.
courier notifications get-metrics --notification-id <TEMPLATE_ID>
Three things that will otherwise waste your time:
200 with an empty series, not a 404. A typo looks like "no sends yet."Only if you map them to a subscription topic. Courier checks preferences for anything mapped to a topic and skips what the user opted out of. Most teams deliberately leave password resets and receipts unmapped, because those should send regardless.
Pass an idempotency key on the send. If the same key arrives twice, Courier returns the stored response instead of sending again. Without one, a retry on a network timeout produces a second code and a confused user.
No. Use single routing with a fallback order so it tries SMS and falls back to email if there's no phone number. Sending both means two codes arriving, which is worse for security and for the person trying to log in.
A digest releases on a schedule the recipient controls, configured on the subscription topic. A batch releases on volume or a quiet period, configured in the journey. Digest for "every Monday," batch for "when eight of these pile up."
Preferences, digests, and batching, in that order. There's no global per-user cap in Courier, so if you need a hard limit across every flow, you'd build it against the message log yourself.
Yes, and this is one of the more useful things about the model. Journeys and templates are the same objects whether an agent or a person made them, so a marketer opens it in the dashboard and edits it normally.
Previous chapter
Designing Journeys with AI, and the AI Node
A journey is a multi-step flow that Courier runs on its own: send, wait, check something, branch, send again. The state lives on Courier's side, which is why a seven-day delay doesn't require a cron job or a row in your database tracking where everyone is.
Next chapter
Templates as Code: Environments, CI, and Governance
Building things with an agent is one problem. Making the result repeatable is another: content in version control, promotion between environments, and a way to answer "what did that actually do?" after the fact.
© 2026 Courier. All rights reserved.