An email template is a partially completed email: a seed document of blocks, a default subject and preview text, and a width, rendered through your company’s email brand. It is the what the email says half of a send; the who gets it half is chosen per email. A template does not know how it will be used, which is the point: the same one can be the starting point for a blast today and a form’s response email tomorrow. Older raw-HTML layouts still send, and are covered at the end of this page.
Where templates live
Open the Design tab and pick Email templates in the left library panel. Brand settings is pinned at the top, then two groups: Your templates and Built-in starters. Each row shows the template’s kind and width and, when forms use it, how many. The panel has a search box and a sort control offering Recently created and Name (A–Z); New template asks for a name and opens the new template at /email-templates/<id>.
The built-in starters are created for every workspace the first time the library is opened: Newsletter, Blog Post, Release Notes, Product Launch, Event Invitation, Welcome, Promotion, Win-back, Survey, Press Release, Gated Asset — which answers a form submission with the asset it unlocked — and Blank, which holds only the masthead and the compliance footer.
Settings → Emails does not list these templates. It shows a read-only list of the older raw-HTML layouts, and its Open template links cannot open them — see how email works in Leed.
Who can edit one
Listing templates, opening one read-only and picking one in Engage ▸ Emails needs only contact:read — Read Only and above. Creating, editing, testing and deleting a template need contact:write, whose minimum role is Content Publisher, and creating one additionally needs Growth. The Developer access override is no longer involved; it governs only the older raw-HTML layouts.
| Action | API route | Privilege | Entitlement |
|---|---|---|---|
| List templates | GET /api/email-templates | contact:read | none |
| Open a template | GET /api/email-templates/:id | contact:read | none in the API; the CMS editor needs Custom themes and layouts |
| Create a template | POST /api/email-templates | contact:write | Custom themes and layouts |
| Save an email as a template | POST /api/email-templates/from-email/:id | contact:write | Custom themes and layouts |
| Update a template | PUT /api/email-templates/:id | contact:write | none |
| Send a test | POST /api/email-templates/:id/test | contact:write | none |
| Delete a template | DELETE /api/email-templates/:id | contact:write | none — refused with 409 while a form uses it |
| Read or update the brand | GET / PUT /api/email-brand | contact:read / contact:write | none in the API; the CMS editor needs Custom themes and layouts |
Two things follow from that table. A template in use cannot be deleted: while any form answers submissions with it, Delete is disabled and names the forms, and the API refuses with 409 — detach it from those forms first. Emails already made from a template are unaffected by deleting it, because each email owns a copy. And only creating is gated in the API: updating and deleting stay open so a workspace that has dropped below Growth can still repair or remove a template it is sending with, even though the CMS shows it the upgrade prompt instead of the editor.
Where the entitlement lands on your plan is on feature availability by plan, and what a gate looks like from inside the product is on when a feature is gated.
Editing a template
A template opens with a setup card above its body:
- Name and Description.
- Default subject and Default preview text — what every email made from the template starts with. Preview text is the line inboxes show after the subject.
- Width — Narrow (600) or Wide (720).
Colors, logo and footer are not set here; they come from Brand settings, below. The body is a block editor holding the whole email: the masthead, your blocks, and the compliance footer, which renders your footer links, postal address and the unsubscribe link. Insert blocks from the insert control or by typing /. Generate with AI (Mod + J) on the toolbar writes or rewrites blocks from a prompt, and fills in the name, subject or preview text if they are empty.
The header reads like Email template · Narrow · 600px · 3 placeholders · 2 forms reply with it · Edited 5m ago, and carries Copy template, which duplicates the saved template as Copy of … and opens it, and Send test, which mails the template to you with sample data in every unfilled block. Delete is in the overflow menu. The right rail has a Preview tab — the email in light and dark mode — and a Forms tab listing the forms that answer submissions with this template and whether each one supplies everything it needs.
Edits save automatically about a second after you stop. There is no Save button; a chip in the header reads Saving…, Saved or Unsaved changes. Several people can edit one template at once and see each other’s cursors.
Normal blocks and placeholders
Every block in a template is one of two kinds. A normal block — Text, Heading, Image, Button, Story row, Details list or Divider — is sent exactly as written. A placeholder is a slot every email made from the template has to fill: it shows a label, an optional hint for whoever fills it, and a Filled by choice. The insert menu has a section for each kind — Content and Placeholder (Image, Text, Button, Story row, Details list and Dynamic pages) — and a block’s gutter menu offers Turn into placeholder, which clears its content, and Turn into normal block. Deleting a placeholder asks for confirmation in place.
Filled by decides who supplies the slot. Left as Emailer, the person composing each email fills it, and Send stays disabled until they have. For a form’s response email it can instead be filled automatically by a setting on the form, the page it sits on, the gated asset, the submission or the contact. A Dynamic pages block is always filled by your site at send time — the newest pages, the most viewed, or pages recommended for each recipient — so it never holds up a send.
flowchart LR
T["Template<br/>normal blocks + placeholders"] --> E["Engage ▸ Emails draft<br/>a copy the emailer owns"]
E --> P{"Every placeholder,<br/>subject and preview text filled?"}
P -- "no" --> W["Send disabled<br/>tooltip names what is missing"]
P -- "yes" --> S["Send"]
T --> F["A form's response email"]
F --> A["Placeholders filled from the form,<br/>its asset and the submission"]
A --> M["Sent to the submitter"]
Merge fields
The toolbar and the / menu insert merge fields in four groups. Contact — First name, Last name, Full name, Email and Homepage link — fills on every send. Form asset (Asset name, Download link), Form page (Page title, Page link) and Submission fill on a form’s response email. A blast can fill the asset and page fields when the emailer picks an asset or page for it; submission fields cannot be filled on a blast, so an email that uses one cannot be sent from Engage ▸ Emails.
Brand settings
Brand settings, pinned above the templates, is one set for the whole company: the logo and a dark-mode logo, whether to show your name beside it, an accent color for buttons and header rules, the footer’s postal address and links, and the Look — a palette preset with per-mode color overrides. Every template and every email renders with it. When an email is sent, the look it rendered with is frozen onto it, so changing the brand later never alters a send that has already gone out.
Older raw-HTML layouts
Before templates, an email was a layout: a subject line and a block of raw HTML rendered with Mustache. Layouts can no longer be created or opened in the CMS — the API routes under /api/templates/email remain, where listing needs contact:read and creating, opening or updating a layout needs developer:write — but existing ones still send. The legacy composer at /emails sends a layout to a contact group, and a form that already answered submissions with one keeps doing so (its settings show Current legacy template). Everything from here to the end of the page applies to those layouts only; an email built from a template gets its tracking pixel and unsubscribe link from the shell and footer, and never needs any of it.
Filling in the recipient
These are the values Leed passes to every render.
| Variable | Type | Braces to use | Always present? | What it contains |
|---|---|---|---|---|
pixel | URL | {{{ }}} | Yes | The per-recipient tracking pixel. Put it in an <img> src |
unsubscribe | URL | {{{ }}} | Yes | The per-recipient unsubscribe URL. Mandatory — see below |
firstName | Text | {{ }} | Yes, may be empty | The recipient’s first name, or an empty string |
lastName | Text | {{ }} | Yes, may be empty | The recipient’s last name, or an empty string |
email | Text | {{ }} | Yes | The recipient’s email address |
name | Text | {{ }} | Yes | firstName lastName, falling back to firstName, then lastName, then email — in that order |
homepage | URL | {{{ }}} | Yes | A tracked link to your site’s home page |
asset | Object | see below | No | Present when the send carries a document — a form’s gated download, or in a test a sample document |
page | Object | see below | No | On a blast, present only when the composer’s Page field is set. In a test, always present |
recommendations | Array of objects | see below | Yes on a real send | Three published pages from your site |
The object variables carry these paths. recommendations is an array whose entries have exactly the same shape as page, so iterate it with a section: {{#recommendations}}…{{/recommendations}}.
| Path | Type | Notes |
|---|---|---|
asset.href | URL | A tracked download link — use triple braces |
asset.filename | Text | The stored file name, e.g. whitepaper.pdf |
asset.name | Text | The asset’s display name in your library |
page.title | Text | |
page.summary | Text | |
page.href | URL | A tracked link — use triple braces |
page.featureImage.src | URL | Absolute, on your site domain. Absent when the page has no feature image |
page.featureImage.alt | Text | Falls back to unknown when the image carries no alt text |
page.publishedAt | Date | Absent when the page has no publish date |
recommendations[].* | — | Same fields as page |
Text versus URLs: double braces and triple braces
This is the rule with teeth, and it is the one that breaks layouts.
{{ }} HTML-escapes its value. Mustache’s escape set is larger than most people expect — it includes / and = as well as &, <, >, ", ' and a backtick. So a URL written in double braces comes out mangled:
<!-- Wrong: double braces escape the slashes -->
<a href="{{unsubscribe}}">Unsubscribe</a>renders as
<a href="https://example.com/e/o/b_7fa2/9c31d4.unsubscribe">Unsubscribe</a>A browser will still follow that link, because it decodes the entities when it parses the attribute — which is exactly why the mistake survives every visual check you make. What does not survive it is the unsubscribe check below, which compares raw strings.
The fix is one character. Use triple braces — or the equivalent {{&unsubscribe}} — for every URL:
<!-- Right -->
<a href="{{{unsubscribe}}}">Unsubscribe</a>
<img src="{{{pixel}}}" width="1" height="1" alt="" />Triple-brace every one of unsubscribe, pixel, homepage, page.href, asset.href and recommendations[].href. Use plain double braces for names, titles, summaries and anything else that is text — escaping is doing its job there, and a contact whose company is written Smith & Co will thank you.
The unsubscribe link is mandatory
After rendering and after link rewriting, and immediately before handing the message to the mail service, Leed searches the finished HTML for the exact unsubscribe URL it generated for that recipient. If the string is not there, the send throws:
Unsubscribe link not detected in email. emailId: <id> emailTemplateId: <id> companyId: <id>The page a recipient lands on when they click it, and the two templates you can override to brand that page, are described in unsubscribes and opt-outs.
Testing a layout
With the layout editor gone, a layout’s only test is Send a test to yourself in the legacy composer. It sends the selected layout to you — the recipient is bound server-side to your signed-in account’s email address — with your own name and address as the recipient data and the composer’s Page as the page variable. A test send is classified transactional: it works on every plan including Free, and it does not touch the marketing allotment.
Links in a layout are rewritten at send time
After Mustache renders the body, Leed scans it for anchors pointing at your own site and converts each one into a per-recipient tracked URL. You do not have to do anything to opt in — writing an ordinary link is enough.
The scan is deliberately narrow, and everything below is left exactly as you wrote it:
- Links to any host that is not your site domain (or
www.plus your site domain). - Links whose path already contains
/e/— already-tracked URLs are never double-wrapped. - Relative hrefs.
href="/pricing"is not rewritten; writehref="https://yoursite.example/pricing"if you want the click counted. - Anchors written with single quotes. The scan matches
href="…"with straight double quotes only. - Anything that is not an
<a href>— a bare URL in text, a linked image’s surrounding markup, a button built from a<form>.
The URLs that variables give you (homepage, page.href, asset.href, recommendations[].href, pixel, unsubscribe) are already tracked links and are unaffected by this pass. What the resulting URL is made of, what each hit records, and what tracking cannot tell you are all in tracked links and open tracking.
What happens at send time
flowchart TD
A["Stored subject + HTML body"] --> B["Mustache render<br/>with this recipient's data"]
B --> C["Rewrite customer-domain anchors<br/>into per-recipient /e/ URLs"]
C --> D{"Does the rendered HTML contain<br/>the exact unsubscribe URL?"}
D -- "no" --> E["Throw:<br/>Unsubscribe link not detected in email<br/>this recipient is never mailed"]
D -- "yes" --> F["Send from no-reply@ your site domain<br/>return path bouncer+batch=contact"]
F --> G["Stamp the sent time on<br/>the recipient's row"]
The failure edge is the whole reason this diagram exists. It happens once per recipient, after the batch has already been accepted, and it leaves no trace anywhere in the CMS.
One layout, many uses
A layout does not know how it will be used. That is what let the same one serve a form’s response email and a blast from the legacy composer — but it means the layout has to survive contexts it was not written for.
Name layouts by purpose rather than by campaign: Whitepaper delivery, Monthly digest, Webinar follow-up. A name like July send is useless the second time you open the dropdown, and since layouts cannot be deleted, every bad name is permanent.
Keep each layout self-contained, and guard the variables that are not always there. page is absent on a blast where nobody set the Page field, and asset is absent on anything that is not delivering a file. An unguarded {{page.title}} renders as nothing, which is usually fine; an unguarded <a href="{{{page.href}}}">Read the post</a> renders as a link to nowhere, which is not. Wrap optional blocks in a section:
{{#page}}
<tr><td>
<a href="{{{page.href}}}" style="color:#c6005c;">{{page.title}}</a>
<p style="margin:4px 0 0;">{{page.summary}}</p>
</td></tr>
{{/page}}A layout attached to a form also delivers the gated asset, which is the one case where asset is reliably present — see response emails and gated downloads.
A minimal layout you can paste in and edit
This renders on every recipient, satisfies the unsubscribe check, and degrades sensibly when page is absent. Replace the colors and the copy; keep the structure and the last two rows.
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f5f5;padding:24px 0;">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;font-family:Arial,Helvetica,sans-serif;color:#171717;">
<tr>
<td style="padding:24px 32px 8px;">
<p style="margin:0;font-size:16px;">Hi {{firstName}},</p>
</td>
</tr>
<tr>
<td style="padding:0 32px 16px;font-size:15px;line-height:22px;">
<p style="margin:0;">Write the message here.</p>
</td>
</tr>
{{#page}}
<tr>
<td style="padding:0 32px 16px;">
<a href="{{{page.href}}}" style="color:#c6005c;font-size:15px;">{{page.title}}</a>
<p style="margin:4px 0 0;font-size:13px;color:#525252;">{{page.summary}}</p>
</td>
</tr>
{{/page}}
{{#asset}}
<tr>
<td style="padding:0 32px 16px;">
<a href="{{{asset.href}}}" style="color:#c6005c;font-size:15px;">Download {{asset.name}}</a>
</td>
</tr>
{{/asset}}
<tr>
<td style="padding:16px 32px 24px;font-size:12px;color:#737373;border-top:1px solid #e5e5e5;">
<a href="{{{homepage}}}" style="color:#737373;">Visit our site</a>
·
<a href="{{{unsubscribe}}}" style="color:#737373;">Unsubscribe</a>
</td>
</tr>
<tr>
<td style="line-height:1px;font-size:1px;">
<img src="{{{pixel}}}" width="1" height="1" alt="" style="display:block;" />
</td>
</tr>
</table>
</td>
</tr>
</table>