Skip to main content

Templates

A template is a code-defined email in the Mailroom registry. Projects enable the templates they use, optionally override the subject and sender, and supply data at send time. Content blocks (hero copy, product strips) live in versioned documents you can edit without a deploy.

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

// What the project can send, and how each template is configured.
for (const t of await mailroom.templates.list()) {
console.log(t.key, t.enabled, t.subject);
}

// The variable contract: exactly what `send({ template, data })` may pass.
const { dataSchema, systemVars } = await mailroom.templates.schema("welcome");
console.log(dataSchema.map((v) => `${v.path}: ${v.kind}`));
console.log(`injected for you: ${systemVars.join(", ")}`);

// Enable it for this project and set the default subject line. Adding a
// template-level `from` here is rejected with `invalid_from` unless the address
// sits on one of the project's verified sending domains.
await mailroom.templates.setEnabled("welcome", {
enabled: true,
defaultSubject: "Welcome to Acme",
});

// Content lives in versions: save a draft, then publish when it looks right.
// Documents are template-specific, so start from what the template already has
// rather than inventing a shape — publishing an unrelated object can leave the
// template unrenderable (sends then fail with `invalid_content`).
const current = await mailroom.templates.getContent("welcome");
const base = (current.published?.content ??
current.draft?.content ??
(current.defaultContent as Record<string, unknown>)) ?? {};

const draft = await mailroom.templates.putContent("welcome", { content: base });
console.log(`draft v${draft.version}`);

const live = await mailroom.templates.publish("welcome", { content: base });
console.log(`published v${live.version}`);

const after = await mailroom.templates.getContent("welcome");
console.log(after.draft?.version, after.published?.version);

Passing data

Wherever a template needs a value from you it references a dotted path into the data object:

  • Text is filled by path — a heading bound to orderNumber renders data.orderNumber.
  • Links and images interpolate {{ path }} inside href / src, e.g. href="{{ orderUrl }}".
  • Repeats (product strips, line-item lists) loop a block over an array path such as products or lineItems and expose each element under an alias, so an item field reads as product.name or li.price.
  • Conditionals (showIf) drop a block when a dotted path is falsy — a sale price only renders when product.priceWas is set.

Resolution is null-safe: a missing or misspelled path renders empty rather than failing the send. Nothing validates data server-side, so a typo is a silently blank email — read the schema (below) instead of guessing.

await mailroom.send({
to: "ada@example.com",
template: "sale-announcement",
data: {
percent: 15,
couponCode: "SALE15",
products: [
{ name: "Aria Wireless Earbuds", url: "https://…", priceFrom: "109.65", priceWas: "129.00" },
],
},
});

System variables (do not send these)

Injected from the brand and the send call. Anything you put in data under these keys is ignored:

VariableSource
brandNameThe brand's name
postalAddressThe brand's postal address (compliance footer)
unsubscribeUrlGenerated per recipient for marketing sends
yearCurrent year (footer copyright)

Discovering the contract

const { key, type, dataSchema, systemVars } = await mailroom.templates.schema("sale-announcement");
// dataSchema: [{ path: "percent", kind: "number", sample: 15 }, …]

dataSchema lists every variable's dotted path, kind, and a sample value — the authoritative answer to "what can I pass to this template?". It may also include the system variables the renderer injects; systemVars is the list to ignore when building your data object.

Common item shapes

  • Product cards (products, recommendations): name, url, and optionally category, description, imageUrl, priceFrom (current price), priceWas (struck original, shown on sale), strengths (pre-joined string like "5MG · 10MG"), madeInUsa (boolean).
  • Order line items (lineItems): name, meta, price.
  • Cart snapshot (items): title, quantity, optional price.

Enabling and configuring

await mailroom.templates.setEnabled("welcome", {
enabled: true,
defaultSubject: "Welcome to Acme",
from: { email: "hello@verified-domain.example", name: "Acme" }, // null clears it
});

Sends fail with template_not_enabled until a template is enabled for the project. defaultSubject applies when a send omits subject. A template-level from must sit on one of the project's verified sending domains — anything else is rejected with invalid_from; omit it to inherit the brand default.

Content versions

putContent saves a draft; publish makes a version live. Rendering always uses the published version, so you can stage copy safely.

// Start from the current document — a content shape is specific to its template.
const { draft, published, defaultContent } = await mailroom.templates.getContent("welcome");
const base = published?.content ?? draft?.content ?? defaultContent;

await mailroom.templates.putContent("welcome", { content: base }); // draft
await mailroom.templates.publish("welcome", { content: base }); // live
A content write replaces the template body

For document-backed templates the authored document is the content — publishing an unrelated object does not merely change some copy, it replaces the email. The write itself may pass validation and still leave every send failing at render time (invalid_content, or template "…" has no document). Always start from the document you read back, and test-send before publishing to a live template. There is no "unpublish": recovering means publishing a correct document again.

Multi-brand projects

Every template method takes an optional brand. Enablement, subject, sender, and content are stored per brand, so acme and acme-eu can ship the same template with different copy. Single-brand projects can ignore the parameter.

Reference