Browse the docs

SendFlow

Settings

Triggers, named exits, send windows, re-entry, frequency caps and enrolment scope.

Settings describe the shape of a campaign — who comes in, who goes out, and the rules that apply throughout. They all sit at the top of the workflow, before the first statement.

SendFlow
workflow "Renewal" v1 {
  enter on 3d before attr.renewal_date where attr.plan = "pro"
  exit "renewed" when exists(activity.invoice_paid within 7d)
  send window 9am-6:30pm in contact timezone
  reentry per occurrence
  frequency cap 4 per 7d
  enroll existing since 30d

  send "renewal-notice" via topic "billing"
}

Putting a setting after a statement is a parse error, not a warning. Their order among themselves does not matter, and the formatter preserves whatever order you chose.

enter — the trigger

Exactly one, always. A workflow with no enter can never start, and one with two has no defined beginning; both are errors.

There are four kinds of trigger.

A segment. The contact enters when they match a saved SendQL segment.

SendFlowSyntax
enter on segment "trial-started"

An activity. The contact enters when they do something in your product. This is the natural trigger for most lifecycle campaigns.

SendFlowSyntax
enter on activity.cart_updated

An event. The contact enters on an email event — one of the eight built-ins. Legal, but usually a deliverability alarm rather than a campaign.

SendFlowSyntax
enter on event bounce

A date, relative to an attribute. The contact enters a fixed duration before or after one of their own datetime attributes.

SendFlowSyntax
enter on 3d before attr.renewal_date
enter on 1d after attr.trial_ends_at

This is the only way to act on a date in the future, and it is the reason SendQL has no need to express one. Each contact is enrolled relative to their date, not a global one.

Any trigger can take a where filter, which is a full SendQL predicate:

SendFlowSyntax
enter on activity.signup where attr.plan = "free" and not suppressed

exit — named exits

Any number, including none. An exit is a goal: a predicate that is checked before every single step, and the moment it is true, the contact leaves.

SendFlowSyntax
exit "converted" when attr.plan != "trial"
exit "purchased" when exists(activity.order)

The name is not decoration — it is what conversion reporting counts. Two exits may not share a name.

An exit fires wherever the contact happens to be: mid-wait, inside a repeat, between two sends. This is what makes them worth using over a defensive if before each send. Declare the goal once.

send window

At most one. Restricts sends to a window in the contact’s own timezone, so nobody gets a 3am marketing email.

SendFlowSyntax
send window 9am-6:30pm in contact timezone

Times are 12-hour with an am/pm suffix; minutes are optional (9am and 9:00am are the same thing, and the formatter will normalise the second to the first). A window that opens and closes at the same time is a warning — it is almost certainly a typo for “all day”.

reentry

At most one. What happens when a contact who has already been through the workflow qualifies again.

FormMeaning
reentry onceNever re-enter. The default when undeclared.
reentry on rematchRe-enter if they stop matching and then match again.
reentry per occurrenceRe-enter every time the trigger fires.

per occurrence is what you want for a cart-abandonment flow — a contact can abandon many carts. once is what you want for onboarding.

frequency cap

At most one. Overrides your organisation-wide sending cap for this workflow.

SendFlowSyntax
frequency cap 4 per 7d
frequency cap off

Two subtleties worth getting right:

  • Counting stays cross-workflow. The cap changes this workflow’s threshold and window, not what gets counted against it. Messages from your other campaigns still fill the contact’s quota.
  • off exempts the workflow entirely, which is what a time-critical transactional stream needs. Use it sparingly and deliberately.

Undeclared, your organisation’s default applies.

enroll — enrolment scope

At most one. Whether contacts who already match are back-enrolled when the workflow is switched on.

FormMeaning
enroll forwardOnly contacts who match from activation onward. The default.
enroll existingAlso back-enrol everyone who already matches — a one-time backfill.
enroll existing since 30dBack-enrol, but only as far back as the window.

The default is forward-only, for every trigger kind, and it is worth pausing on. Switching on enter on segment "all-users" does not mail every current member of that segment — it enrols only the contacts who match from that moment onward. Back-enrolling your history is opt-in, precisely because the alternative is a campaign that sends to your entire list the second you turn it on.

If you do want the backfill, bound it:

SendFlowSyntax
enroll existing since 30d

enroll existing with no since on a date-relative trigger is an error, because an unbounded backfill there would enrol every contact who has ever had an anchor date — which is all of them, forever.

The full set

SettingCardinalityDefault when undeclared
enter on …exactly one— (required)
exit "<name>" when …any numberno goals
send window …at most onesend at any hour
reentry …at most oneonce
frequency cap …at most oneyour org-wide cap
enroll …at most oneforward