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.
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
orderNumberrendersdata.orderNumber. - Links and images interpolate
{{ path }}insidehref/src, e.g.href="{{ orderUrl }}". - Repeats (product strips, line-item lists) loop a block over an array path
such as
productsorlineItemsand expose each element under an alias, so an item field reads asproduct.nameorli.price. - Conditionals (
showIf) drop a block when a dotted path is falsy — a sale price only renders whenproduct.priceWasis 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:
| Variable | Source |
|---|---|
brandName | The brand's name |
postalAddress | The brand's postal address (compliance footer) |
unsubscribeUrl | Generated per recipient for marketing sends |
year | Current 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 optionallycategory,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, optionalprice.
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
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
TemplatesResource— generated method reference- REST: templates — the endpoints these methods call