Render context
Whenever Better Email renders Liquid, two namespaces are always there:
globalholds values you define — Global inputs and values published from the Global Script.contextholds read-only facts Better Email knows about the current render — whether it's a preview, which integration the email is connected to, what's in the email, and which block is rendering right now.
That's the whole model. If you define it, look in global. If the platform knows it, look in context.
Two terms this page relies on: a module is the reusable definition in the template; a block is one instance of a module placed in an email. The same module can appear as several blocks in one email.
{% if context.email.moduleKeys contains "hero_banner" %}
<!-- The email contains the Hero banner module -->
{% endif %}
The context variable
context is grouped by what each fact describes:
| Variable | Type | Description |
|---|---|---|
context.isPreview | boolean | true while rendering the in-product preview, false for the final exported email. |
context.integration | object or null | The integration the email is connected to. null when there is none. |
context.integration.id | number | Id of the integration. The stable value to branch on. |
context.integration.name | string or null | Display name of the integration, e.g. Lifecycle SFMC. |
context.integration.type | string or null | Technical type, e.g. SalesforceMarketingCloudIntegration. Useful for ESP-specific output. |
context.email | object or null | Facts about the email being rendered. null when no email is being rendered (e.g. in the Global Script). |
context.email.subject | string | The email's subject line, with its own Liquid already rendered. |
context.email.preheader | string | The email's preheader, with its own Liquid already rendered. |
context.email.moduleKeys | array of strings | The module keys of the modules used in the email — unique, in order of first appearance. |
context.block | object or null | Facts about the block currently rendering. null everywhere outside module code. |
context.block.id | string | Unique id of this block — distinct even when the same module is used twice in an email. |
context.block.moduleKey | string | The module key of the module this block is an instance of. |
context.block.previous | object or null | The block just before this one in the same content zone, as { id, moduleKey }. null for the first block of a zone. |
context.block.next | object or null | The block just after this one in the same content zone, as { id, moduleKey }. null for the last block of a zone. |
Availability: a group is null when the thing it describes doesn't exist
You never need to memorize an availability matrix. Each group of context is simply null until the thing it describes exists (the rendering pipeline shows exactly when each group comes into existence):
context.integrationexists on every Liquid surface: template base, modules, subject line, preheader, the Global Script, and Only show if conditions. It isnullwhen the email isn't connected to an integration (and in the Template Editor, where there is no email yet).context.isPreviewexists everywhere except Only show if conditions — what's visible must agree between the editor and every render of the same email, and preview and export differ onisPreviewby definition. Use it in template or module markup instead.context.emailis populated while an email is rendering: template base, module code, subject and preheader. In the Global Script it is partially populated —moduleKeysis real, butsubjectandpreheaderarenullthere because they are rendered after (and using) the script's output. In Only show if conditions and in the Template Editor (where no email exists) it isnull.context.blockis populated only inside module code, while a block renders. It isnullin the template base, subject, and preheader.
When in doubt, guard:
{% if context.email %}
{{ context.email.subject }}
{% endif %}
While the subject line or preheader is itself being rendered, context.email.subject and context.email.preheader contain the raw saved text, not the rendered output — a value cannot reference its own result. Everywhere else they are fully rendered.
Module keys
Every module in a template has a module key: a short, stable identifier in snake_case, such as hero_banner or footer. Better Email derives the key from the module name automatically, and you can change it in the Template Editor — select the module and edit the Module Key field next to the module name. Keys may only contain lowercase letters, numbers, and underscores, and must be unique within the template.
Module keys show up in context in two places:
context.email.moduleKeys— every module the marketer added to the email (each key once, in order of first appearance)context.block.moduleKey— the module the current block is an instance of ("which module am I?")
This makes it easy for the template base to adapt to the email's content:
{% unless context.email.moduleKeys contains "footer" %}
<!-- Marketer removed the footer module: render a minimal legal footer -->
<p style="font-size: 11px;">{{ global.company_name }} · {{ global.company_address }}</p>
{% endunless %}
Liquid that references a module key breaks silently if the key is renamed. Pick readable keys early and treat them like an API: rename the module name freely, but leave the key alone once templates depend on it.
TODO: Add a screenshot of the Module Key field in the Template Editor.
Examples
Reuse the subject line in the email body
A hero module can mirror the subject line so marketers only write the headline once:
<h1>{{ setting_key.headline | default: context.email.subject }}</h1>
Integration-driven branding in the Global Script
Because context.integration is available in the Global Script, multi-brand branching lives in one place:
{% if context.integration.id == 1 %}
{% set_global primary_color = "#FFF333" %}
{% elsif context.integration.id == 2 %}
{% set_global primary_color = "#0044CC" %}
{% endif %}
Branch on context.integration.id — it is always present when an integration is connected, and it is the only field the Template Editor's calculated-values panel can reconstruct (its integration selector carries just an id). name and type are best for display and ESP-specific markup, not conditions.
Restrict a module to one integration
In a module's Only show if condition:
context.integration.id == 42
The module only appears in the module sidebar (and renders) for emails connected to that integration.
React to modules used anywhere in the email
context.email.moduleKeys answers "is module X somewhere in this email?" — useful when one part of the email must react to content that lives elsewhere. A classic case is compliance text: if the marketer added the promo offer module, the footer in the template base must include the offer's terms:
{% if context.email.moduleKeys contains "promo_offer" %}
<p style="font-size: 11px; color: #6b7280;">
* Offer valid for new customers only. Terms and conditions apply.
</p>
{% endif %}
Because context.email.moduleKeys is also available in the Global Script, a flag that several modules need can be computed once there instead:
{% if context.email and context.email.moduleKeys contains "promo_offer" %}
{% set_global show_offer_terms = true %}
{% else %}
{% set_global show_offer_terms = false %}
{% endif %}
Guard with {% if context.email %} in the script — in the Template Editor's calculated-values panel there is no email, so context.email is null there.
For logic about the block directly before or after the current one, use context.block.previous / context.block.next instead — see the next example.
React to neighboring blocks
Inside module code, context.block.previous and context.block.next describe the surrounding blocks in the same content zone. A module can, for example, drop its own top spacing when it directly follows a full-bleed hero:
{% if context.block.previous and context.block.previous.moduleKey == "hero_banner" %}
{% assign top_padding = "0" %}
{% else %}
{% assign top_padding = "32px" %}
{% endif %}
Or a divider module can hide itself when it would be the last thing in the email:
{% if context.block.next %}
<hr />
{% endif %}
Both are null at the edges of a zone, so guard with {% if %} before reading their fields. Blocks in other content zones are never neighbors.
Unique ids per block
context.block.id is unique for every block in the email — useful for anchors or ids that must not collide when the same module is used twice:
<a id="block-{{ context.block.id }}"></a>
ESP-specific output
context.integration.type identifies the ESP, which pairs well with preview-aware rendering:
{% if context.isPreview %}
<p>Preview placeholder</p>
{% elsif context.integration.type == "SalesforceMarketingCloudIntegration" %}
%%[ /* AMPscript for the exported email */ ]%%
{% endif %}
context.isPreview works in the Global Script too, so set_global values can differ between preview and export. Use that power sparingly — the preview should stay visually representative of the delivered email.
Legacy names
Two older names remain supported so existing templates keep working. New templates should use the context equivalents:
| Legacy | Use instead |
|---|---|
meta.isPreview | context.isPreview |
global.integrationId | context.integration.id |