GLOSSARY

What is a Merge Variable?

A merge variable is a named placeholder inside a message template (written as {{firstName}}) that the sending engine replaces with a value from each recipient’s contact record at send time, so one template produces a personalised message for every contact.

Free forever plan · No credit card required · Cancel anytime

Quick definition

A merge variable is a named placeholder inside a message template (written as {{firstName}}) that the sending engine replaces with a value from each recipient’s contact record at send time, so one template produces a personalised message for every contact.

In a single sentence: {{firstName}} becomes Maya for Maya and Deniz for Deniz.

What it means

A merge variable is a named slot in a template. You write the message once, drop {{firstName}} where the name goes, and the sending engine looks up firstName on each contact record and swaps the value in as it renders that recipient's copy. One template, one review, one approval, and a thousand messages that each address a specific person.

The mechanism is old. Word processors called it mail merge in the 1980s, Mailchimp calls the syntax merge tags, Salesforce calls it merge fields, HubSpot calls it a personalisation token. The vocabulary drifted; the behaviour did not. What changed is the stakes. A mail-merge slip in a printed letter cost one envelope. The same slip in an outbound campaign ships Hi {{firstName}}, to four hundred people who now know exactly how the message was made, and on a DM channel it hands each of them a reason to report you.

The interesting part of merge variables is therefore not the substitution. It is what happens on the rows where the value is not there, which is always more rows than you think.

Syntax families and why we use double braces

Four conventions are common in the wild:

  • {{firstName}}: double brace. The de facto standard in modern outreach tooling and our convention throughout Pinlyx.
  • {firstName}: single brace. Readable, and a direct collision with spintax.
  • %%FIRST_NAME%% and *|FNAME|*: legacy email service provider syntaxes. Ugly on purpose, because nothing else in an email body looks like that.
  • ${firstName}: borrowed from templating languages. Fine in code, risky in a message body where a currency symbol is ordinary text.

