Sequences (Drip Campaigns) Feature¶
Overview¶
Sequences (/sequences) is a multi-channel drip-campaign builder for automated, time-delayed outreach across Email, WhatsApp Lite (free-form dynamic messages), WhatsApp Cloud (Meta-approved templates), and SMS. It handles contact enrollment, per-step delays, sending windows, daily send caps, opt-out/unsubscribe compliance, reply-pause, deal-based exit conditions, and analytics — all organization-scoped (multi-tenant, RLS-enforced).
- Frontend: the platform, hooks the app, the app
- Runtime:
sequence-executoredge function (cron-driven) - Unsubscribe:
sequence-unsubscribeedge function (public, token-based) - Tables:
sequences,sequence_steps,sequence_enrollments,sequence_execution_log,sequence_daily_send_counts,sequence_unsubscribe_tokens
1. Sequence Builder¶
Sequence:
- Name + description
- channels (JSONB flags): email, sms, whatsapp_lite, whatsapp_cloud
- Status: draft, active, paused, archived — only active sequences are processed by the executor
- Optional sending window: sending_window_start / sending_window_end (HH:MM) resolved in sending_window_timezone → org settings.timezone → UTC
- Optional exit conditions: exit_on_deal_created, exit_on_deal_stages[]
Steps (sequence_steps, ordered by step_order):
- channel — email | sms | whatsapp_lite | whatsapp_cloud (legacy whatsapp treated as Lite)
- delay_minutes — wait after the previous step before this step fires
- message_body (required), subject (email), channel_account_id (which connected account sends it)
- WhatsApp Cloud: template_name, template_language, and optional template_variables[] (ordered map for placeholder resolution)
Variable substitution (message bodies / subject) uses single-brace tokens, replaced at send time from the contact:
| Token | Value |
|---|---|
{first_name} |
contact first name |
{last_name} |
contact last name |
{name} |
full name |
{email} |
primary email (emails[0]) |
{phone} |
primary phone (phones[0]) |
WhatsApp Cloud templates use Meta's positional
{{1}},{{2}}placeholders instead. Each placeholder is filled from the step'stemplate_variablesmap when configured (a token like{first_name}is resolved from the contact); otherwise it falls back to the contact's name so the send still succeeds.
2. Contact Enrollment¶
Paths:
- Single contact — SequenceEnrollmentMenu (contact profile / dropdown toggle)
- Bulk — BulkSequenceEnrollmentMenu (multi-select on the contacts list)
- From a deal — DealSequenceEnrollmentMenu (enrolls the deal's linked contacts)
- By tag — enrollByTag (all contacts carrying the selected tag)
- Test send — TestSequenceDialog (runs the steps immediately against one contact; see §7)
What is written to sequence_enrollments: status='active', current_step_index=0, enrolled_by, and next_step_at = now + firstStep.delay_minutes. A UNIQUE(sequence_id, contact_id) constraint guarantees one enrollment per contact per sequence.
Entry validation (bulk / dialog / tag paths):
- Contacts with opt_in_status = false are excluded.
- Contacts with no email and no phone are excluded (nothing could ever be sent).
- Contacts already enrolled are left untouched — re-enrolling never restarts, resurrects, or resets a completed, removed, unsubscribed, bounced, or in-progress contact. The UI reports how many were newly enrolled vs skipped.
Enrollment status lifecycle:
| Status | Meaning | Set by |
|---|---|---|
active |
receiving steps | enrollment |
completed |
all steps sent | executor (no more steps) |
paused |
contact replied → held | reply trigger |
removed |
manually removed | UI (removeEnrollments / toggle) |
failed |
step failed 3× or contact permanently unreachable (no email/phone) | executor |
exited |
deal exit condition met | deal-exit trigger + executor |
bounced |
hard-bounced email address | executor |
unsubscribed |
opted out / used unsubscribe link | unsubscribe fn + executor |
active is the only status the executor processes; every other status stops sending.
3. Execution Engine (sequence-executor)¶
Runs on a cron schedule. Each tick:
- Dequeue — selects up to 50 enrollments with
status='active'andnext_step_at <= now(partial indexidx_sequence_enrollments_next_step). - Claim — optimistic-lock update (
next_step_atpushed +10 min, matched on the priornext_step_at); a losing racer matches 0 rows and skips. Prevents double-send across overlapping runs. - Guards (in order), each reschedules or stops without sending:
- Parent sequence still
active? else skip. - Exit conditions — if the contact has a deal (created / in an exit stage), enrollment →
exited. - Sending window — if outside the window (in the resolved timezone),
next_step_atis moved to the next window open. - WhatsApp Lite pacing — a random 10–45 minute gap is enforced between consecutive sends per WhatsApp Lite account, guaranteed across cron ticks via an atomic DB slot claim (
claim_wa_lite_send_slot). When an account is still cooling down, the enrollment is deferred to the reserved time; only one send per account is released per window, so messages are never blasted. - Send — dispatch by channel (see §4), then write one
sequence_execution_logrow (sent|failed|skipped) with a per-stepexecuted_attimestamp and the rendered content. - Progress the enrollment based on the result:
- sent → advance to the next step (or
completedif last). - failed → retry with exponential backoff (5 → 10 → 20 min); after 3 failures →
failed. - skipped (retry) — e.g. a daily send cap was hit → reschedule the same step to the next day. The message is not dropped or skipped past.
- skipped (terminal) — opt-out, unsubscribed, hard-bounced, or no email/phone → stop the enrollment with the matching status (
unsubscribed/bounced/failed). - skipped (advance) — recoverable config gaps (e.g. no channel account / no template on this step) → move to the next step.
Every outbound send is also mirrored to the inbox (conversations + messages, source: "sequence").
4. Channels & Sending¶
| Channel | Send path | Notes |
|---|---|---|
email-send (SMTP) or email-send-ses (AWS SES) |
Auto-selects provider from the step's channel account. Injects a CAN-SPAM unsubscribe footer. Hard-bounce detection pauses the contact's active enrollments. Daily cap per sender (SMTP default 150, SES from account config). | |
| WhatsApp Lite | whatsapp-lite-send (Appgain) |
Free-form text + variables. Daily cap 200/account/day. Enforced random 10–45 min gap between sends per account (§3). |
| WhatsApp Cloud | whatsapp-cloud-send |
Requires a Meta-approved template_name; components/parameters built from the template definition + template_variables. |
| SMS | Appgain notify scheduler | Text + variables; uses org or channel Appgain credentials. |
Daily send caps are enforced atomically via increment_sequence_send_count_v2 (per account_kind + account + day). If a send is reserved against the cap but then fails, the reserved slot is returned (decrement_sequence_send_count_v2) so failures/retries don't leak quota.
Opt-out is checked on every channel at send time (contact.opt_in_status); email additionally checks the unsubscribe-token table and the bounce-suppression list.
5. Compliance & Unsubscribe¶
- Unsubscribe link is appended to every sequence email, pointing at
/sequence-unsubscribe?token=…. - Tokens live in
sequence_unsubscribe_tokens, one per(email, organization_id). Each token is a 256-bit random opaque value (encode(gen_random_bytes(32),'hex')) — unguessable; looked up server-side by the publicsequence-unsubscribeedge function. - On unsubscribe the function: marks the token
unsubscribed_at, sets the contactopt_in_status = false, sets that contact's active enrollments tounsubscribed, and logs acontact_activity_logentry. - Global opt-out: because
opt_in_statusis honored by all channels, unsubscribing stops Email, WhatsApp, and SMS for that contact — not just the current sequence. - Unsubscribes are not silently reversed. An internal opt-in flip / import does not auto-clear a prior unsubscribe; re-subscription requires an explicit, deliberate action.
6. Analytics & Reporting¶
Backed by sequence_execution_log (statuses sent / failed / skipped), enrollment states, and the bounce-suppression list. The dashboard (SequenceAnalyticsDashboard) reads a 30-day window by default (range index idx_sequence_execution_log_org_executed_at), paginated to avoid the PostgREST 1000-row cap.
Headline metrics:
- Messages Sent = executions with status sent only (skipped/failed excluded).
- Success Rate = sent / (sent + failed) — over delivery attempts, not skips.
- Unsubscribes — count + rate over enrollments in the range.
Breakdowns: - Channel performance — sent / failed / rate per channel (rate over attempts). - Enrollment status — active, completed, paused, removed, unsubscribed, bounced, exited (goal met), failed (all terminal states are surfaced so the breakdown reflects reality). - Per-step — executions, successes, failures, skips, and grouped error reasons per step. - Deliverability — per-recipient email failures, with bounces reclassified from the suppression list. - Recent activity — latest executions with rendered subject/content.
Reply-pause rows (Auto-paused: contact replied) are logged as skipped but represent a good outcome, so they are excluded from all error/deliverability aggregations.
Known reporting gaps (roadmap): email opens/clicks are tracked in email_tracking_events but not yet joined into sequence reports; reply-rate and a true enrolled→step cohort funnel are not yet surfaced.
7. Test Sends¶
TestSequenceDialog invokes the executor in test mode against a single contact. It:
- Verifies the caller's organization — the JWT must belong to the organization_id, and all lookups are org-scoped (no cross-tenant sends).
- Runs every step immediately (skips daily-cap accounting), staggering consecutive WhatsApp Lite steps.
- Sends real messages to the target contact (it is a live test, not a dry run) — still honoring opt-out, unsubscribe, and bounce checks.
How-To Guide¶
All tasks start at Sidebar → Sequences (/sequences). The page has two tabs: Sequences (list + builder) and Analytics.
A. Create a sequence¶
- Click Create Sequence (top-right).
- Enter a Sequence Name (required) and an optional Description.
- (Optional) configure a Sending Window and Exit Conditions — see B and C.
- Under Steps, click one of the channel buttons to add a step: Email, SMS, WA Lite, or WA Cloud. Add as many steps as you need; drag the handle to reorder.
- For each step set:
- Delay — how long to wait after the previous step before this one fires (
Immediately,5 minutes, …1 hour,1 day,1 week). - Send via — the connected channel account that will send it.
- Subject (email only) and Message (see D for personalization). For WA Cloud, pick an approved Template instead of free text (see E).
- Save. The sequence is created as Draft.
- When ready, Activate it (see H) — the executor only processes active sequences.
The first step usually uses Immediately so enrolled contacts are picked up on the next executor tick.
B. Configure a sending window (quiet hours)¶
- In the builder, enable Sending Window.
- Set From / To (e.g.
09:00–17:00) and the Timezone. - Steps that come due outside the window are held and automatically sent when the window next opens. Overnight windows (e.g.
20:00–06:00) are supported.
C. Configure exit conditions (stop on conversion)¶
- In the builder, open Exit Conditions.
- Toggle Exit when any deal is created for the contact, and/or select deal stages that should end the sequence.
- When a contact matches, their enrollment moves to
exitedand no further steps are sent. This fires both instantly (DB trigger) and as a safety check on every executor run.
D. Personalize messages with variables¶
Insert single-brace tokens into a subject or message body; they're replaced per contact at send time:
{first_name} · {last_name} · {name} · {email} · {phone}
Example: Hi {first_name}, thanks for your interest! Missing values render as empty (or a safe fallback), so the send never breaks.
E. Set up a WhatsApp Cloud template step¶
- Add a WA Cloud step and pick the connected WhatsApp Cloud account under Send via.
- Choose an approved Template (name + language) from the picker — WhatsApp Cloud does not allow free text outside the 24-hour service window.
- If the template has positional placeholders (
{{1}},{{2}}, …), provide an ordered variable map so each placeholder is filled correctly (a token like{first_name}is resolved from the contact). Without a map, placeholders fall back to the contact's name.
F. Enroll contacts¶
There are four ways to enroll (all skip opted-out contacts, contacts with no email/phone, and contacts already enrolled):
- From the sequence — on the Sequences tab, click the Enroll (person+) action, then pick contacts or a tag.
- From the contacts list — select contacts → Enroll in Sequence → choose the sequence.
- From a deal — use the deal's Enroll in Sequence menu to enroll the deal's linked contacts.
- By tag — enroll everyone carrying a tag in one action.
The confirmation reports how many were newly enrolled vs skipped (already enrolled / not eligible). Re-enrolling never restarts someone who already completed, unsubscribed, bounced, or is mid-sequence.
G. Test before launching¶
- On the Sequences tab, open the sequence's Test action.
- Search for and pick one contact.
- Run it — every step executes immediately against that contact. This sends real messages (it is a live test, not a preview) and still respects opt-out/unsubscribe/bounce checks. You can only test sequences in your own organization.
H. Activate, pause, archive, delete¶
- Activate / Pause — the play/pause action toggles the sequence between
activeandpaused. Pausing holds all its enrollments (no steps sent) until reactivated. - Edit — change steps/settings anytime; changes apply to future sends.
- Delete / Archive — removes it from processing.
I. Monitor performance¶
Open the Analytics tab (default window: last 30 days; adjustable):
- Overview — Messages Sent (actual sends), Success Rate (sent ÷ delivery attempts), Unsubscribes.
- Enrollment Status — active / completed / paused / removed / unsubscribed / bounced / exited (goal met) / failed.
- Channel Performance — sent / failed / rate per channel.
- Per-step — executions, successes, failures, skips, and grouped error reasons.
- Deliverability — per-recipient email failures with bounces flagged.
- Activity — the most recent executions with the rendered content.
To see who is where in a sequence, open its enrollments panel from the list.
J. Handle unsubscribes & opt-outs¶
- Every sequence email includes an unsubscribe footer. When a recipient clicks it, their contact is marked opted-out, active enrollments become
unsubscribed, and they're excluded from all future sends on every channel. - A contact replying (inbound message) auto-pauses their active enrollments so you don't message over a live conversation.
- Honored unsubscribes are not silently reversed by imports or edits — re-subscribing requires a deliberate action.
K. Troubleshooting (why a contact didn't receive a step)¶
Check the Per-step / Deliverability analytics — the error_message explains each skip/failure:
| Symptom | Cause | Resolution |
|---|---|---|
Enrollment unsubscribed right away |
Contact opted out / unsubscribed | Expected — respect the opt-out |
Enrollment failed, "no phone/email" |
Contact missing the channel's address | Add the contact's email/phone |
| Step skipped, "Daily … limit reached" | Sender hit its daily cap | It auto-retries the next day; raise the cap or add a sender |
| Step skipped, "No template selected" | WA Cloud step without a template | Edit the step and pick an approved template |
| Nothing sends at all | Sequence is Draft/Paused, or outside the sending window | Activate it / wait for the window |
Enrollment bounced |
Email hard-bounced previously | Address is suppressed; fix or remove it |
Use Cases¶
Welcome Email Sequence¶
5-email onboarding series (immediate → +1d → +3d → +5d → +7d), auto-enrolled when a contact is tagged "Subscriber" via an automation.
WhatsApp Follow-Up¶
3 WhatsApp Lite nudges (+2h → +2d → +5d) manually enrolled after a quote is sent; replies auto-pause the sequence.
Re-Engagement Campaign¶
2-email win-back (We miss you, {first_name} → last-chance) bulk-enrolled on contacts tagged "Inactive 90 Days".
Post-Purchase WhatsApp Cloud¶
Order confirmation → shipping → delivery → feedback using Meta-approved templates, auto-enrolled when a deal closes.
Test Cases¶
- Create sequence with steps — create, add 3 steps with delays, save, confirm it appears active.
- Enroll contacts — enroll 5; opted-out / no-channel contacts are skipped; already-enrolled are skipped; the rest become
active. - Step execution — 1-step sequence, enroll, wait for cron, confirm message sent,
sequence_execution_logrow issent, enrollmentcompleted. - Daily cap deferral — exceed a sender's daily cap; confirm the step is rescheduled to the next day (not dropped) and the message goes out then.
- Unsubscribe — click the unsubscribe link; confirm token
unsubscribed_atset, contactopt_in_status=false, active enrollments becomeunsubscribed, and re-enrollment is skipped. - Reply-pause — send an inbound message from an enrolled contact; confirm active enrollments become
pausedand the pause does not appear as a delivery error in analytics. - WhatsApp Cloud template — select an approved template, map
template_variables, enroll a valid number, confirm the template is delivered.
Related Documentation¶
docs/audits/sequence-lifecycle-audit.md— full lifecycle audit + remediation notes- Automations, Contacts, AI Re-Engagement
- WhatsApp Lite API Guide
Document Version: 2.1.0 Last Updated: July 2026