Email Layouts

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.

ActionAPI routePrivilegeEntitlement
List templatesGET /api/email-templatescontact:readnone
Open a templateGET /api/email-templates/:idcontact:readnone in the API; the CMS editor needs Custom themes and layouts
Create a templatePOST /api/email-templatescontact:writeCustom themes and layouts
Save an email as a templatePOST /api/email-templates/from-email/:idcontact:writeCustom themes and layouts
Update a templatePUT /api/email-templates/:idcontact:writenone
Send a testPOST /api/email-templates/:id/testcontact:writenone
Delete a templateDELETE /api/email-templates/:idcontact:writenone — refused with 409 while a form uses it
Read or update the brandGET / PUT /api/email-brandcontact:read / contact:writenone 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.

The layout canvas on a below-Growth workspace, with the upgrade prompt filling the editor area

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.

VariableTypeBraces to useAlways present?What it contains
pixelURL{{{ }}}YesThe per-recipient tracking pixel. Put it in an <img> src
unsubscribeURL{{{ }}}YesThe per-recipient unsubscribe URL. Mandatory — see below
firstNameText{{ }}Yes, may be emptyThe recipient’s first name, or an empty string
lastNameText{{ }}Yes, may be emptyThe recipient’s last name, or an empty string
emailText{{ }}YesThe recipient’s email address
nameText{{ }}YesfirstName lastName, falling back to firstName, then lastName, then email — in that order
homepageURL{{{ }}}YesA tracked link to your site’s home page
assetObjectsee belowNoPresent when the send carries a document — a form’s gated download, or in a test a sample document
pageObjectsee belowNoOn a blast, present only when the composer’s Page field is set. In a test, always present
recommendationsArray of objectssee belowYes on a real sendThree 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}}.

PathTypeNotes
asset.hrefURLA tracked download link — use triple braces
asset.filenameTextThe stored file name, e.g. whitepaper.pdf
asset.nameTextThe asset’s display name in your library
page.titleText
page.summaryText
page.hrefURLA tracked link — use triple braces
page.featureImage.srcURLAbsolute, on your site domain. Absent when the page has no feature image
page.featureImage.altTextFalls back to unknown when the image carries no alt text
page.publishedAtDateAbsent 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:&#x2F;&#x2F;example.com&#x2F;e&#x2F;o&#x2F;b_7fa2&#x2F;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.

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.

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; write href="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>
            &nbsp;&middot;&nbsp;
            <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>
ESC