Merge Tags and Dynamic Email Content Explained

A merge tag is a placeholder in your email template that your ESP replaces with recipient-specific data at the moment each message is sent. Write {{ customer.first_name }} once in your template, and every subscriber gets their actual name in that spot. The placeholder is gone by the time the email hits an inbox; what remains is a message that reads as if it were written specifically for that person.

That substitution happens for every field your contact database holds: name, company, plan tier, last login date, order total, or any custom attribute you track. It also extends to event properties when emails are triggered by user actions, so a transactional receipt can pull in a specific order number, item list, and shipping address that no static template could anticipate.

How Merge Tags Get Populated

Your ESP holds two inputs for every outgoing message: the template and the contact record. At send time, the platform scans the template for placeholder tokens, looks up the matching field in the contact record, and substitutes the value inline. If the field is present and populated, the substitution is seamless. If the field is empty and no fallback is defined, the tag renders as a blank space, which is how you end up with “Hi , welcome to the platform.”

The mechanism is the same regardless of platform. What varies is the token syntax, which differs meaningfully across ESPs.

Syntax Varies by Platform

Most modern ESPs and templating engines use one of three token formats:

Platform / EngineSyntax exampleNotes
Mailchimp*|FNAME|*Pipe-delimited, uppercase field names
HubSpot / HubL{{ contact.firstname }}Dot-notation object model
Klaviyo / Liquid{{ first_name }}Liquid engine; supports filters
Customer.io / Liquid{{ customer.first_name }}Scoped to customer or event objects
Handlebars (custom){{ firstName }}Logic-lite; common in Node.js stacks
Jinja2 (custom){{ first_name }}Python-based; used in Django/Flask pipelines
React EmailJSX props passed at render timeNo token syntax; data is a typed function argument

For developers building on top of a sending API, the choice of templating engine matters more than the specific syntax. React Email compiles templates as React components, so dynamic content flows through props rather than token substitution. You get type safety, IDE autocompletion, and conditional logic using standard JavaScript rather than a template-specific dialect. A transactional email fired from a backend API call can pass a fully typed payload, and the template renders exactly what the payload contains.

Fallback Values Prevent Silent Failures

Contact data is never 100% complete. New signups skip optional fields. CRM imports carry formatting inconsistencies. People change jobs and their company name goes stale. Any merge tag that references a field that might be empty needs a fallback.

A fallback is a word or phrase substituted when the referenced field is empty. In Liquid, the most concise form is the default filter:

Hi {{ customer.first_name | default: "there" }}

If first_name is present, the subscriber sees their name. If it is absent, they see “Hi there” rather than “Hi .” Customer.io’s documentation recommends using default rather than checking for empty strings with == blank or != blank, since the filter handles both null and empty-string cases in one expression.

Handlebars uses a helper approach:

