A layout is a subject line and a block of HTML that Leed fills in for each recipient. It is the what the email says half of a send; the who gets it half is a contact group. A layout does not know how it will be used, which is the point: the same one can back a blast today and a form’s response email tomorrow.
Where layouts live
Open the Design tab and pick Emails in the left library panel — the section is labeled Transactional email templates. The panel lists your layouts with a search box and a sort control offering Recently created and Name (A–Z). New email template opens a dialog that asks for one thing: a name. Create it and the canvas opens at /email-templates/<id>, with the layout’s name in the header and the subtitle Email template.
The same list appears, read-only, at Settings → Emails, where each row carries an Open template link back into this canvas. That screen is an inventory; nothing is editable there.
Who can edit one
Listing layouts and picking one from a dropdown needs only contact:read — Read Only and above. Creating, opening and updating a layout needs developer:write, whose minimum role is Content Publisher and which is normally reached through the Developer override rather than through a role. That override is a checkbox on Settings → Team; billing and developer access covers what else it unlocks and why it is granted sparingly.
| Action | API route | Privilege | Entitlement |
|---|---|---|---|
| List layouts | GET /api/templates/email | contact:read | none |
| Create a layout | POST /api/templates/email | developer:write | Custom themes and layouts |
| Open a layout | GET /api/templates/email/:emailTemplateId | developer:write | Custom themes and layouts |
| Update a layout | PUT /api/templates/email/:emailTemplateId | developer:write | Custom themes and layouts |
| Send a test | POST /api/email/test | contact:write | none |
| Delete a layout | — no route exists — | — | — |
Two things follow from that table and are worth stating plainly. There is no delete. A layout can be renamed and emptied but never removed, so a workspace accumulates layouts and naming them well matters more than it should. And the entitlement is enforced in the CMS, not in the API — the upgrade prompt is what a below-Growth author actually hits, because the access check refuses the route first anyway.
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 layout
The canvas has two fields and one button.
- Subject — the subject line. It is rendered through the same variable engine as the body, so
Hello {{firstName}}works here too. - Body — the HTML, edited in a code editor with word wrap on and no minimap.
- Test — sends the layout to yourself. It force-saves first: pressing Test cancels the pending autosave and writes the current subject and body immediately, so a test is never taken against a stale copy.
Edits autosave one second after you stop typing. There is no Save button and no dirty indicator; if you navigate away inside that one-second window the last keystrokes are lost, which is the one reason to pause before closing the tab.
Because the body is raw HTML you have complete control of the markup — and complete responsibility for it. Email clients are a decade behind browsers: use a table for structure rather than flexbox or grid, put every style inline rather than in a <style> block, give images explicit widths, and assume that anything clever degrades. A layout that renders correctly in the code editor’s preview-less pane and in your own webmail has still only been tested in one client.
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 a document you picked in the test dialog |
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. 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
Test sends the layout to you. The recipient is bound server-side to your signed-in account’s email address; the email field in the dialog is render data, so whatever you type there appears inside the message as the email variable and changes nothing about where the message goes. You cannot use the test dialog to mail a colleague.
The dialog has five fields — firstName, lastName, email, a page picker and an asset picker — and they populate the corresponding variables. 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 between Save and Send
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 a feature — it is what lets you pick the same one from a form’s settings and from the blast 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>