Using Liquid in our email editor

A practical guide to the Liquid template language inside the TouchBasePro editor: merge fields, conditionals, loops, filters, JSON custom fields, and the [tbprandom] and date helpers you can use in any subject, preheader or body block.

The TouchBasePro editor uses Liquid as its template language. Liquid is the same templating engine that powers Shopify storefronts and a long list of other marketing tools. It lets you drop dynamic, per-recipient values into a subject line, preheader, from-name, link or body, and lets you wrap that content in lightweight if/else and for-loop logic.

This guide covers:

  • The three building blocks: output, tags, filters
  • The data we expose on every send (Subscriber, Campaign, Company, Domain)
  • Short-form bracket merge fields the editor accepts as a shortcut
  • Our extra fields: [tbprandom], current-date helpers, web version and unsubscribe links
  • Conditionals, loops and filters
  • Working with JSON custom fields
  • Worked examples you can paste straight into a subject or a text block
  • Gotchas and a short reference list

Liquid was created by Shopify and is documented in full at the official Liquid reference. This article distils the parts that are useful inside the TouchBasePro editor and adds the merge fields specific to our platform.

1. The three building blocks

Liquid uses three kinds of markup:

  • Objects (output): {{ ... }} prints a value.
    Hi {{ Subscriber.Name }},
    
  • Tags (logic): {% ... %} runs a control statement. Tags do not print anything on their own.
    {% if Subscriber.CustomFields.City == "Cape Town" %}
      Free local delivery on us.
    {% endif %}
    
  • Filters: | transforms an output value. Chain as many as you need.
    Hi {{ Subscriber.Name | default: "there" | capitalize }},
    

Tag and output blocks can sit anywhere: a subject line, the from-name, the preheader, an HTML body, or a plain-text alternate.

2. The data we ship to your template

Every send gets a per-recipient data tree. These are the objects you can reach with {{ ... }}.

Subscriber

Field Example Notes
Subscriber.Email jane@example.com The recipient's email address.
Subscriber.Name Jane Smith Display name. Blank if not captured.
Subscriber.Id 5f9b... (32-char hex) Per-message identifier. Use it to build personal links.
Subscriber.Random 847362910 A random integer assigned to each recipient at send time.
Subscriber.SubscribedDate 2024-08-12 09:14:00 When they joined the list.
Subscriber.CustomFields.X depends on the field All custom fields on the subscriber, by name.
Subscriber.WebVersion URL Link to the rendered web version of this campaign.
Subscriber.Links[N] URL Indexed access to the trackable links in this campaign.
Subscriber.ListId 42 The list this send went out on.
Subscriber.SegmentId 17 or -1 Segment id, or -1 if not segmented.

Campaign

Field Example
Campaign.Id 32-char hex GUID
Campaign.Name October Newsletter
Campaign.FromName Jane @ Acme
Campaign.FromEmail jane@acme.co.za
Campaign.ReplyTo support@acme.co.za

Company

Field Example
Company.Name Acme Pty Ltd
Company.Id 32-char hex GUID

Domain

Domain.Selector, Domain.Interaction and Domain.Tracking are the pieces TouchBasePro uses to assemble click and open tracking URLs. You rarely need them by hand, but they are available if you want to construct a custom tracked link.

3. Short-form bracket merge fields

The editor accepts a friendlier bracket syntax for the most common merge fields. They are rewritten to Liquid behind the scenes.

You type We render
[email] or <email/> {{ Subscriber.Email }}
[name] or <name/> {{ Subscriber.Name }}
[tbpid] {{ Subscriber.Id }}
[tbprandom] A random integer between 0 and 9,999,999
[currentday] Day of the month (1 to 31)
[currentdayname] Day of the week (Monday, Tuesday ...)
[currentmonth] Month number (1 to 12)
[currentmonthname] Short month name (Jan, Feb ...)
[currentyear] Four-digit year
[webversion] The "No images? Click here" web-version link
[unsubscribe] The recipient's personal unsubscribe link
[preferences] A link to the subscriber preferences centre
[abuse] (plain text only) The abuse-report URL

Use the bracket form when you want a quick mail-merge in a subject or preheader. Use Liquid ({{ Subscriber.* }}) when you need filters, fallbacks or logic.

Custom field shortcut with fallback

Hi [FirstName,fallback=there],

Every subscriber custom field is reachable by name:

  • [FieldName] prints the value (blank if it is null).
  • [FieldName,fallback=Default] prints Default if the value is null or empty.

The same logic in pure Liquid:

Hi {{ Subscriber.CustomFields.FirstName | default: "there" }},