Hi {{#if firstName}}{{firstName}}{{else}}there{{/if}}

A few rules that prevent the most common fallback failures:

  • Set a fallback for every optional field, not just names. Company name, job title, and location all need defaults if you use them.
  • Choose neutral fallbacks. “there,” “your team,” or “friend” work across audiences. Avoid platform-specific nicknames that look odd in unrelated contexts.
  • Test with a contact record that has every optional field blank. Seeing the email with no data populated surfaces problems that a full-data test contact hides.

Conditional Blocks: Showing Different Content to Different Recipients

Fallbacks handle missing values for a single field. Conditional blocks handle cases where the entire shape of the email changes depending on who receives it.

In Liquid, used by Klaviyo, Customer.io, and Vero, conditional logic looks like this:

{% if customer.plan == "pro" %}
  You have access to advanced analytics. Here's your weekly summary:
  {{ event.analytics_summary }}
{% elsif customer.plan == "starter" %}
  Upgrade to Pro to unlock advanced analytics.
{% else %}
  Get started by choosing a plan.
{% endif %}

The ESP evaluates the condition for each recipient at send time and renders only the matching branch. Everyone else’s template renders a different block entirely. One template definition, multiple rendered outputs.

This is where the distinction between merge tags (field substitution) and dynamic content (block-level conditional rendering) matters in practice. Merge tags personalize text within a static layout. Conditional blocks change which sections of the layout exist for a given recipient. Used together, they cover most personalization requirements without needing separate template versions per segment.

For developers building with a sending email API, the same logic is often handled upstream in application code, passing only the final values to the template rather than embedding the conditionals inside the template itself. Both approaches are valid; the right choice depends on whether the personalization logic should live in the template layer or the application layer.

Data Sources: Contact Attributes vs. Event Properties

Merge tags draw from two distinct data sources, and mixing them up causes silent failures.

Contact attributes are stored fields tied to a person in your contact database: name, email, company, lifecycle stage, custom properties you define. These are available in any email sent to that contact, whether triggered manually, on a schedule, or via automation.

Event properties are attached to a specific action. When a user completes a purchase, that event carries an order ID, item list, and total. When they start a trial, the event carries the plan they selected and the trial end date. Customer.io’s documentation is explicit: event properties can only be used in messages triggered by the relevant event. A weekly newsletter cannot pull in last week’s purchase data unless that data has been copied to a contact attribute first.

The practical implication: if you want a recurring email to reference behavioral data, write that data to a contact attribute at the time the event fires, then reference the attribute in your template. Trying to reference event properties in non-triggered emails is a common source of blank merge fields that are hard to trace.

Four Pitfalls Worth Knowing

1. Over-personalization that reads as surveillance. Using browsing history or purchase data in the subject line (“We saw you looked at the Pro plan 3 times…”) creates discomfort rather than relevance. Name personalization and purchase confirmation details feel appropriate. Behavioral data that signals observation often does not.

2. Capitalization inconsistency from CRM imports. A contact record with first_name: "JOHN" or first_name: "john" produces an awkward greeting. Liquid’s capitalize filter ({{ customer.first_name | capitalize }}) normalizes the output. Build this into your templates rather than relying on data cleanup.

3. Token syntax errors that send literally. A typo like {{ custmer.first_name }} does not throw an error in most ESPs; it renders the raw token string or a blank. The only reliable check is a pre-send test with real data across multiple contact records, including records with incomplete data.

4. Scope confusion between contact and event. Referencing {{ event.order_total }} in a lifecycle email (not triggered by an order event) will produce a blank or an error depending on the platform. Map your data sources before writing your templates, not after.

Frequently Asked Questions

What is the difference between a merge tag and a personalization token?

They refer to the same concept under different names. ESPs call the same mechanism “merge tags” (Mailchimp), “personalization tokens” (HubSpot), “substitution strings” (SendGrid), or “personalization fields” depending on the platform. In all cases, it is a placeholder in the template that gets replaced with recipient-specific data at send time.

What happens if a merge tag references an empty field?

Without a fallback, most ESPs substitute a blank string, so “Hi {{ first_name }}” becomes “Hi .” With a fallback defined using the default filter (Liquid) or an if/else block (Handlebars), the message renders the fallback text instead. Always define fallbacks for every optional field used in a template.

Can I use merge tags in email subject lines?

Yes. Subject line merge tags work the same as body merge tags and support fallback values. Subject line personalization is widely used because the recipient’s name or a relevant data point appears before the email is opened, adding immediate context. The same caveat about fallbacks applies: a missing value with no fallback will produce a blank space in the subject line.

What is the difference between merge tags and conditional content blocks?

Merge tags substitute a single value (a name, a number, a date) within an otherwise static layout. Conditional content blocks show or hide entire sections of the email based on recipient data. A promotion email might use a merge tag for the recipient’s name and a conditional block to show a different offer to free-plan users versus paid users. Both use the same templating language but serve different purposes.

Why are my event property merge tags rendering as blank in scheduled emails?

Event properties are only available in messages triggered by a specific event. A scheduled or broadcast email does not have an associated event, so event-scoped references ({{ event.property }}) return blank. If you need behavioral data in a non-triggered email, write the relevant data to a contact attribute when the event fires and reference the contact attribute in your template instead.

Do merge tags work differently in React Email compared to other template engines?

React Email does not use token substitution syntax. Instead, templates are React components that receive typed props at render time. Dynamic content flows through standard JavaScript: conditional rendering uses {condition && <Block />}, and personalized values are passed as typed arguments. This eliminates the risk of token typos and gives developers IDE autocompletion and compile-time type checking that string-based merge tags cannot provide.