Template syntax
A small Mustache subset, logic-light on purpose. The renderer treats your model as untrusted end-user data, so it HTML-escapes output by default. That default alone rules out code execution and template injection.
The whole language
The renderer supports these tags and nothing else. Whitespace inside a tag is trimmed, so {{ name }} and {{name}}are the same, and a section's closing name matches its opening name exactly.
| Syntax | Meaning | Escaping |
|---|---|---|
{{ name }} | Interpolation: inserts the value | HTML-escaped |
{{{ name }}} | Raw interpolation, triple mustache | Not escaped |
{{& name }} | Raw interpolation, ampersand form | Not escaped |
{{# section }} … {{/ section }} | Section: renders once when the value is truthy, iterates when it is an array | Tags inside follow their own rules |
{{^ section }} … {{/ section }} | Inverted section: renders when the value is falsy or missing | Tags inside follow their own rules |
{{! comment }} | Comment: produces no output | None |
{{ a.b.c }} | Dotted path: walks into nested objects | HTML-escaped |
{{ . }} | Current item inside a section over an array of scalars | HTML-escaped |
Escaping
{{ name }} escapes the five characters & < > " ', so a value like <script> arrives as inert text. Use it for anything that lands inside HTML. Reach for {{{ name }}} or {{& name }} when the value is trusted HTML you generated yourself and want rendered as markup. The plain-text body has no markup to protect, so {{ name }} is the right choice throughout it.
Template : Hi {{ name }}
Model : { "name": "<b>Ada</b>" }
Output : Hi <b>Ada</b>
Template : {{{ html }}}
Model : { "html": "<b>bold</b>" }
Output : <b>bold</b>How values resolve
- Missing variables render as empty.
a{{ nope }}bwith an empty model producesab. A typo drops the placeholder rather than raising an error, so preview with a complete model before you ship. - Scalars render directly. Strings render as-is; numbers and booleans render their text form (
42,true). - Objects and arrays render as empty as a plain variable.
{{ items }}whereitemsis an array produces nothing. You reach the contents through a section. - Truthiness decides a section body.
null,false, the empty string and the empty array are falsy. Numbers, objects, non-empty strings and non-empty arrays are truthy. - Lookup climbs the context stack. Inside a section the current item is searched first, then the enclosing scopes outward, so a body can mix per-item fields with top-level ones such as
product.
Sections in practice
A section over an array repeats its body once per item, with each item as the current scope:
Template : {{# items }}[{{ name }} ×{{ quantity }}]{{/ items }}
Model : { "items": [ { "name": "Cap", "quantity": 2 },
{ "name": "Mug", "quantity": 1 } ] }
Output : [Cap ×2][Mug ×1]An array of bare scalars uses {{ . }} for the item itself:
Template : {{# tags }}{{ . }} {{/ tags }}
Model : { "tags": ["new", "beta"] }
Output : new betaA single flag toggles a block, and its inverse fills the empty case:
Template : {{# premium }}Thanks for going Pro.{{/ premium }}{{^ premium }}Upgrade any time.{{/ premium }}
Model : { "premium": false }
Output : Upgrade any time.Subjects render too
The subject goes through the same renderer: Welcome to {{ product }} becomes Welcome to Acme. One model fills the subject, the HTML body and the text body together on a send. Fields you set directly on the send request override the rendered value, so passing a subject alongside the template replaces it for that one message.
HTML and plain text
A template carries an HTML body, a plain-text body, or both. Shipping both is the recommended shape: clients that render HTML get the designed version, and clients or filters that prefer text get a clean fallback. Give both bodies the same variable names so one model fills them at once. Every template in the library follows this pattern.
Layouts
A layout is a reusable wrapper for the chrome every mail shares: the logo, the postal address, the unsubscribe link. A template picks a layout, and the layout wraps the template's rendered body. The wrapper embeds that body through a raw contentvariable so the body's own HTML survives:
<table role="presentation" width="100%">
<tr><td>{{ product }}</td></tr>
<tr><td>{{{ content }}}</td></tr>
<tr><td>Acme GmbH · <a href="{{ unsubscribe_url }}">Unsubscribe</a></td></tr>
</table>The raw {{{ content }}} placeholder (or the {{& content }} form) is required. An escaped {{ content }}would show the mail's markup as text, so the editor blocks saving until a raw placeholder is present. The wrapper sees the same model as the template plus the injected content, so it can use variables such as product and unsubscribe_url too.
Preview before you send
The render endpoint runs the real renderer server-side and sends nothing, so you see the exact output for a given model:
POST /api/v2/server/templates/{permalink}/render
{ "template_model": { … } } # returns subject, html_body, text_body{{> shared }}), lambdas and set-delimiter ({{= =}}) are left out on purpose. For shared header or footer chrome, reach for a layout. For filters, arithmetic or branching logic, compute the value in your application, where it can be tested, and pass the finished string in the model. Section nesting is capped at 32 levels and rendered output at 512 KiB; a template past either limit fails to render rather than running away. See the template library for ready-made bodies and Sending email for the send call.