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.
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:
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.