Skip to main content

Sequences

A sequence is a flow graph: a trigger, then waits, sends, branches, and goals. Contacts move through it as enrollments, one step per tick.

sequence.ts
import { mailroom } from "./client";

// A linear sequence: Mailroom builds the flow graph from these steps. Every
// `template` must exist in your project's registry — an unknown key is rejected
// with `unknown_template`, so check `mailroom.templates.list()` first.
await mailroom.sequences.create({
key: "onboarding",
name: "Onboarding",
status: "active",
trigger: "user.signed_up", // events.track({ type: "user.signed_up" }) enrolls
steps: [
{ template: "welcome" },
{ template: "getting-started", delayMinutes: 60 * 24 },
{ template: "tips", delayMinutes: 60 * 24 * 3 },
],
});

// Dry-run before you enrol anyone: which nodes run, which emails would send.
const sim = await mailroom.sequences.simulate("onboarding", {
contact: { status: "subscribed", tags: ["beta"] },
});
console.log(sim.path.map((s) => s.kind).join(" → "), sim.sends, sim.endedBy);

// Enrol one contact...
const { enrollmentId } = await mailroom.sequences.enroll("onboarding", {
email: "ada@example.com",
});

// ...or a batch (chunked automatically above 5000 recipients).
const bulk = await mailroom.sequences.bulkEnroll("onboarding", {
emails: ["grace@example.com", "alan@example.com"],
});
console.log(`${bulk.enrolled} enrolled, ${bulk.skipped} skipped`);

// Stop one contact early; the remaining steps never send.
await mailroom.enrollments.cancel(enrollmentId);

Two ways to define one

Steps (create) — a linear drip. Give an ordered list of templates with delays and Mailroom builds the graph for you. This covers most onboarding and nurture flows.

await mailroom.sequences.create({
key: "onboarding",
name: "Onboarding",
status: "active",
trigger: "user.signed_up",
frequencyCap: { count: 3, windowHours: 168 }, // at most 3 emails a week
steps: [
{ template: "welcome" },
{ template: "getting-started", delayMinutes: 60 * 24 },
],
});

Graph (update) — the full model, for branching. get() returns the graph, update() replaces it atomically, and the graph is the portable unit: export from one project, import into another.

const graph = await mailroom.sequences.get("onboarding");
await mailroom.sequences.update("onboarding", { ...graph, status: "paused" });

Node kinds: trigger, send_email, wait_delay, wait_until, branch, split, update_contact, webhook, subflow, goal, exit. Edges carry an optional branchKey to say which arm of a branch/split they leave from.

An invalid graph is rejected with invalid_graph and a list of problems — nothing is half-applied.

Enrolling

Explicitly, by email or contact id:

const { enrollmentId } = await mailroom.sequences.enroll("onboarding", { email: "ada@example.com" });

In bulk — chunked into sequential requests above 5000 recipients, with already-active enrollments skipped rather than duplicated:

const { requested, enrolled, skipped } = await mailroom.sequences.bulkEnroll("onboarding", {
emails: ["grace@example.com", "alan@example.com"],
});

Or automatically, by tracking an event whose type matches the sequence's trigger:

events.ts
import { mailroom } from "./client";

// Tracking an event upserts the contact, enrols them into every sequence whose
// trigger matches `type`, and exits them from the sequences you list.
const result = await mailroom.events.track({
type: "order.placed",
email: "ada@example.com",
data: { orderId: "ord_123", total: 4200, currency: "USD" },
exitSequences: ["abandoned-cart"],
});

console.log(result.contactId, result.enrolled, result.reArmed, result.exited);

events.track() upserts the contact, enrolls them into every sequence triggered by that type, re-arms flows waiting on it, and cancels enrollments in the exitSequences you name — one call for the whole lifecycle transition. That is how you wire "order placed → stop the abandoned-cart flow, start the post-purchase flow".

Re-entry and frequency

reentry on the graph decides what happens when someone qualifies twice: once_ever, once_active (default-ish behaviour: no parallel runs), always, or after_cooldown with cooldownHours.

frequencyCap bounds how many emails the flow may send a contact in a rolling window — the safety net against a chatty trigger.

Simulate before you ship

const sim = await mailroom.sequences.simulate("onboarding", {
contact: { status: "subscribed", tags: ["beta"] },
event: { type: "user.signed_up" },
});
// sim.path: the nodes walked · sim.sends: templates that would send
// sim.endedBy: "exit" | "goal" | "dead_end" | "hop_guard"

No emails are sent and no enrollment is created. dead_end means a branch leads nowhere; hop_guard means the walk hit its step ceiling, usually a loop.

You can simulate against a real contact (contactId) or a hypothetical one (contact).

Stopping

await mailroom.enrollments.cancel(enrollmentId); // one contact, remaining steps dropped
await mailroom.sequences.update("onboarding", { ...graph, status: "paused" }); // whole flow
await mailroom.sequences.remove("onboarding"); // delete flow + enrollments

Scopes

Everything sequence-related — including reads and events.track() — uses sequences:write.

Reference