Skip to main content

Render context

Whenever Better Email renders Liquid, two namespaces are always there:

  • global holds values you define — Global inputs and values published from the Global Script.
  • context holds 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:

VariableTypeDescription
context.isPreviewbooleantrue while rendering the in-product preview, false for the final exported email.
context.integrationobject or nullThe integration the email is connected to. null when there is none.
context.integration.idnumberId of the integration. The stable value to branch on.
context.integration.namestring or nullDisplay name of the integration, e.g. Lifecycle SFMC.
context.integration.typestring or nullTechnical type, e.g. SalesforceMarketingCloudIntegration. Useful for ESP-specific output.
context.emailobject or nullFacts about the email being rendered. null when no email is being rendered (e.g. in the Global Script).
context.email.subjectstringThe email's subject line, with its own Liquid already rendered.
context.email.preheaderstringThe email's preheader, with its own Liquid already rendered.
context.email.moduleKeysarray of stringsThe module keys of the modules used in the email — unique, in order of first appearance.
context.blockobject or nullFacts about the block currently rendering. null everywhere outside module code.
context.block.idstringUnique id of this block — distinct even when the same module is used twice in an email.
context.block.moduleKeystringThe module key of the module this block is an instance of.
context.block.previousobject or nullThe block just before this one in the same content zone, as { id, moduleKey }. null for the first block of a zone.
context.block.nextobject or nullThe 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.integration exists on every Liquid surface: template base, modules, subject line, preheader, the Global Script, and Only show if conditions. It is null when the email isn't connected to an integration (and in the Template Editor, where there is no email yet).
  • context.isPreview exists 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 on isPreview by definition. Use it in template or module markup instead.
  • context.email is populated while an email is rendering: template base, module code, subject and preheader. In the Global Script it is partially populated — moduleKeys is real, but subject and preheader are null there 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 is null.
  • context.block is populated only inside module code, while a block renders. It is null in the template base, subject, and preheader.

When in doubt, guard:

{% if context.email %}
{{ context.email.subject }}
{% endif %}
Self-reference in subject and preheader

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 %}
Keep keys stable

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 %}
Keep preview representative

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:

LegacyUse instead
meta.isPreviewcontext.isPreview
global.integrationIdcontext.integration.id