Skip to content

Merge Codes (Tags)

Merge codes are placeholders in your content that get replaced with dynamic values when the content is rendered. They allow you to personalize emails, SMS messages, web pages, and forms with recipient-specific data, event details, dates, and more.

Syntax

A merge code follows this pattern:

$$type.property$$

For example:

  • $$recipient.email$$ — the recipient's email address
  • $$event.name$$ — the event name
  • $$date.shortdate$$ — today's date in short format

Alternative delimiters

Curly braces also work: {{recipient.email}}. Both forms are equivalent.

Available tag types

Type Description Example properties
recipient Recipient fields email and/or mobilenumber (for SMS recipients), plus any custom recipient properties
message Mail or SMS message name, subject, description, fromname, fromemail
publication Publication/list name
date Current date shortdate, longdate, custom
event Event details name, description, start_date, end_date, date, registration_opens_date, registration_closes_date, spots_left, location_address, location_details
formresponse Form response data Any form field name, status, email, ticket_participated_qr_code_url
share Social sharing public_url
survey Survey name
webpage Web page name
recipientpage Recipient page name, gdpr_information, responsible_person_name, responsible_person_email
multioptin Multi opt-in name
automatedflow Automated flow name
automatedflow_participant Flow participant id, primaryemail, plus any participant property
system_mail System mail name, subject, fromname, fromemail
mail_content Mail content preheader, subject
system System information version
custom Custom variables Any key provided by the caller

Formatting values

Use the |format() verb to control how dates and numbers are displayed:

$$event.start_date|format(yyyy-MM-dd)$$        → 2026-04-25
$$event.start_date|format(dd MMMM yyyy)$$      → 25 April 2026
$$event.start_date|format(HH:mm)$$             → 09:00
$$date.custom|format(yyyy-MM-dd HH:mm:ss)$$    → 2026-07-06 14:30:00

Legacy format syntax

The older parenthesis syntax is still supported for backward compatibility:

$$event.start_date(yyyy-MM-dd)$$
$$date.custom(yyyy-MM-dd HH:mm:ss)$$

Both forms produce identical output. The parenthesis syntax is internally converted to a format verb.

Common date formats

Format string Example output Description
d 7/6/2026 Short date
D Monday, July 6, 2026 Long date
g 7/6/2026 2:30 PM General (short date + short time)
G 7/6/2026 2:30:00 PM General (short date + long time)
t 2:30 PM Short time
T 2:30:00 PM Long time
yyyy-MM-dd 2026-07-06 ISO date
yyyy-MM-dd HH:mm 2026-07-06 14:30 ISO date + 24h time
dd MMMM yyyy 06 July 2026 Day + full month name + year
MMMM July Full month name
dddd Monday Full day name

For the complete reference, see Standard date and time format strings and Custom date and time format strings.

Common number formats

Format string Input Example output Description
N 1234.5 1,234.50 Number with group separator and 2 decimals
N0 1234.5 1,235 Number with group separator, no decimals
N3 1234.5 1,234.500 Number with 3 decimals
C 1234.5 $1,234.50 Currency
P 0.85 85.00% Percentage
F2 1234.5 1234.50 Fixed-point, 2 decimals

For the complete reference, see Standard numeric format strings and Custom numeric format strings.

Culture-aware formatting

On public pages, date and number formatting follows the viewer's language: a culture query-string parameter if present, then the language cookie, then the browser language (Accept-Language header). The same merge code produces different output depending on the viewer's locale. For example, $$event.start_date|format(g)$$ shows 7/6/2026 2:30 PM for English (US) viewers and 2026-07-06 14:30 for Swedish viewers.

A mail or SMS has no viewer when it is sent, so it is formatted in the sending server's culture.

Default formats

When no format is specified, these defaults apply:

Value type Default format
DateTime g (general short)
decimal, double, float N (number with group separators)
int, long No format (plain number)
string No formatting

Resolvers may specify their own default format, which takes precedence over the table above. For example, $$event.start_date$$ defaults to g (general short date/time) even without an explicit format verb.

Numeric recipient properties

Recipient properties of a numeric type support the full range of format strings:

$$recipient.balance|format(N2)$$      → 1,234.50
$$recipient.balance|format(C)$$       → $1,234.50
$$recipient.points|format(N0)$$       → 1,235

With no format verb, a numeric recipient property renders as the plain stored number — 1234.5, not 1,234.50. Group separators and fixed decimal places are opt-in via |format().

Text properties cannot be number-formatted

Formatting is applied to the stored value's actual type. If a numeric-looking value is stored as text on the recipient, |format(N2) has nothing to work with and is silently ignored — the value renders unchanged. If a format verb appears to do nothing, check the recipient property's configured type first.

Verbs (transforms)

