Sequencer Overview
The sending half of Superkabe — campaigns, sequences, dispatch, and how it hands off to the protection layer.
In one paragraph
A Superkabe Campaign owns a multi-step Sequence. Each step has one or more Variants (A/B). Leads enrolled in the campaign become CampaignLeads. A 60-second dispatcher finds CampaignLeads whose next-send time is due, picks a mailbox via ESP-aware routing, and queues a batched send job. The send adapter dispatches via Gmail API / Microsoft Graph / SMTP with HMAC-signed tracking pixels and List-Unsubscribe headers. Replies are detected by the IMAP poller and stop the sequence. Bounces are detected by SMTP transcripts and DSN parsing, and trigger the protection layerif rates cross thresholds.
The Entity Model
Six tables make up the sequencer's sending model. Most operations the dispatcher does are joins across these — knowing the model is the fastest way to predict what the system will do in any given situation.
| Entity | Represents | Lifetime |
|---|---|---|
| Campaign | A named send program with a sequence, schedule, and a pool of mailboxes | Persistent — paused when finished |
| SequenceStep | One step in the campaign's sequence (subject + body + delay before next step) | Persistent |
| StepVariant | A/B variant of a step. Picked at send-time by weighted rotation | Persistent |
| CampaignLead | A lead enrolled in this campaign. Carries current_step, next_send_at, sticky mailbox, status | Per enrollment — terminal on reply / bounce / unsubscribe / completion |
| CampaignAccount | Junction row: which mailboxes the campaign can dispatch from. Optional per-mailbox override caps | Persistent |
| SendEvent | One row per email actually sent. Source of truth for analytics + bounce-rate windows | 90-day retention |
The protection layer adds Mailbox andDomain rows alongside ConnectedAccount — see the State Machine doc for how those interact with the sequencer.
A Send's Lifecycle, Step by Step
A single email going from "lead enrolled" to "reply received" passes through ten distinct stages. Each is owned by a specific service so failures are debuggable in isolation.
Lead arrives via CSV upload, Clay webhook, manual create, REST API, or migration import. Lands in the org-wide Lead table.
leadHealthService scores the lead GREEN / YELLOW / RED based on email validation + domain signals. RED is rejected at intake.
A CampaignLead row is created in the chosen campaign. status=active, current_step=0, next_send_at=now (for the first step) or in the future (for follow-ups).
sendQueueService scans active campaigns, finds CampaignLeads where next_send_at ≤ now, applies suppression filters (bounced / unsubscribed / paused).
For each due lead, ESP-aware routing scores each campaign mailbox: 0.6 × remaining_capacity + 0.4 × per-ESP performance. Highest score wins. Sticky pinning preserves the same mailbox for subsequent steps.
Weighted rotation picks a variant. Spintax + token replacement personalize the subject/body. CAN-SPAM postal-address footer + List-Unsubscribe URL are appended.
Emails are batched per mailbox (one BullMQ job per mailbox per tick) so a single SMTP connection sends all of that mailbox's emails for the tick. Reduces connection churn and improves rate predictability.
gmailSendService / microsoftSendService / emailSendAdapters fire the actual API call. Tracking pixel and click-tracking link rewrites are injected here. SendEvent row written on success.
On send success: current_step++, last_sent_at=now, next_send_at=now+step.delay. If this was the last step, status=completed.
imapReplyWorker polls every 60s. A reply flips CampaignLead.status=replied and stops further sends. A hard bounce triggers the protection layer's suppression cascade.
Sending Window & Daily Caps
The dispatcher applies four independent caps before assigning sends. The smallest remaining capacity wins. If any one is exhausted, the lead is held until the next tick.
ConnectedAccount.daily_send_limit. The absolute cap on what this mailbox sends per day across all campaigns combined.
Campaign.daily_limit. The cap on what this campaign sends per day, summed across all its assigned mailboxes.
CampaignAccount.daily_limit_override. Caps how much one specific mailbox contributes to one specific campaign — used when a mailbox is shared across campaigns.
Mailbox.warmup_limit. Active only when the mailbox is in QUARANTINE / RESTRICTED_SEND / WARM_RECOVERY. The protection layer owns this.
Sending window: each campaign declares schedule_timezone,schedule_start_time,schedule_end_time, and a list of valid days (e.g. Mon–Fri). The dispatcher resolves "is it in the sending window now?" against the campaign's timezone, not UTC. Outside the window, due leads accumulate and dispatch resumes when the next valid hour begins.
Send gap: Campaign.send_gap_minutes spaces sends from the same mailbox apart by at least N minutes (with a small random jitter). This makes the sending pattern look human and prevents tight bursts that ISPs flag as automation.
Stop Conditions
A CampaignLead is removed from the dispatch pool the moment any of these terminal events fires. Each maps to a distinct status; the dispatcher's pre-send filter checks them all.
| Event | Detected by | Resulting CampaignLead.status |
|---|---|---|
| Reply received | imapReplyWorker (60s polling) | replied |
| Hard bounce | SMTP 5xx transcript / RFC 3464 DSN | bounced |
| Unsubscribe (footer click or List-Unsubscribe) | trackingController.processUnsubscribe | unsubscribed |
| Sequence finished (last step sent) | sendQueueService.advanceStep | completed |
| Operator paused (UI / API) | campaignController2.pauseLead | paused |
Org-wide suppression: the dispatcher also runs a defense-in-depth check against the org-wideLead.status table. A lead markedunsubscribed orbounced at the org level is filtered out of every campaign — not just the one where the event happened. This is required for CAN-SPAM § 5(a)(4)(A), CASL § 11(3), and GDPR Art. 21 compliance.
Handoff to the Protection Layer
The sequencer is the active half of the platform — it dispatches mail. The protection layer is the reactive half — it watches what the sequencer just did and intervenes if the data goes bad. The handoff between them happens at three exact points:
monitoringService aggregates SendEvents into rolling windows to compute bounce rate per mailbox and per domain. Threshold breach triggers pauseMailbox → cooldown → quarantine → 5-phase healing.
A hard bounce flips Lead.status=bounced ANDCampaignLead.status=bounced in one transaction. The dispatcher won't pick that lead up on subsequent ticks.
If a mailbox is connection_status='disconnected' (e.g. paused, expired, recovering), the dispatcher skips it. Ifrecovery_phase is in a constrained phase, the warmup_limit cap kicks in. The sequencer never overrides protection — protection always wins.
For the full picture of how protection takes over once it's triggered, read Auto-Healing Pipeline and State Machine.
Compliance Surfaces in the Sequencer
Five regulatory surfaces are enforced inside the dispatcher itself, not as opt-in features. None can be disabled because deliverability + legal exposure both depend on them:
- CAN-SPAM § 5(a)(5) — every commercial email must carry a valid postal address. The dispatcher refuses to send any campaign where
Organization.mailing_addressis null and fires a Slack alert until the operator configures one. - RFC 8058 / Gmail bulk-sender policy — every send carries
List-Unsubscribe+List-Unsubscribe-Postheaders. One-click unsubscribe is mandatory above 5K sends/day. - GDPR Art. 21 / CASL § 11(3) — unsubscribes propagate org-wide via the Lead.status cascade. The dispatcher checks it as defense-in-depth against partial transaction failures.
- EU compliance mode — when
Campaign.eu_compliance_mode=true, the tracking pixel is suppressed (ePrivacy Art. 5(3) requires explicit consent for tracking cookies/pixels for EU recipients). - RFC 3464 DSN parsing — async bounces (mailbox accepted then later rejected) are captured by the IMAP poller and converted to BounceEvents so suppression can fire.
What's NOT in the Sequencer
Worth being explicit about — these are protection-layer responsibilities and the sequencer never touches them:
- Pausing a mailbox — owned by
monitoringService.pauseMailbox - The 5-phase healing pipeline — owned by
healingService - DNSBL / IP blacklist checks — owned by
dnsblService+mailboxIpBlacklistWorker - SPF / DKIM / DMARC validation — owned by
infrastructureAssessmentService - Domain warming / cooldowns — owned by
warmupService+warmupTrackingWorker
Related Reading
How each send picks the optimal mailbox for the recipient's ESP
The intake gate that runs before a lead can enroll
CampaignLead, Mailbox, and Domain status transitions
What happens after the dispatcher hands off to protection
Programmatically create campaigns, enroll leads, fetch sends
Push enriched leads from Clay directly into a campaign