Skip to content

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

TriggerFires whenUse for
On subscribe (signup)a contact joins the audiencenew‑user onboarding
When a tag is added (tag_added)you apply a tag to a contactinvite 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 todayrenewals, 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

StepDoes
✉ Send emailtemplate + subject + body (merge tags like {{firstName}} work)
⏱ Waitpause N minutes / hours / days before the next step
⧉ Conditiona segment‑style test that routes the run — pick what happens when it matches vs doesn't
🏷 Apply tagtag 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:

BranchDoes
Continue to next stepfall through to the step below
Skip the next stepjump 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 Wait before 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:

SignalHowNotes
Opened / clicked this journey's previous emailthe 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 / activatedtag 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 Wait step 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.