Verbs transform the resolved value. Chain multiple verbs with |:

$$recipient.firstname|toupper$$              → BRUCE
$$recipient.firstname|tolower$$              → bruce
$$recipient.firstname|capitalize$$           → Bruce
$$recipient.firstname|trim$$                 → (whitespace removed)
$$recipient.firstname|toupper|trim$$         → BRUCE
$$event.start_date|format(yyyy-MM-dd)$$      → 2026-04-25
Verb Description
toupper Convert to uppercase
tolower Convert to lowercase
capitalize First letter uppercase, rest lowercase
trim Remove leading and trailing whitespace
format(x) Apply a format string (see Formatting above)
urlencode Force URL encoding (override default)
htmlencode Force HTML encoding (override default)
noencode Disable encoding (override default)
collapse / collapse(chars) Remove the surrounding whitespace when the value is blank (see below)

Verbs are applied left-to-right after the value is resolved.

An unrecognised verb leaves the whole merge code unresolved, so a misspelt verb shows up in a test send rather than being silently dropped. A format string that does not fit the value is ignored, and the value renders unformatted. A property name cannot contain |, because | starts the verb list.

Removing the gap left by a blank value

A merge code that resolves to nothing still leaves the space around it:

Hi $$recipient.firstname$$!     →  "Hi !"   for a recipient with no first name

The collapse verb removes that gap:

Hi $$recipient.firstname|collapse$$!     →  "Hi!"         when blank
                                         →  "Hi Bruce!"   when set

collapse does nothing at all when the value is present, so it is safe to add to any merge code where a blank value would look wrong. It applies both when the value is blank and when the property is missing entirely.

Collapsing punctuation too

Whitespace is always collapsed. To also remove punctuation that would be left stranded, list the characters as an argument:

Dear $$recipient.firstname|collapse(,)$$, welcome    →  "Dear welcome"

The argument is a set of characters, not a list — spaces inside it are ignored, since whitespace always collapses. These are all equivalent:

$$recipient.firstname|collapse(,.)$$
$$recipient.firstname|collapse(, .)$$
$$recipient.firstname|collapse(. ,)$$

Only characters directly touching the merge code are removed, and a whole run of them is taken — Hi $$recipient.firstname|collapse(!)$$!!! renders Hi. Punctuation elsewhere in the sentence is untouched.

What collapse does and does not fix

Content Blank value renders
Hi $$recipient.firstname\|collapse$$! Hi!
Hi $$recipient.firstname\|collapse$$ there Hi there (one space)
A collapsed merge code alone on its own line the surrounding line break is kept
Dear $$recipient.firstname\|collapse(,)$$, welcome Dear welcome

A question mark cannot be listed

? is reserved for the ?? fallback delimiter, so collapse(?) cannot be parsed and the merge code will render as raw text in your content. Use ! or ., or rephrase the sentence. All other punctuation works.

Collapse and fallbacks are alternatives

If a merge code has both a fallback and collapse, the fallback wins — it produces a value, so there is no gap left to collapse. Use ?? when you want substitute text, and collapse when you want the sentence to close up instead.

Collapse also cannot rescue a sentence that depends on the value for its meaning. Your order $$recipient.ordernumber|collapse$$ has shipped reads as Your order has shipped — grammatical, but missing the information. Use a fallback there instead.

Fallback values

Use ?? to provide a default value when a merge code can't be resolved:

$$recipient.firstname??Guest$$               → "Guest" if firstname is not available
$$recipient.company??$$                      → empty string if company is not available
$$recipient.name|toupper??VALUED CUSTOMER$$  → "VALUED CUSTOMER" if name is not available

The fallback is used when:

  • The tag type has no resolver (e.g., unknown type)
  • The resolver returns no value for the property (e.g., recipient has no value for that field)
  • The resolved value is blank — an empty string, or a string containing only whitespace

When a value IS resolved, the fallback is ignored and the resolved value is used normally (including any verbs).

A merge code that cannot be resolved here and has no fallback is left in the content as written — for example $$recipient.email$$ on a page that has no recipient.

Blank values count as missing

This matters more than it sounds. Imported recipient data stores "no value" as an empty string at least as often as it stores a true null — a CSV column present but empty, a spreadsheet cell with a stray space, an API field sent as "". From inside your content these are indistinguishable from a missing field, and all of them leave the same hole in the sentence. So all of them trigger the fallback:

$$recipient.firstname??Guest$$
Stored value Result
NULL / field absent Guest
"" Guest
" " (whitespace only) Guest
"bruce" bruce

Numbers and booleans are never blank

Only text values are tested for blankness. A numeric 0 and a false are real values, not missing ones — $$recipient.balance??N/A$$ renders 0 for a recipient who owes nothing, never N/A. This is deliberate: treating zero as missing is a common templating-engine trap that silently corrupts financial and count-based content.