The choice matters because a bulk-send pipeline runs two passes over the same string. One expands spintax, where {a|b|c} picks a variant so a thousand sends do not share one fingerprint. The other substitutes merge variables. If both grammars used single braces the parser would have to guess, and it would guess wrong on exactly the inputs that matter: a job title containing a pipe, a company name containing a brace. Double braces are unambiguous and easy to validate, because no rendered message should ever leave the system still containing {{.

The null field: three engine behaviours, three outputs

Take one template and one contact. The template is Hi {{firstName}}, saw {{company}} is hiring in {{city}}. The contact has firstName of "Maya", company of "Acme Robotics", and city of null, because the city column was blank in the import file for about a fifth of the rows and nobody checked.

Depending on which engine renders it, that one contact produces three completely different messages. The cheat sheet below shows all three side by side. The short version: a raw token is the loudest failure, an empty string is the quietest, and only a declared fallback produces a sentence a human would have written.

Fallback syntax in most engines, ours included, is a pipe inside the braces: {{city|your area}}, read as "use city, and if there is no city, use your area". Chains work the same way, so {{city|region|your area}} tries two fields before giving up. Write the fallback at the moment you write the sentence, never afterwards, because it has to fit the grammar around it.

Missing is not one thing

Here is the bug that survives every code review, because the engine is doing exactly what it was told. A CSV import produces at least four flavours of "no value", and only the first is null:

  • Genuine null. The column was absent or the cell was never populated. Every fallback implementation catches this.
  • Empty string. The cell existed and contained nothing. An engine that only tests value === null treats an empty string as a real value and substitutes it, which is the "hiring in ." case with extra steps.
  • Whitespace only. A single space or a tab. Looks identical to empty in a spreadsheet, passes both null and empty-string checks, renders as hiring in  .
  • Junk sentinels. The literal strings null, NULL, N/A, -, unknown, or in our own market yok. These are real strings as far as any engine is concerned, so Hi {{firstName|there}}, cheerfully renders Hi N/A,.

The fix is a normalisation step before substitution, not a smarter fallback: trim the value, treat the empty result as missing, and compare the lowercased value against a short blocklist of sentinels. Do this once in the send pipeline and every template in the workspace inherits it.

Casing, titles and the shape of imported data

Real contact data is not clean. The same first-name column will contain MARIA from a system that uppercased everything, maria from a web form with no validation, maria k. from a scraped source, and Maria from the rows somebody typed by hand. Substituted raw, your opener greets people as Hi MARIA,, which reads as shouting, or Hi maria k.,, which reads as a database leak.

Title-casing on render fixes the common cases and creates smaller new ones: mcdonald becomes Mcdonald rather than McDonald, and hyphenated names need the segment after the separator capitalised too. Perfect name casing is not a solvable problem, which is the argument for normalising at import (once, reviewably) rather than at send (invisibly, every time).

Company suffixes work the same way. Trimming Acme Robotics Inc. to Acme Robotics makes a DM sound like a person wrote it, while trimming Acme Robotics A.S. in formal Turkish B2B reads as sloppy rather than casual. Trim for conversational channels, keep the legal form for anything shaped like a document.

Derived variables

Not every variable comes off a column. The useful ones are computed at render time from something you already have, and each computation carries its own failure mode; the reference table further down lists four we use constantly and what breaks each one. The general rule is to derive only from a field you trust. A first name split out of a full name is safe because the full name came from the contact. A day-part greeting derived from a timezone that was itself guessed from a phone prefix is two inferences deep, and the error lands in your first line. See contact enrichment for keeping provenance on fields like these.

Order of operations with spintax

When a template contains both spintax and merge variables, the order of the two passes is a security decision, not a preference.

Spin first, then merge. The spinner sees only the template you wrote, expands the branches, and hands a plain string to the merge pass, which then substitutes values. Contact data never reaches the spinner.

Merge first, then spin. Now contact data is inside the string the spinner parses. A company named Smith | Jones LLP injects a pipe into your template. A contact whose bio contains a brace injects a branch. At best the output is mangled; at worst a hostile record turns your template into one the sender never wrote and never reviewed. This is template injection, the same class of bug as SQL injection, with the same root cause: untrusted data reaching a parser.

Spin first, merge second, and escape any braces or pipes surviving in a merged value. If your tool does it the other way round, at minimum sanitise every merged value before substitution.

Merging into links

Merged values inside URLs need URL encoding, always: a company named Acme & Sons dropped raw into ?company={{company}} truncates the parameter at the ampersand, and a value with a space breaks the link outright in clients that do not auto-escape. Never merge into a path segment either. /demo/{{company}} 404s for every contact whose company name is not also a route, and it leaks contact data into your access logs and analytics referrers. Query parameters are the right place, and a signed token is better still.

Testing before you send

Previewing the first row of your segment tells you nothing, because the first row is almost always complete. Two habits catch the rest:

  • Preview against 20 random contacts, not the top of the list. Random sampling surfaces the uppercase names, the missing cities and the company called -.
  • Run a null audit over the segment before the send: for each variable in the template, count how many rows will fall back. If {{city}} falls back on 34% of a 1,200-contact segment, you now know 408 people will read "your area", and you can decide whether that sentence still earns its place.

Two guardrails we recommend, both cheap to enforce:

  • No merge variable inside the first three words of a subject line unless its fallback reads naturally alone. Inbox previews truncate, and the fallback is what a large share of your list actually sees.
  • Block the batch when a personalisation-critical field falls back on more than a threshold share of rows. We use 20% as the default trigger for a warning and 40% for a hard block. Those numbers are our convention, not an industry standard, but any threshold beats none.

Why it matters

Merge variables are the difference between a template and a message. On cold outreach, a correctly merged and genuinely relevant detail is the clearest signal that a human chose to write to this person, and it is the first thing a recipient checks before deciding whether to reply or report. On DM channels the stakes beat email: a recipient who sees a raw token on Telegram or Instagram has the report button within reach, and reports are what become restrictions. The inverse holds too. Heavy personalisation does not rescue a bad list, because a perfectly merged message to somebody with no reason to hear from you is still an unwanted message. Merge variables raise the ceiling of a good list, not the floor of a bad one.

Common mistakes

  • Shipping without a fallback. The most common failure by a wide margin, and the easiest to prevent with a pre-send check that refuses any body containing {{ after rendering.
  • Fallbacks that only fit one sentence. {{company|your company}} reads fine in "how is your company handling this" and badly in "saw your company is hiring". Write the fallback for the sentence it lives in.
  • Personalising with stale data. A job title enriched eighteen months ago and wrong today makes you look like you bought a list, because you did.
  • Testing only the happy path. Send yourself one preview with every optional field deliberately blanked. That single test catches most of this page.

Related concepts

  • Spintax: the other half of the template pipeline, and the reason merge variables use double braces.
  • Contact enrichment: where the values behind your variables come from, and how fresh they are.
  • Deduplication: two records for one person means two different merged messages to the same inbox.
  • Drip campaign: every step of a sequence carries its own variables and its own null risk.
  • Cold outreach: the context where a merge failure costs the most.
  • Deliverability: broken personalisation drives complaints, and complaints drive placement.

How Pinlyx handles it

Pinlyx resolves merge variables on every send path: Telegram, X DMs, WhatsApp, Instagram, email and the in-app composer. The composer autocompletes the fields available for the current segment, supports pipe fallbacks and fallback chains, and normalises missing values by trimming and screening junk sentinels before substitution, so an imported N/A triggers the fallback instead of shipping. Spintax expands before merge substitution and surviving braces or pipes are escaped, so a contact record cannot inject template syntax.

Before a batch leaves, the pre-send check counts fallback hits per variable across the segment and shows how many recipients each fallback will reach, and any rendered body still containing a double brace fails the send. Previews pull random contacts rather than the first rows, which is the difference between a preview and a test.

Worked example · one null field, three outputs

Same template. Same contact. Three very different messages.

The contact has a first name and a company but no city. What your recipient reads depends entirely on how the engine treats a missing value.

Token left in place

Engine finds no value, emits the raw token.

Template

Hi {{firstName}}, saw {{company}} is hiring in {{city}}.

Contact record

firstName: "Maya" · company: "Acme Robotics" · city: null

What the recipient reads

Hi Maya, saw Acme Robotics is hiring in {{city}}.

The worst outcome: you have told a stranger, in writing, that they are row 4,182 of a bulk send. Wrap the send in a validator that refuses any rendered body still containing a double brace.

Empty-string substitution

Engine substitutes nothing and leaves the surrounding text alone.

Template

Hi {{firstName}}, saw {{company}} is hiring in {{city}}.

Contact record

firstName: "Maya" · company: "Acme Robotics" · city: null

What the recipient reads

Hi Maya, saw Acme Robotics is hiring in .

Quieter than a raw token and still broken: a floating space before a full stop, and a sentence that reads as unfinished. The reader may not work out why it feels wrong, which is arguably worse than a bug they would have forgiven.

Declared fallback

Engine takes the value after the pipe when the field is missing.

Template

Hi {{firstName|there}}, saw {{company}} is hiring in {{city|your area}}.

Contact record

firstName: "Maya" · company: "Acme Robotics" · city: null

What the recipient reads

Hi Maya, saw Acme Robotics is hiring in your area.

The only version worth shipping. The fallback was written by the person who wrote the sentence, so it fits the grammar around it. Note that the firstName fallback is lowercase "there" because it sits after "Hi ", not at the start of a sentence.

Derived variables · and what breaks them

Computed at render time, wrong at render time.

{{firstName}} from a full-name field

Split fullName on the first space.

"Maya Kaya" produces "Maya"

Breaks on "Dr. Maya Kaya" (produces "Dr.") and on cultures that write the family name first. Strip a known title list before splitting, and skip the split when the field contains no space.

{{companyDomain}} from an email address

Take everything after the @ sign.

"maya@acmerobotics.com" produces "acmerobotics.com"

Only meaningful on a work address. Free-mail domains (gmail.com, outlook.com, yandex.com, and the equivalents in your market) must be excluded or you will greet somebody as if they work at Gmail.

{{localHour}} for a send window

Contact timezone applied to the scheduled send time.

A contact in Istanbul at 09:00 UTC produces 12

Timezone is itself an enriched field and is often wrong or missing. Fall back to the workspace timezone rather than to UTC, because UTC is nobody’s working day.

{{greeting}} as a day-part

Derived from localHour: morning, afternoon, evening.

localHour 12 produces "Good afternoon"

Compounds the timezone risk: a wrong timezone turns into a visibly wrong greeting. Prefer a neutral opener unless you actually trust the timezone field.

Watch out for

Merge before spin is template injection.

If the merge pass runs first, contact data lands inside the string the spintax parser reads. A company recorded as Smith | Jones LLP injects a spin branch into a template nobody wrote and nobody approved. It is the same shape of bug as SQL injection: untrusted data reaching a parser.

Expand spintax first, substitute merge variables second, and escape braces and pipes that survive inside a merged value. The order costs nothing and closes the hole.

Merge variables: FAQ

The questions that come up right before somebody sends their first thousand-contact batch.

Nothing, in practice. "Merge tag" is the Mailchimp-era name, "merge field" is the Salesforce name, "personalisation token" is the HubSpot name, and "merge variable" is the term most outbound tooling settled on. They all describe the same thing: a named placeholder replaced per recipient at send time. If a vendor doc uses one and your tool uses another, they are talking about the same mechanism.
Because spintax already owns single braces. In a bulk-send pipeline you almost always run both: {a|b|c} picks a variant and {{firstName}} pulls a value. If both used single braces the parser could not tell whether {city} is a variable or a one-option spin, and a company name containing a pipe would silently turn into a spin branch. Double braces keep the two grammars separate, which is why nearly every outreach tool converged on them.
It depends entirely on the engine, which is why you must know which behaviour yours has before your first batch. Three exist: leave the raw token in the output (ships "{{city}}" to a real person), substitute an empty string (ships "hiring in ." with a floating space), or use a declared fallback such as {{city|your area}}. Only the third is safe. Test it by deliberately sending yourself a preview with the field blanked.
It should be, but many engines only test for null. A CSV import routinely produces three flavours of missing: a genuine null, an empty string, and a whitespace-only cell that looks empty in a spreadsheet. Bad exports add a fourth, the literal text "null" or "N/A" or "-". A fallback chain that only catches null will happily render "Hi N/A," so trim the value, treat whitespace-only as missing, and keep a blocklist of junk sentinels.
Carefully. A subject line is the shortest, most scrutinised text you send, and an awkward fallback is far more visible there than in paragraph three. Our house rule: no merge variable inside the first three words of a subject line unless the fallback reads naturally on its own, because the inbox preview truncates and the fallback is what a large share of your list will see.
Two or three that carry real information beat eight that are decoration. Every extra variable is another field that can be null and another chance the message reads as machine-assembled. Personalisation is judged on whether the detail is relevant, not on how many slots you filled.
Ready to ship

Personalise every send. Never ship a raw token.

Pinlyx resolves merge variables with fallback chains, junk-value screening and a pre-send audit that counts exactly how many contacts hit each fallback.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.