Skip to main content

Templating Guide

We use a Handlebars-based template engine that lets you build fully dynamic email templates. It provides powerful features to customize content based on your data.

  • Variables: Insert placeholders that are automatically replaced with real values when the email is sent.
  • Conditionals: Show or hide sections of content based on specific conditions.
  • Loops: Iterate over collections such as arrays, maps, slices, or structs to generate repeated or dynamic sections.
  • Custom Helpers: Apply custom logic for formatting, comparisons, and advanced template behavior.

Based on your workspace, contact, and event data, you can use simple variables, expressions, and nested objects or arrays to create highly flexible and complex templates.

Available Variables

Every email template has access to the following top-level variables, automatically populated at render time:

VariableTypeDescription
preview_textStringThe preview text for the email, displayed in the inbox before the email is opened.
workspaceObjectRefers to Workspace Properties that can be created at Workspace Settings. Access nested properties with dot notation: {{workspace.name}}.
contactObjectContains data about the recipient contact (e.g. email_address, name, custom fields). Access nested properties with dot notation: {{contact.email_address}}.
actions.view_online_urlStringA URL that allows the recipient to view the email in their browser.
actions.confirm_urlStringA URL for the recipient to confirm their subscription.
actions.unsubscribe_urlStringA URL for the recipient to unsubscribe from future emails.
eventObjectContains data about the event that triggered the email (if applicable). Access nested properties with dot notation: {{event.name}}.

For example, to include an unsubscribe link and greet the contact by name:

Hello {{contact.first_name}},

Thank you for being part of {{workspace.name}}.

<a href="{{actions.unsubscribe_url}}">Unsubscribe</a> | <a href="{{actions.view_online_url}}">View in browser</a>

Built-in & Custom Helpers

In addition to Handlebars built-in helpers (if, unless, each, with, lookup, etc.), we've registered several custom helpers for your convenience:

HelperDescription
eqReturns true if two values are equal.
Usage: {{#if (eq status "active")}}...{{/if}}
neReturns true if two values are not equal.
Usage: {{#if (ne role "admin")}}...{{/if}}
gtReturns true if the first value is greater than the second (numbers only).
Usage: {{#if (gt amount 10)}}...{{/if}}
ltReturns true if the first value is less than the second (numbers only).
Usage: {{#if (lt score 50)}}...{{/if}}
gteGreater than or equal comparison (numbers only).
Usage: {{#if (gte amount 100)}}...{{/if}}
lteLess than or equal comparison (numbers only).
Usage: {{#if (lte score 40)}}...{{/if}}
andLogical AND. Returns true if both values are truthy.
Usage: {{#if (and a b)}}...{{/if}}
orLogical OR. Returns true if either value is truthy.
Usage: {{#if (or a b)}}...{{/if}}
notLogical NOT. Returns true if the value is falsy.
Usage: {{#if (not is_disabled)}}...{{/if}}
uppercaseConverts a string to uppercase.
Usage: {{uppercase name}}
lowercaseConverts a string to lowercase.
Usage: {{lowercase email}}
capitalizeCapitalizes the first letter of a string.
Usage: {{capitalize city}}
truncateTruncates a string to a specified length, adding ellipsis if needed.
Usage: {{truncate description 50}}
defaultReturns the default value if the variable is undefined or empty.
Usage: {{default value "N/A"}}
formatDateFormats a date string according to the specified format.
Usage: {{formatDate value "Y-m-d"}}. See Carbon date formatting docs.
rawOutputs HTML content without escaping, allowing raw HTML to render directly. Use with caution.
Usage: {{raw html}}

If you need additional custom helpers, you can request them via our support channels, and we'll get coding!

Example Template

Hello {{capitalize contact.first_name}} {{uppercase contact.last_name}},

{{#if (eq contact.status "active")}}
Welcome back to {{workspace.name}}!
{{else}}
Please confirm your subscription by clicking <a href="{{actions.confirm_url}}">here</a>.
{{/if}}

{{#if event}}
We noticed you triggered the "{{event.name}}" event on {{formatDate event.created_at "D M d, Y"}}.
{{/if}}

{{#each event.orders}}
- Order #{{id}}: ${{amount}}
{{/each}}

<a href="{{actions.view_online_url}}">View in browser</a> | <a href="{{actions.unsubscribe_url}}">Unsubscribe</a>

Nesting Helpers

Helpers can be combined to build compound expressions. Wrap each helper call in parentheses and nest them as needed:

{{#if (and (eq contact.status "active") (gt contact.order_count 0))}}
You have {{contact.order_count}} orders on your account.
{{/if}}

{{#if (or (eq contact.plan "pro") (eq contact.plan "enterprise"))}}
Thank you for being a premium member!
{{/if}}

Using #with for Scoping

The #with block helper lets you scope into a nested object, so you don't have to repeat the parent key on every variable:

{{#with contact}}
Hello {{capitalize first_name}} {{uppercase last_name}},
Your email is {{email_address}}.
{{/with}}

{{#with workspace}}
You are receiving this from {{name}}.
{{/with}}

This is equivalent to writing {{capitalize contact.first_name}}, {{contact.email_address}}, etc., but keeps your templates cleaner when referencing many properties from the same object.

HTML Escaping

All variable output is HTML-escaped by default to prevent XSS vulnerabilities. For example, if a variable contains <b>bold</b>, it will render as the literal text <b>bold</b> rather than bold.

To output raw, unescaped HTML, use the raw helper:

{{raw contact.bio}}
warning

Only use raw with content you trust. Never use it with user-supplied input that hasn't been sanitized.

Fallback Rendering

If a template fails to compile due to syntax errors, CampaignLark will attempt a fallback render before discarding the email. During fallback, simple variable placeholders like {{contact.first_name}} and {{workspace.name}} are still replaced with their actual values and the resulting HTML is injected into the send queue. Complex expressions, conditionals, and loops will not be processed during fallback.