Your storage
We don’t replicate any of this. You own it. We own the conversation, the threading, and the no-reply timer.Firing a step
Idempotency-Key: ${lead.id}:${lead.current_step}— if your worker crashes after we accepted the send but before your DB recorded it, the retry callsfireStepagain and we return the same response without sending twice.noReplyEventAfter: step.waitNext— each step defines its own follow-up window. This is the timer that wakes us up to advance to the next step.
Reacting to events
The handler for the four event types is small:email.sent confirmations are useful for monitoring
(“how many sends are in flight”), but they don’t change lead state in
this model — the send already happened in fireStep and the DB row
already moved to in_flight.
Bootstrapping the campaign
To start a campaign, you fire the first step for each lead:429s, and look bursty. A simple pacer (one send per N
milliseconds, randomized within a window) is enough; we don’t need a
queue.
Recovery
Your webhook receiver was down for an hour. You missed events. Recover by walking the events stream from the lastseq you
persisted:
seq, so this is
trivially deduppable: track (identity, lastSeq) per identity and refuse
anything <= it.
What we don’t emit yet
Bounces and engagement events aren’t in the stream today.- Bounces.
email.bouncedis on the roadmap. Today, a bounce arrives as an inboundemail.received(oremail.replied) frommailer-daemon@…. If your sequencer needs to suppress dead addresses immediately, pattern-match the sender domain in youremail.receivedhandler. - Opens / clicks.
email.openedandemail.clickedaren’t in the stream. Most sequencers run their own tracking pixel and link rewriter — inject them into thehtmlyou pass to/sendand you get the same telemetry every other tool offers, plus full control over what’s tracked. (Whether you should track opens at all is a product question, not a 12m one.)
email.bounced
ships; until then, the mailer-daemon heuristic is what every
sequencer uses.
What you don’t have to build
Worth being explicit about, since teams often start a sequencer build without realizing how much is already done:- A scheduler. The no-reply timer is the scheduler.
- A mailbox warmup system. We run it.
- A mailbox rotation algorithm. We run it.
- A reply-detection system. The
email.repliedevent is it. - Threading headers, subject prefixes, sender continuity. We do them.
- Per-mailbox daily caps and budget tracking. We do that too.