← All packages

@lacspace/sequence

v1.0.0Mail Kit0 deps

Multi-step follow-up email sequences as a pure state machine: no I/O, no timers, no storage. Given a sequence, a plain-JSON enrollment and `now`, nextAction() says send / wait (with the exact next time) / skip / stop / paused. Delays in days, hours, minutes and business days; send windows (days of week, HH:MM or fractional hours, holidays) in the recipient's IANA timezone, DST-correct via Intl (Asia/Kathmandu +05:45 included); stop on reply, bounce, unsubscribe, complaint, click, meeting booked or manual stop; step conditions (no_reply, opened, no_open, clicked, no_click); pause/resume; 3-strike failure stop with retry delay; reply threading; seeded deterministic jitter; due() scheduler tick with per-hour and per-day send budgets; previewTimeline() and validateSequence(). Zero dependencies, isomorphic.

npm i @lacspace/sequence

Usage

sequence.ts
import { advance, due, validateSequence, type Enrollment, type Sequence } from "@lacspace/sequence";

const followUp: Sequence = {
  id: "demo-follow-up",
  steps: [
    { id: "intro", templateId: "tpl_intro" },
    { id: "bump", delay: { businessDays: 2 }, templateId: "tpl_bump", threadWith: "previous" },
    { id: "last", delay: { days: 4 }, templateId: "tpl_last", condition: "no_open" },
  ],
  window: { start: "09:00", end: "17:00", weekdaysOnly: true },
  // default stopOn: reply, bounce, unsubscribe, complaint, manual
};

validateSequence(followUp); // { ok: true, errors: [] }

let enrollment: Enrollment = {
  id: "enr_1",
  sequenceId: "demo-follow-up",
  contact: "sam@example.com",
  timezone: "America/New_York",
  enrolledAt: new Date().toISOString(),
  history: [],
  events: [],
};

// Every minute, in your scheduler:
const now = new Date();
const tick = due([enrollment], { [followUp.id]: followUp }, now, {
  perHour: 50, perDay: 400, sentThisHour: 0, sentToday: 0,
});

for (const { enrollmentId, action } of tick.send) {
  // send with action.step.templateId; set In-Reply-To/References from action.threadWith
  enrollment = advance(enrollment, { type: "sent", stepId: action.step.id, messageId: "<id@mail>", at: now.toISOString() });
  // on error: advance(enrollment, { type: "failed", stepId: action.step.id, error: String(err), at: now.toISOString() })
}
for (const { stepId } of tick.skip) {
  enrollment = advance(enrollment, { type: "skipped", stepId, at: now.toISOString() });
}
for (const { enrollmentId, reason } of tick.stop) {
  // set the enrollment's status yourself, e.g. "replied", "done", "unsubscribed"
}

// From your inbound mail / tracking webhooks:
enrollment = advance(enrollment, { type: "reply", at: new Date().toISOString() });

Exports 18

DEFAULT_STOP_ONMAX_CONSECUTIVE_FAILURESadvanceduehash32isTerminalStatusisValidTimeZonejitterMsnextActionnextRunnormalizeWindowoffsetMsparseClockpreviewTimelinescheduleStepvalidateSequencezonedPartszonedToUtc

Keywords

emailsequencedripfollow-upcadenceoutreach

More in Mail Kit

@lacspace/email-templates

Compose bulletproof, responsive, dark-mode HTML emails from simple blocks, plus 13 ready-made transactional templates (OTP, verify, password-reset, magic-link, receipt, order, shipping, invitation, digest, announcement). Ships plaintext generation, preheaders and {{var}} i18n interpolation. Zero-dependency, isomorphic.

@lacspace/email-validate

Smart, network-free email validation — RFC-5322 syntax (incl. quoted local parts & IP-literal domains), disposable/temp-mail & role-account detection, free-provider flags, Gmail normalization and 'did you mean?' typo suggestions. Zero-dependency, isomorphic.

@lacspace/email-verify

Best-effort email deliverability for Node — syntax + disposable/role, MX lookup with priority ranking, an optional SMTP RCPT probe (no mail sent), catch-all detection, a 0-100 confidence score, and de-duped batch verification. All DNS/SMTP injectable; zero npm dependencies.

@lacspace/mailer

A tiny zero-dependency SMTP client for Node — send email over raw net/tls with STARTTLS & AUTH, plus a fluent MIME builder (inline images, attachments, alternatives), RFC 5322 address + RFC 2047 helpers, batch send with retry, and no-network test transports. Provider presets (Hostinger, Gmail, Outlook, Zoho…) make setup one line.

@lacspace/imap

Zero-dependency IMAP4rev1 client for Node (RFC 3501 + IDLE, MOVE, UIDPLUS, CONDSTORE, SPECIAL-USE, LIST-EXTENDED, LITERAL+, SASL-IR, QUOTA, ID). Implicit TLS and STARTTLS with verification on, LOGIN / PLAIN / XOAUTH2 / OAUTHBEARER, streaming byte-accurate parser, async-iterable FETCH, envelopes with RFC 2047 decoding, BODYSTRUCTURE trees shared with @lacspace/mime, modified UTF-7 mailbox names, special-use detection (incl. Gmail XLIST and localised names), IDLE with NOOP fallback. Works with Hostinger (Dovecot), Gmail, Outlook/Exchange, GoDaddy.

@lacspace/mime

Isomorphic RFC 5322 / RFC 2045-2049 MIME parser and builder for webmail. parseMime() turns raw mail (string or bytes) into from/to/cc/subject/date/text/html/attachments/inline cid images/priority/List-Unsubscribe one-click and the part tree; handles nested multipart (mixed, alternative, related, report, signed), message/rfc822 forwards, base64 and quoted-printable (tolerant), RFC 2047 encoded words (split multibyte, adjacent whitespace), RFC 2231 parameters, 30+ charsets and malformed input without ever throwing. parseBodyStructure() parses IMAP BODYSTRUCTURE (literals, extension data) into the same tree with IMAP partIds, plus findTextParts / listAttachments / decodePart for lazy fetching. buildMime() writes CRLF messages with encoded-word headers, QP/base64 bodies, mixed/alternative/related nesting, Message-ID generation and header-injection guards; replyHeaders() / forwardSubject() for threading. Pairs with @lacspace/imap and @lacspace/mailer. Zero dependencies; Node 18+, edge runtimes and browsers.