Build a drip / onboarding automation
Run a triggered, multi-step email sequence — onboarding new signups, a beta-wave invite with an automatic follow-up when no action is taken — on a per-person clock, from your own Cloudflare account.
An Automation (a journey) is a triggered, multi-step email sequence that each contact walks on their own clock. Where a newsletter is one blast to everyone at once, an automation enrols people as they qualify and sends them a sequence relative to their entry — so a user who signs up today and one who signs up next week each get day‑0 / day‑2 / day‑5 from their moment.
It lives under Lists & newsletters → Automations. It is warm by construction (🌱): it sends from your warm domain to opted‑in contacts. (Cold outreach to strangers is a different surface — see Run outreach.)
The model: trigger + steps
A journey is exactly two things:
Automation
├─ trigger : signup | tag_added | api_event | date_based (who enters, and when)
└─ steps : [ email · delay · condition · tag ] (run in order, per person)
Triggers — who enters
| Trigger | Fires when | Use for |
|---|---|---|
On subscribe (signup) | a contact joins the audience | new‑user onboarding |
When a tag is added (tag_added) | you apply a tag to a contact | invite waves, segments, lifecycle |
API event (api_event) | you call journeys.trigger (or enrol by email in the UI) | app events — activated_workspace, purchased, … |
Date‑based (date_based) | a contact's merge‑field date matches today | renewals, trial‑ending, birthdays |
A contact enters a journey once (a journey_runs row, currentStep = 0)
unless the journey allows re‑entry. The send happens on the next advancer tick,
through the same idempotent ledger as every other send — so a journey can
never double‑send.
Steps — what happens
| Step | Does |
|---|---|
| ✉ Send email | template + subject + body (merge tags like {{firstName}} work) |
| ⏱ Wait | pause N minutes / hours / days before the next step |
| ⧉ Condition | a segment‑style test that routes the run — pick what happens when it matches vs doesn't |
| 🏷 Apply tag | tag the contact (drive other journeys / segments off it) |
Condition routing — the key to follow‑ups
A condition tests the contact, then takes a branch. For each of matches (passes) and doesn't match (fails) you choose one of:
| Branch | Does |
|---|---|
| Continue to next step | fall through to the step below |
| Skip the next step | jump past the next step and continue after it |
| Exit (stop the journey) | end the run for this person |
The defaults reproduce a simple gate — matches → continue, doesn't → exit — so old automations are unchanged. But because each branch is a choice, one condition can do an if/else: "if they acted, skip the nudge; if not, send it" is a single step. That is the whole follow‑up mechanic (Recipe 2).
Recipe 1 — Onboard new app users
Audience: your signups list. Trigger: On subscribe.
Automation: "New user onboarding" trigger: On subscribe
─────────────────────────────────────────────────────────────
1. ✉ Email "Welcome — here's your first win"
2. ⏱ Wait 2 days
3. ✉ Email "Set up your workspace" (merge: {{firstName}})
4. ⏱ Wait 3 days
5. ✉ Email "A tip you'll like"
Every new signup walks this independently. Activate it once; it runs forever for each new contact who subscribes.
Steps in the UI: Automations → New automation → name it, pick the audience, set the trigger to On subscribe → Save → add the steps (Send email / Wait / …) in the Configure page → Activate.
Recipe 2 — Beta wave + automatic follow‑up when no action is taken
This is the "send a nudge only to people who didn't act" pattern. The mechanic is wait → condition (skip on action) → follow‑up.
Audience: your contacts. Trigger: When a tag is added — beta-wave-3.
(Tag the wave‑3 people; they enter as you tag them.)
Automation: "Beta wave 3" trigger: tag_added "beta-wave-3"
──────────────────────────────────────────────────────────────────────
1. ✉ Email "You're invited: beta wave 3" 🌱 Warm [Track opens & clicks ✓]
2. ⏱ Wait 3 days ← give them time to act
3. ⧉ Condition opened the previous email? (nudge preset)
├─ opened → DID act → Continue is SKIPPED (no nag)
└─ didn't → did NOT act → Continue ↓
4. ✉ Email "Still time — your beta seat is waiting" ← the auto follow‑up
On the condition step, choose engagement mode → "opened the previous email" and the nudge preset (the editor sets the branches for you). Then:
- The
Waitbefore the condition is what makes it "no action taken yet." It gives the window — and tracking events only land after the email goes out. - The routing is the suppression. People who opened are skipped past the nudge; people who didn't get it. No inverted logic, no list to maintain.
- Pick what "action" means (see below) — engagement is the built‑in option; a tag is the certain one.
What counts as "action"?
Choose the signal the condition tests:
| Signal | How | Notes |
|---|---|---|
| Opened / clicked this journey's previous email | the condition's engagement mode — "opened the previous email" / "clicked the previous email" | Per‑journey, per‑run — it tests engagement with the email this sequence just sent, not audience‑wide. Turn on "Track opens & clicks" on that email step (off by default). |
| Actually accepted / activated | tag the contact, then condition on not_has_tag "activated" | Most reliable. Your app (or a journey step) tags the contact when they do the thing. |
Engagement mode (the built‑in follow‑up signal). On a condition step, switch it to "opened the previous email" (or clicked) and pick the nudge preset — the step then continues only for contacts who did not open/click, which is exactly the follow‑up cohort. Two caveats it surfaces inline:
- Turn on tracking on the email step you're measuring (the "Track opens & clicks" toggle). Opens are unreliable (Apple Mail Privacy + image prefetch inflate them); clicks are firmer but only fire if the email had a link.
- A
Waitstep MUST sit between the email and the condition — the sequence runs instant steps back‑to‑back, so a condition right after an email always reads "not opened yet." The wait is the window.
When you want certainty, use a tag. Open/click is a soft signal; if "acted" means a
real product action, have your app set a tag (activated_workspace, accepted_invite)
and condition on not_has_tag. Tag‑based works with tracking off and never mis‑reads.
"If they acted, send A; if not, send B"
A single journey can fork — the condition's two branches drive it:
1. ⧉ Condition "has acted"
├─ matches → Continue → 2. ✉ Email "A" (for actors)
└─ doesn't → Skip the next step → (jumps past A) → 3. ✉ Email "B"
2. ✉ Email "A — nice work, here's what's next"
3. ✉ Email "B — don't miss out"
Actors continue into email A; non‑actors skip A and land on email B. (For longer or many‑way branches — more than a skip‑one — split into separate tag‑triggered journeys: an Apply tag step plus a second journey keyed off that tag. The skip branch covers the common two‑way case in one place.)
Driving it from an app event (the api_event trigger)
For onboarding milestones (not just "subscribed"), enrol on a real app event:
// From your app/server, when the user does the thing:
await trpc.journeys.trigger.mutate({
journeyId: "jr_…",
email: "ada@acme.com", // or contactId
});
The call only enrols (a journey_runs row); the first email sends on the next
advancer tick, ledgered. Re‑triggering a contact already in a non‑re‑entrant journey
is a safe no‑op.
Watching it run
Open the automation → the Enrollments view shows the live per‑step funnel (how many people are parked at each step) plus the active / completed / stopped rollup. "Completed" = finished the sequence; "stopped/exited" = left early (a condition gate failed, or they unsubscribed). Pause freezes all runs and stops new enrolments; Activate resumes.
Things to know
- Warm only. Automations send from your warm domain to opted‑in contacts. Cold strangers are Outreach, a separate surface.
- One global suppression list. An unsubscribe/bounce suppresses the contact across every automation and campaign — a journey can't email a suppressed address.
- Per‑person clock. Waits are relative to each contact's
enrolled_at, not a wall clock — so the sequence is correct no matter when each person enters. - Idempotent. Every send goes through the shared ledger; re‑ticks and retries never double‑send.