Skip to content

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-executor edge function (cron-driven)
  • Unsubscribe: sequence-unsubscribe edge 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): - channelemail | 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's template_variables map 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:

  1. Dequeue — selects up to 50 enrollments with status='active' and next_step_at <= now (partial index idx_sequence_enrollments_next_step).
  2. Claim — optimistic-lock update (next_step_at pushed +10 min, matched on the prior next_step_at); a losing racer matches 0 rows and skips. Prevents double-send across overlapping runs.
  3. Guards (in order), each reschedules or stops without sending:
  4. Parent sequence still active? else skip.
  5. Exit conditions — if the contact has a deal (created / in an exit stage), enrollment → exited.
  6. Sending window — if outside the window (in the resolved timezone), next_step_at is moved to the next window open.
  7. 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.
  8. Send — dispatch by channel (see §4), then write one sequence_execution_log row (sent | failed | skipped) with a per-step executed_at timestamp and the rendered content.
  9. Progress the enrollment based on the result:
  10. sent → advance to the next step (or completed if last).
  11. failed → retry with exponential backoff (5 → 10 → 20 min); after 3 failures → failed.
  12. 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.
  13. skipped (terminal) — opt-out, unsubscribed, hard-bounced, or no email/phone → stop the enrollment with the matching status (unsubscribed / bounced / failed).
  14. 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 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 public sequence-unsubscribe edge function.
  • On unsubscribe the function: marks the token unsubscribed_at, sets the contact opt_in_status = false, sets that contact's active enrollments to unsubscribed, and logs a contact_activity_log entry.
  • Global opt-out: because opt_in_status is 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

  1. Click Create Sequence (top-right).
  2. Enter a Sequence Name (required) and an optional Description.
  3. (Optional) configure a Sending Window and Exit Conditions — see B and C.
  4. 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.
  5. For each step set:
  6. Delay — how long to wait after the previous step before this one fires (Immediately, 5 minutes, … 1 hour, 1 day, 1 week).
  7. Send via — the connected channel account that will send it.
  8. Subject (email only) and Message (see D for personalization). For WA Cloud, pick an approved Template instead of free text (see E).
  9. Save. The sequence is created as Draft.
  10. 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)

  1. In the builder, enable Sending Window.
  2. Set From / To (e.g. 09:0017:00) and the Timezone.
  3. Steps that come due outside the window are held and automatically sent when the window next opens. Overnight windows (e.g. 20:0006:00) are supported.

C. Configure exit conditions (stop on conversion)

  1. In the builder, open Exit Conditions.
  2. Toggle Exit when any deal is created for the contact, and/or select deal stages that should end the sequence.
  3. When a contact matches, their enrollment moves to exited and 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

  1. Add a WA Cloud step and pick the connected WhatsApp Cloud account under Send via.
  2. Choose an approved Template (name + language) from the picker — WhatsApp Cloud does not allow free text outside the 24-hour service window.
  3. 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

  1. On the Sequences tab, open the sequence's Test action.
  2. Search for and pick one contact.
  3. 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 active and paused. 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

  1. Create sequence with steps — create, add 3 steps with delays, save, confirm it appears active.
  2. Enroll contacts — enroll 5; opted-out / no-channel contacts are skipped; already-enrolled are skipped; the rest become active.
  3. Step execution — 1-step sequence, enroll, wait for cron, confirm message sent, sequence_execution_log row is sent, enrollment completed.
  4. 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.
  5. Unsubscribe — click the unsubscribe link; confirm token unsubscribed_at set, contact opt_in_status=false, active enrollments become unsubscribed, and re-enrollment is skipped.
  6. Reply-pause — send an inbound message from an enrolled contact; confirm active enrollments become paused and the pause does not appear as a delivery error in analytics.
  7. WhatsApp Cloud template — select an approved template, map template_variables, enroll a valid number, confirm the template is delivered.


Document Version: 2.1.0 Last Updated: July 2026