Template language
Templates use a Handlebars-style syntax. Placeholders in mjml, content, and subject are filled from the data object when you send an email.
{{variable}}, literals, or, {{#if}}, and {{#each}} work in both MJML and HTML content templates. :if, :each, and unquoted attributes like color={{color}} are MJML-only — they are handled by Mailcast’s MJML compiler.
Variables
{{name}} looks up name in data. Dotted paths walk nested maps: {{user.email}} reads data.user.email.
Hello {{name}}
With name set to Andrew in data, this renders Hello Andrew.
Variables can access nested structures using dot notation. For example, {{user.email}} reads data.user.email.
Missing keys and subkeys are ignored.
{{account.user.name}} will return nothing when account or user or name is missing.
Values from data are HTML-escaped (&, <, >, ", '). Newlines (\n, \r\n) become <br>.
{{message_body}}
With message_body set to Hi Andrew\n\nWelcome, this renders Hi Andrew<br><br>Welcome. <script> in a value renders as <script>.
Handlebars templates in data
By default, values in data are inserted as plain text. Template syntax inside a value is not expanded.
When the app supplies the template and the user writes a field such as a message body that itself uses variables, {{#if}}, or {{#each}}, list that field in substitute when you send an email:
{
"data": {
"name": "Andrew",
"message_body": "Hi {{name}}"
},
"substitute": ["message_body"]
}
Listed fields are rendered against the same data first, without HTML-escaping. The outer template then interpolates the result once, which escapes HTML and turns newlines into <br>. Unlisted fields are never re-parsed.
substitute fields support {{variable}}, {{#if}}, and {{#each}}. This will just exapand text data. No HTML is supported.
Dotted paths work the same as elsewhere: "user.bio" expands data.user.bio. Missing keys are skipped. A listed path that is not a string, or that contains invalid template syntax, is rejected.
If a key is also listed in markdown, Handlebars runs first, then CommonMark.
Markdown in data
By default, values in data are plain text. To render a field as CommonMark (emphasis, strong, links, paragraphs, line breaks), list it in markdown when you send an email:
{
"data": {
"message_body": "Hi **Andrew**\n\nSee [us](https://mailcast.io)"
},
"markdown": ["message_body"]
}
*italic* and _italic_ become <em>. **bold** becomes <strong>. [text](url) becomes a link. Single newlines become <br>. Blank lines start a new paragraph.
Raw HTML in the field is escaped. javascript: links are dropped. Unlisted fields are not parsed as Markdown.
Do not list fields that are used in HTML attributes (href, src). Markdown wraps content in tags and will break those attributes.
Literals
{{'red'}} and {{"red"}} always render red. A mustache may contain only a literal but they are usually used with or to provide a fallback.
Quotes inside a literal do not start another template. {{name or "{{name or \"hello world\"}}"}} is name when present, otherwise the string {{name or "hello world"}}.
Defaults
{{color or 'red'}} uses color when it is present, otherwise the next value. You can chain: {{color or fallback or 'red'}}.
null, false, and "" are considered missing and will trigger the or value. Any other value is considered present and will be displayed.
Attributes
Mustaches work inside quoted HTML attribute values in both MJML and content. Use the opposite quote from the attribute so the value does not close early:
<div style="color:{{color or 'red'}}">
<a href="{{url or 'https://example.com'}}">
<mj-text color="{{color or 'red'}}">Hello</mj-text>
In MJML only, you can also write an unquoted attribute whose whole value is a mustache. The compiler keeps it so it can be substituted when you send:
<mj-text color={{color}}>Hello</mj-text>
<mj-button href={{url}}>Continue</mj-button>
<mj-image src={{badge_url}} />
Unquoted color={{color}} is not valid HTML, so it does not work in content templates. MJML attributes that contain {{ are not type-checked at compile time.
Conditionals
{{#if name}}…{{/if}} includes the block when name is present. {{else}} is the fallback.
Hello {{#if name}}{{name}}{{else}}there{{/if}}
Conditions also accept not, and, or, and comparisons (==, !=, >, >=, <, <=):
{{#if age >= 13 and age <= 19}}teenager{{/if}}
{{#if not active}}Inactive{{/if}}
:if is MJML-only. The value is an expression: :if={{show}} compiles to {{#if show}}. There is no :else; use {{#if}} / {{else}} in the text when you need a fallback. In content templates, write {{#if}} directly.
<mj-text :if={{show}}>Welcome</mj-text>
This is equivalent to:
{{#if show}}
<mj-text>
Welcome
</mj-text>
{{/if}}
Loops
{{#each users as |user|}}…{{/each}} repeats the block for each item. Inside the block, {{user}} is the item and {{user.name}} is a field.
A missing or empty list renders nothing.
:each is MJML-only. The value is an expression: :each={{users as |user|}} compiles to {{#each users as |user|}}. In content templates, write {{#each}} directly:
<mj-text :each={{users as |user|}}>Hello {{user.name}}</mj-text>
This is equivalent to:
{{#each users as |user|}}
<mj-text>
Hello {{user.name}}
</mj-text>
{{/each}}
:if and :each can be combined. :each is wrapped inside :if.
Full example
Template
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text color="{{accent or 'red'}}">
Hello {{#if user.name}}{{user.name}}{{else}}there{{/if}}
</mj-text>
<mj-text :if={{promo}}>{{promo}}</mj-text>
<mj-text :each={{items as |item|}}>{{item.name}}</mj-text>
<mj-text>
{{#each items as |item|}}
{{item.name}} — {{item.price}}
{{/each}}
</mj-text>
<mj-button href="{{url or 'https://example.com'}}">
Continue
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>
Data
{
"user": { "name": "Andrew" },
"promo": "Free shipping",
"items": [
{ "name": "Apples", "price": "$4" },
{ "name": "Oranges", "price": "$3" }
],
"url": "https://mailcast.io"
}
accent is omitted, so the text color falls back to red.
Result
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text color="red">
Hello Andrew
</mj-text>
<mj-text>Free shipping</mj-text>
<mj-text>Apples</mj-text>
<mj-text>Oranges</mj-text>
<mj-text>
Apples — $4
Oranges — $3
</mj-text>
<mj-button href="https://mailcast.io">
Continue
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>