[tbprandom] is the one most people ask about. We generate a fresh integer per recipient, per send. The classic use is busting an image cache so each open re-fetches a tracking pixel or a dynamic banner:

<img src="https://cdn.example.com/banner.png?r=[tbprandom]" alt="">

4. Conditionals

{% if Subscriber.CustomFields.Tier == "Gold" %}
  You qualify for our priority concierge line.
{% elsif Subscriber.CustomFields.Tier == "Silver" %}
  Thanks for being a Silver member.
{% else %}
  Welcome aboard.
{% endif %}

You also have unless (the inverse of if) and case / when:

{% case Subscriber.CustomFields.Country %}
  {% when "South Africa" %}Local pricing applies.
  {% when "Namibia", "Botswana" %}Regional pricing applies.
  {% else %}International pricing applies.
{% endcase %}

Operators you can use inside if: ==, !=, >, <, >=, <=, and, or, contains.

{% if Subscriber.Email contains "@gmail.com" %}
  Add us to your Promotions tab to see future updates.
{% endif %}

5. Loops

{% for item in Subscriber.CustomFields.Order.items %}
  - {{ item.qty }} x {{ item.name }} ({{ item.price }})
{% endfor %}

You can limit, offset, reverse, and use forloop.index (1-based) or forloop.index0 (0-based):

{% for tag in Subscriber.CustomFields.Tags limit: 3 %}
  {{ forloop.index }}. {{ tag }}
{% endfor %}

Inside a loop you also get forloop.first, forloop.last, forloop.length, and break / continue.

6. Filters worth knowing

The filters below cover most email work. Chain with |.

Strings: default, upcase, downcase, capitalize, strip, truncate: 60, truncatewords: 12, replace: "old", "new", remove: "x", prepend: "Mr ", append: ".", escape, strip_html, url_encode, slice: 0, 5, size, split: ",".

Numbers and math: plus: 5, minus: 5, times: 1.15, divided_by: 100, round: 2, floor, ceil, abs, at_most: 100, at_least: 0.

Dates: date: "%d %B %Y" (for example, 12 October 2025).

Arrays: first, last, join: ", ", size, sort, uniq, compact, reverse.

A few in context:

{{ Subscriber.Name | default: "there" | capitalize }}
{{ Subscriber.SubscribedDate | date: "%d %b %Y" }}
{{ Subscriber.CustomFields.Total | times: 1.15 | round: 2 }}
{{ Subscriber.CustomFields.Tags | join: ", " }}

The full filter list lives in the official Liquid reference.

7. JSON custom fields

A custom field can hold a flat string, a list of strings, or a full JSON object. JSON gives you the most flexibility because you can nest data and walk it like an object in the template.

Suppose you set a custom field called Order to a JSON object. You can then render it directly in the editor. The tabs below show the template, the subscriber's data, and what gets posted out:

Order {{ Subscriber.CustomFields.Order.ref }} for
{{ Subscriber.CustomFields.Order.currency }} {{ Subscriber.CustomFields.Order.total | round: 2 }}:

{% for item in Subscriber.CustomFields.Order.items %}
- {{ item.qty }} x {{ item.name }} ({{ item.price }})
{% endfor %}
{
  "Subscriber": {
    "Email": "jane@example.com",
    "CustomFields": {
      "Order": {
        "ref": "INV-10234",
        "currency": "ZAR",
        "total": 1499.00,
        "items": [
          { "name": "Wireless mouse", "qty": 1, "price": 599 },
          { "name": "USB-C hub", "qty": 1, "price": 900 }
        ]
      }
    }
  }
}
Order INV-10234 for
ZAR 1499:

- 1 x Wireless mouse (599)
- 1 x USB-C hub (900)

To prototype quickly, paste your JSON and template into a public Liquid sandbox and check the output before you put it in a real campaign.

8. Worked examples

Every example below is a static illustration: the Template tab shows what you write in the editor, Subscriber data is the JSON we evaluate against, and Rendered output is what subscribers see in their inbox.

Personalised greeting with a fallback chain

Walk the fallback chain: try FirstName, then Name, then the literal "there".

Hi {{ Subscriber.CustomFields.FirstName | default: Subscriber.Name | default: "there" }},
{
  "Subscriber": {
    "Name": "Jane Smith",
    "CustomFields": {
      "FirstName": "Janie"
    }
  }
}
Hi Janie,

Same template, with FirstName missing, falls back to Name:

Hi {{ Subscriber.CustomFields.FirstName | default: Subscriber.Name | default: "there" }},
{
  "Subscriber": {
    "Name": "Jane Smith",
    "CustomFields": {}
  }
}
Hi Jane Smith,

Day-of-week offer