A blank value with no fallback still renders as blank

Blankness only triggers the fallback. If a merge code has no ?? fallback and the value is blank, the merge code renders as an empty string — it does not fall back to showing the raw $$recipient.firstname$$ text in your content.

Encoding behavior

Merge code values are automatically encoded based on where the tag appears in the content. This prevents XSS vulnerabilities and ensures URLs work correctly.

Context Encoding applied Example
HTML body text HTML encoding & → &amp;, < → &lt;
URL query parameter (?key=$$tag$$) URL encoding @ → %40, & → %26, space → %20
URL path (/path/$$tag$$/page) URL path encoding spaces → %20
HTML attribute (alt="$$tag$$") HTML encoding Same as body text
Plain text (SMS, text part of a mail) No encoding, except inside a link Value used as-is; in https://…?q=$$tag$$ the value is URL-encoded as above

You can override the automatic encoding with verbs:

$$recipient.name|noencode$$     → No encoding applied regardless of context
$$recipient.name|urlencode$$    → Force URL encoding even in body text
$$recipient.name|htmlencode$$   → Force HTML encoding even in plain text

noencode disables XSS protection for that tag

Context encoding is what prevents a recipient-supplied value from breaking out of an attribute or injecting script. With noencode, a value such as " onmouseover="alert(1) or javascript:... in an href is emitted verbatim into the page.

Only use noencode on properties that are system-generated or validated on import — never on free-text fields, and never on anything a recipient can set themselves through a form.

Values that are already encoded

Store values raw: the engine encodes each one for the place its merge code sits. Some imports store values already encoded for the link they will be used in, such as Wayne%20Enterprises. Encoding those a second time would break the link, so a value that is already validly encoded for its position is inserted as-is:

Stored value Context Rendered output
Wayne Enterprises URL query parameter Wayne%20Enterprises
Wayne%20Enterprises URL query parameter Wayne%20Enterprises (kept)
Ben &amp; Jerry's HTML body text Ben &amp; Jerry's (kept, shows as Ben & Jerry's)
Wayne%20Enterprises Plain text / SMS Wayne%20Enterprises (plain text is never encoded)

The rules:

  • A value is kept only when it is a complete, valid encoding for its position, with nothing left that encoding would change. Anything else is encoded normally, so this can never let a value escape its position.
  • Fallback values are checked the same way.
  • + for a space is recognised only alongside a % escape: Wayne+Enterprises%2C is kept, with the + written as %20. A value whose only encoding is + (Wayne+Enterprises) is treated as raw text and encoded, because a literal +, as in C++, is common.
  • |urlencode and |htmlencode always encode, so use them when a value really is the literal text %20 or &amp;. Without them such a value shows decoded, as a space or &.
  • Each tenant and merge code that relies on this is logged once, so support can see which content depends on pre-encoded data.

Storing raw values is still the better choice: a raw value works in body text, links and SMS alike, while a pre-encoded one shows its %20 as text in an SMS.

QR codes and barcodes

Merge codes inside barcode data= query parameters are automatically URL-encoded. Both plain and URL-encoded tag syntax work:

<!-- Plain syntax -->
<img src="/b/bc/qr/?data=$$recipient.email$$&vm=1&width=200" />

<!-- URL-encoded syntax (used by the editor) -->
<img src="/b/bc/qr/?data=%24%24recipient.email%24%24&vm=1&width=200" />

Both produce the same result: the resolved email address is URL-encoded in the data= parameter.

Caveats

Fallback values in URLs: A fallback is encoded for its position like any resolved value, so $$recipient.name??A b&c$$ in a query parameter renders A%20b%26c. The editor stores a & in an attribute as &amp;; the fallback is decoded before it is encoded, so it is not encoded twice.

Special links are not merge codes: Some tags that look like merge codes (e.g., $$publication.unsubscribe$$, $$message.viewmailhtmlpart$$) are actually special link placeholders handled by a separate system. They are preserved as-is during merge code resolution and replaced later in the pipeline. Do not use formatting or verbs on special links — they will not work.

Nested merge codes: Some merge codes resolve to text that itself contains merge codes. For example, a preheader ($$mail_content.preheader$$) might contain Hello $$recipient.firstname$$!. The engine automatically expands these nested merge codes in a second pass — you don't need to do anything special. This only goes two levels deep to prevent infinite loops.

Two things are never expanded in that second pass. Answers a visitor typed into a form are inserted as text, so a visitor cannot pull other data into a page or mail by typing a merge code. And urlencode / htmlencode inside a resolved value are ignored, so only the content author decides how a value is encoded.

Subjects are one line: if the merge codes in a mail subject resolve to text containing line breaks, the lines are joined with ,.