Skip to content

Campaigns

Campaigns send email journeys driven by your event stream: a trigger event starts a journey, steps send emails after configurable delays, and an exit event ends it early (“goal met”). Delivery events flow back into your analytics as $email_* events, so a campaign’s funnel is queryable like any other data.

Journeys are currently linear: trigger → (delay → email) → (delay → email) → … Branching journeys, quiet hours and timezone-aware sending are on the roadmap.

Piece What it does
Trigger event An event name that starts a journey for the person who did it (e.g. cart_abandoned)
Trigger filters Optional conditions on the trigger event’s properties, combined with AND. Operators: exact, icontains
Audience cohort Optional cohort: only members can enter the journey
Steps Each step = delay + email (subject + body template). Step 1’s delay counts from the trigger
Exit event If the person does this before the journey ends, it stops (goal met) and no further emails send
Re-entry never (one journey per person, ever) or after N days (may re-enter once the previous journey finished ≥ N days ago)
Frequency cap Max messages to a person within a rolling window (in hours) — counted across campaigns, so overlapping journeys can’t pile up
  • Identified persons require verified events to trigger a journey. If identity verification isn’t passing for a user, their events still land in analytics but won’t start campaigns — sending email based on a forgeable event is exactly the failure mode verification exists for. Anonymous events can trigger normally.
  • Exit events don’t require verification. Exiting is the safe direction: better to lose one email than to send an embarrassing one.
  • For triggers that must be facts (payment received, subscription cancelled), send the trigger event server-side with a secret key.

Email subjects and bodies support two variable namespaces:

Hi {{person.first_name}},
you left {{event.item_count}} items in your cart.
  • {{person.x}} — the person’s traits at send time.
  • {{event.y}} — properties of the trigger event, frozen at trigger time. A journey remembers the event that started it, even if the person’s data changed since.

Values are HTML-escaped; unresolved variables render as empty strings. There is no logic (no conditionals or loops) — templates are substitution only.

  • Exactly-once per step. Every send claims a unique (journey, step) slot in a message ledger before talking to the email provider, and the send itself carries an idempotency key. Retries, crashes and concurrent workers can’t duplicate an email.
  • Suppression list. Hard bounces and spam complaints suppress the address permanently, automatically (soft bounces suppress after the third). Suppressed addresses are skipped at send time, and you can add manual suppressions in the panel.
  • Last-second exit check. Even after a send is scheduled, the dispatcher re-checks the journey state before sending — an exit event that arrived in between still stops the email.
  • Pause stops the clock, not the journeys. Pausing a campaign stops new entries and freezes progress; existing journeys keep their state. Reactivating processes overdue steps immediately (with all the exit/suppression checks). Pausing does not cancel journeys.

The provider’s delivery webhooks are converted into events attributed to the same person: $email_sent, $email_delivered, $email_opened, $email_clicked, $email_bounced, $email_complained — each tagged with the campaign, journey and step. The campaign detail page uses them for its funnel (sent → delivered → opened → clicked), and you can use them anywhere: cohorts of people who clicked, flags targeted at non-openers, and so on.

A campaign with an audience cohort can also be sent as a one-time broadcast to the whole cohort (respecting re-entry rules, suppression and frequency caps). Journeys are created in bulk and proceed through the same steps and guarantees.

The campaign editor can send a single step to an address you choose without creating a journey; test sends are excluded from frequency-cap counting.