{% if "Monday,Tuesday,Wednesday" contains "[currentdayname]" %}
  Midweek special: 10% off through Wednesday.
{% endif %}

Cache-busting tracking pixel

The editor swaps [tbprandom] for a fresh integer per recipient. The Liquid {{ Subscriber.Id }} is substituted from the per-message id.

<img src="https://cdn.example.com/pixel.gif?r=[tbprandom]&u={{ Subscriber.Id }}" alt="" width="1" height="1">
{
  "Subscriber": {
    "Id": "5f9b2c1d4e6a4b7c8d9e0f1a2b3c4d5e"
  },
  "tbprandom": 8473629
}
<img src="https://cdn.example.com/pixel.gif?r=8473629&u=5f9b2c1d4e6a4b7c8d9e0f1a2b3c4d5e" alt="" width="1" height="1">

Region-aware preheader

{% case Subscriber.CustomFields.Country %}
  {% when "South Africa" %}Free delivery on orders over R500.
  {% when "Namibia" %}Cross-border shipping available.
  {% else %}International orders ship from Johannesburg.
{% endcase %}
{
  "Subscriber": {
    "CustomFields": {
      "Country": "Namibia"
    }
  }
}
Cross-border shipping available.

Loop through an order

You ordered:
{% for item in Subscriber.CustomFields.Order.items %}
- {{ item.qty }} x {{ item.name }} ({{ item.price | times: item.qty | round: 2 }})
{% endfor %}
Total: {{ Subscriber.CustomFields.Order.total | round: 2 }}
{
  "Subscriber": {
    "CustomFields": {
      "Order": {
        "total": 1499.00,
        "items": [
          { "name": "Wireless mouse", "qty": 2, "price": 599 },
          { "name": "USB-C hub", "qty": 1, "price": 900 }
        ]
      }
    }
  }
}
You ordered:
- 2 x Wireless mouse (1198)
- 1 x USB-C hub (900)
Total: 1499

9. Gotchas and tips

  • Always include a fallback. If a custom field is missing on a recipient, Liquid prints an empty string. | default: "there" is the easiest safety net.
  • Mind your quotes inside HTML. When a Liquid expression uses a string argument, use the opposite quote to the attribute. Inside href="...", write {{ x | default: 'fallback' }} (single-quoted inner string). Inside href='...', use "fallback".
  • Test with a test send before going live. Test sends substitute placeholder values for missing fields so you can see how the template behaves.
  • Liquid is logic-only, not a programming language. There is no include, no file access and no network calls. This is deliberate and keeps every send safe.
  • Whitespace control. Add a hyphen inside a tag to trim surrounding whitespace: {%- if x -%} or {{- x -}}. Useful when looping inside a tightly-formatted HTML table.

10. Quick reference

{{ Subscriber.Email }}
{{ Subscriber.Name | default: "there" }}
{{ Subscriber.CustomFields.MyField | default: "n/a" }}
{{ Subscriber.SubscribedDate | date: "%d %b %Y" }}
{{ Subscriber.Random }}

{% if Subscriber.CustomFields.Tier == "Gold" %} ... {% endif %}
{% unless Subscriber.IsEngagedSubscriber %} ... {% endunless %}
{% for item in Subscriber.CustomFields.Order.items limit: 5 %} ... {% endfor %}

[email]  [name]  [tbpid]  [tbprandom]
[currentday]  [currentdayname]  [currentmonth]  [currentmonthname]  [currentyear]
[webversion]  [unsubscribe]  [preferences]
[FieldName,fallback=Default]

For the full filter and tag list, see the official Liquid documentation.

Frequently asked questions

What template language does the TouchBasePro editor use?
The editor uses Liquid, the same templating language that powers Shopify storefronts and many marketing tools. You can use it in subject lines, preheaders, from-names, links and body content.
What does [tbprandom] do in a template?
[tbprandom] is replaced at send time with a fresh integer between 0 and 9,999,999 unique to that recipient. It is commonly used as a cache-buster on image URLs so each open re-fetches the resource.
How do I render a JSON custom field?
If your custom field holds a JSON object, walk it with dot notation. For example, {{ Subscriber.CustomFields.Order.ref }} reads the ref property, and you can loop over an array with {% for item in Subscriber.CustomFields.Order.items %}.
What is the difference between [FieldName] and {{ Subscriber.CustomFields.FieldName }}?
They resolve to the same value. The bracket form is a shortcut the editor rewrites into Liquid before the send. Use brackets for a quick personalisation and the Liquid form when you need filters, fallbacks or logic.
How do I add a fallback when a custom field might be empty?
In Liquid use the default filter: {{ Subscriber.CustomFields.FirstName | default: "there" }}. In the bracket form use [FirstName,fallback=there].