Output
Wrap any expression in double curly braces to render its value. The HTML body is automatically HTML-encoded to prevent markup injection — the subject line is plain text and is not encoded.
{{ firstName }}
{{ user.name }}
{{ order.total | prepend: "€" }}Dot notation accesses nested properties. Bracket notation works too and is useful for keys with special characters or dynamic lookups.
{{ user.address.city }}
{{ product["price-eur"] }}
{{ items[0].name }}Variables & Types
Parameters are passed as a JSON object when sending. All JSON types are available in templates: strings, numbers, booleans, arrays, and objects.
Send-time parameters (example)
{
"user": {
"name": "Ada Lovelace",
"vip": true,
"points": 1200
},
"items": [
{ "name": "Shirt", "price": 49.90 },
{ "name": "Trousers", "price": 89.00 }
],
"discount": 0.10
}Accessing values in the template
{{ user.name }} {# → Ada Lovelace #}
{{ user.vip }} {# → true #}
{{ user.points }} {# → 1200 #}
{{ items[0].name }} {# → Shirt #}
{{ items.size }} {# → 2 #}
{{ discount | times: 100 }} {# → 10.0 #}Accessing a property that does not exist returns an empty string — it does not cause an error. Use the default filter to provide a fallback value.
Default values
{{ user.nickname | default: user.name }}
{{ discount | default: 0 }}
{{ greeting | default: "Hello" }}Conditionals
Use if / elsif / else to branch on any truthy value. Empty strings, nil, and false are falsy; everything else is truthy.
{% if user.vip %}
<p>Welcome back, VIP member!</p>
{% elsif user.active %}
<p>Welcome back, {{ user.name }}.</p>
{% else %}
<p>Hello there!</p>
{% endif %}unless
unless is the inverse of if — the block renders when the condition is false.
{% unless user.unsubscribed %}
<a href="{{ unsubscribeUrl }}">Unsubscribe</a>
{% endunless %}Comparison operators
==Equal{% if status == "active" %}!=Not equal{% if plan != "free" %}>Greater than{% if points > 1000 %}<Less than{% if balance < 0 %}>=Greater than or equal{% if age >= 18 %}<=Less than or equal{% if items.size <= 5 %}containsString / array contains{% if tags contains "promo" %}Boolean operators
{% if user.vip and order.total > 100 %}
<p>Free shipping applied.</p>
{% endif %}
{% if user.guest or user.trial %}
<p>Sign up for full access.</p>
{% endif %}Inline if (ternary-style with assign)
{% assign label = "Standard" %}
{% if user.vip %}{% assign label = "VIP" %}{% endif %}
<span>{{ label }}</span>Loops
Iterate over arrays with for … in …. The loop variable is scoped to the block.
<ul>
{% for item in items %}
<li>{{ item.name }} — {{ item.price | prepend: "€" }}</li>
{% endfor %}
</ul>forloop object
Inside a loop, the forloop object exposes metadata about the current iteration.
forloop.index1-based positionforloop.index00-based positionforloop.rindexRemaining items (1-based)forloop.rindex0Remaining items (0-based)forloop.firsttrue on first iterationforloop.lasttrue on last iterationforloop.lengthTotal number of iterations{% for item in items %}
{% if forloop.first %}<ul>{% endif %}
<li>{{ forloop.index }}. {{ item.name }}</li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}limit and offset
{# First 3 items #}
{% for item in items limit: 3 %}
{{ item.name }}
{% endfor %}
{# Items 4-6 (0-based offset) #}
{% for item in items limit: 3 offset: 3 %}
{{ item.name }}
{% endfor %}Reversed iteration
{% for item in items reversed %}
{{ item.name }}
{% endfor %}break and continue
{% for item in items %}
{% if item.hidden %}{% continue %}{% endif %}
{{ item.name }}
{% endfor %}
{% for item in items %}
{% if forloop.index > 5 %}{% break %}{% endif %}
{{ item.name }}
{% endfor %}else (empty collection)
{% for item in items %}
<li>{{ item.name }}</li>
{% else %}
<li>No items found.</li>
{% endfor %}Iterating ranges
{% for i in (1..5) %}
<span>Step {{ i }}</span>
{% endfor %}Assign & Capture
assign creates a variable. capture renders a block into a string variable. Both are scoped to the template.
assign
{% assign greeting = "Hello, " | append: user.name %}
{{ greeting }}
{% assign total = items | map: "price" | sum %}
Total: {{ total | prepend: "€" }}
{% assign isLarge = false %}
{% if total > 500 %}{% assign isLarge = true %}{% endif %}capture
{% capture signature %}
Best regards,<br>
{{ sender.name }}<br>
<a href="mailto:{{ sender.email }}">{{ sender.email }}</a>
{% endcapture %}
{# Use it anywhere below #}
{{ signature }}Case / When
Use case / when as a switch statement. A when clause can match multiple values separated by commas. The optional else clause catches all unmatched cases.
{% case user.plan %}
{% when "free" %}
<p>Upgrade to unlock premium features.</p>
{% when "pro", "business" %}
<p>Thank you for being a Pro member.</p>
{% when "enterprise" %}
<p>Your dedicated account manager will be in touch.</p>
{% else %}
<p>Welcome!</p>
{% endcase %}Built-in Filters
Filters transform a value using the pipe | operator. Multiple filters can be chained — they are applied left to right.
{{ "hello world" | capitalize | append: "!" }}
{# → Hello world! #}
{{ items | map: "price" | sum | round: 2 | prepend: "€" }}
{# → €138.90 #}String filters
upcase"hello" | upcasedowncase"HELLO" | downcasecapitalize"hello world" | capitalizeappend"Hello" | append: " world"prepend"world" | prepend: "Hello "strip" hi " | striplstrip" hi " | lstriprstrip" hi " | rstripstrip_html"<b>hi</b>" | strip_htmlstrip_newlines"a\nb" | strip_newlinesnewline_to_br"a\nb" | newline_to_brtruncate"Hello world" | truncate: 8truncatewords"one two three" | truncatewords: 2replace"hi hi" | replace: "hi", "hey"replace_first"hi hi" | replace_first: "hi", "hey"remove"hello" | remove: "l"remove_first"hello" | remove_first: "l"split"a,b,c" | split: ","escape"<b>" | escapeurl_encode"hello world" | url_encodeurl_decode"hello+world" | url_decodebase64_encode"hello" | base64_encodebase64_decode"aGVsbG8=" | base64_decodesha256"secret" | sha256size"hello" | sizeNumber filters
abs-5 | absceil4.2 | ceilfloor4.9 | floorround4.567 | round: 2plus5 | plus: 3minus10 | minus: 3times5 | times: 3divided_by10 | divided_by: 3.0modulo10 | modulo: 3at_least3 | at_least: 5at_most10 | at_most: 5Array filters
firstitems | firstlastitems | lastsizeitems | sizejoinarr | join: ", "reverse[1,2,3] | reversesortarr | sort: "price"sort_naturalarr | sort_natural: "name"uniq[1,2,1] | uniqcompact[1,nil,2] | compactmapitems | map: "name"whereitems | where: "active", truesumitems | map: "price" | sumconcatarr1 | concat: arr2pusharr | push: "new"poparr | popDate filters
The date filter formats a date string or timestamp. Use now or today as the input for the current time.
dateorder.createdAt | date: "%d.%m.%Y"date"now" | date: "%H:%M"{# Common format codes #}
%Y → 2025 (4-digit year)
%m → 06 (2-digit month)
%d → 15 (2-digit day)
%H → 14 (hour, 24h)
%M → 30 (minute)
%S → 00 (second)
%A → Sunday (full weekday)
%B → June (full month name)Miscellaneous
defaultname | default: "Guest"jsonuser | jsonAsset Filters
MailQ adds two custom filters for images uploaded to your tenant's asset library in the portal. Reference assets by their exact filename.
asset_url — public URL
Returns the full public URL of the asset. The image is loaded by the recipient's email client from the MailQ image CDN. Requires external image loading to be enabled (the default for most clients).
<img src="{{ 'logo.png' | asset_url }}" alt="Logo" width="200">
<img src="{{ 'banner.jpg' | asset_url }}" alt="Banner">asset_inline — embedded MIME attachment
Embeds the asset directly in the email as an inline MIME attachment. The filter outputs a cid: reference and the image is attached automatically — no external image loading is required. This guarantees the image is visible even when clients block remote images.
<img src="{{ 'logo.png' | asset_inline }}" alt="Logo" width="200">Using the same filename multiple times in one template reuses the same attachment — the image is only embedded once. Inline attachments increase email size; use them for logos and brand images, not large photos.
Comparison
Unknown filename
If the filename does not match any uploaded asset, the filter returns an empty string. Filenames are case-sensitive and must include the extension (e.g. logo.png, not logo).
Partials
Partials are reusable template snippets — headers, footers, legal notices, signatures — managed in the portal. Include them in any template with {% include 'slug' %}. The partial matching the send language is resolved automatically at render time.
{% include 'email-header' %}
<p>Dear {{ user.name }},</p>
<p>Your order <strong>{{ order.number }}</strong> has shipped.</p>
{% include 'order-items-table' %}
{% include 'email-footer' %}Language resolution
If you send with language: "de", MailQ looks for a partial named email-header in the de locale. Missing partials render as empty — the email is never blocked by an unresolved include.
Partials cannot be sent directly via the API — they can only be included from a non-partial template. Create them in the portal with the Partial flag enabled.
Nesting
Partials can include other partials. Circular references are not detected and will cause a render timeout — keep nesting shallow.
Comments
Anything inside a
commentblock is stripped from the output. Use it to annotate templates or temporarily disable content.