Skip to main content

Rendering pipeline

Every preview and every export runs the same pipeline. Knowing its order explains almost everything that otherwise feels arbitrary about Liquid in Better Email: why the Global Script can't read the subject line, why Only show if can't see context.email, and why an {% assign %} from one block never shows up in another.

The stages

1. Global Script

The Global Script runs before anything else. It reads the raw Global inputs and the render contextcontext.isPreview, context.integration, and (while an email is rendering) context.email.moduleKeys — and publishes values with {% set_global %}. Everything downstream sees the result as global.*.

The email-scoped facts follow from the ordering: moduleKeys is known before the script runs (it only depends on which blocks are in the email), but context.email.subject and context.email.preheader are null here — they are rendered using the script's output, so giving the script access to them would be circular. context.block is always null: the script runs once, not per block. And where no email exists at all — the Template Editor's calculated-values panel and previews — context.email is null entirely, so guard with {% if context.email %}.

2. Visibility conditions

Only show if conditions on settings and inputs are evaluated next, and the values of hidden settings/inputs are dropped before rendering — a hidden input never leaks into the output, even if it still holds a value. Conditions can reference setting values, template.*, global.*, and context.integration.

context.isPreview is deliberately not available in conditions: what's visible must agree between the editor UI and every render of the same email, and preview and export differ on isPreview by definition. Use context.isPreview inside template or module markup instead.

Module-level Only show if works differently: it controls whether a module is available in the Email Editor's module list, not whether an already-added module renders. See conditional module visibility.

3. Dynamic authoring values

Input authoring fields in FX mode — hints, placeholders, select option names and values, color option values — are resolved now, with the same scope as visibility conditions plus the module's own setting values.

4. Subject line and preheader

The subject and preheader render before the email body, so the body can reference the finished values as context.email.subject and context.email.preheader. Inside the subject or preheader themselves, those fields contain the raw saved text — a value cannot reference its own result.

5. Template base

The template base renders with everything except context.block — there is no "current block" at this point.

6. Blocks

A block is one instance of a module placed in the email; the same module can appear as several blocks. Each block renders in isolation — independently of every other block, possibly in parallel: Better Email combines the template base and the block's module code into one document and renders them together. Two consequences:

  • {% assign %} variables set in the template base (before the content zone) are visible inside every block.
  • {% assign %} variables set in one block are not visible in any other block — that isolation is what keeps modules reusable.

Module code is the only place where context.block is populated: id, moduleKey, and the neighboring blocks as previous/next ({ id, moduleKey }, resolved within the block's content zone, null at the edges).

If one block needs a value another block also needs, don't try to pass it between them — compute it once in the Global Script and read it from global.*.

7. Post-processing

After Liquid, Better Email applies segmentation variants, replaces merge tags with the connected integration's personalization syntax, and applies any ESP-specific transformations (for example AMPscript handling for Salesforce Marketing Cloud). Merge tags are untouched by Liquid — they pass through and are substituted here.

What is available where

The full matrix, one row per place you can write Liquid:

SurfaceOwn setting valuestemplate.*global.*context.isPreview context.integrationcontext.emailcontext.block
Global Script— ¹✓ (raw inputs)✓ ⁴
Only show if conditions✓ ⁵
FX-mode authoring fields
Subject line & preheader✓ ²
Template base— ³
Module code

¹ The Global Script reads Global inputs as global.*; it has no other settings of its own.

² Raw saved text, not the rendered result — see stage 4.

³ The template base reaches its own settings through template.*.

⁴ Only moduleKeys, and only while an email is rendering — subject and preheader are null in the script because they render after it (see stage 4); in the Template Editor there is no email and context.email is null.

context.integration only — context.isPreview is not available in conditions (see stage 2).

The pattern behind the matrix: global.*, context.isPreview, and context.integration exist everywhere; context.email exists once an email is being rendered; context.block exists only while a block renders, inside module code. Setting values exist wherever the setting itself is in scope.

Rules of thumb

  • Compute once, read everywhere. Anything needed by more than one module belongs in the Global Script, not copy-pasted per module.
  • Branch on context.integration.id for integration-dependent behavior — it works in every surface, including the Global Script and Only show if.
  • Don't fight block isolation. If you're trying to smuggle state between blocks with assign, the design wants a global.* value instead.
  • Remember the preview runs the same pipeline. context.isPreview is the only difference between preview and export — keep preview branches visually representative.