Skip to main content
Liquid is an open-source template language that enables you to create dynamic, personalized content throughout Conversion. Use Liquid to insert contact data, add conditional logic, and transform values in your emails, workflows, and integrations.

Quick Start

Liquid uses simple syntax to output dynamic values:
Hi {{ contact.first_name }}, welcome to {{ company.name }}!
This outputs: “Hi Sarah, welcome to Acme Corp!”

Where You Can Use Liquid

Liquid is supported throughout Conversion:
LocationSupported Fields
EmailsSubject line and body content
Workflow nodesSlack messages, internal email alerts, webhook URLs and bodies, Salesforce task creation
Change Field nodeThe value being set
WebhooksURL and request body

Liquid Basics

Liquid consists of three core components:

Objects

Objects output dynamic values using double curly braces:
{{ contact.first_name }}
{{ company.industry }}

Tags

Tags create logic and control flow using curly braces with percent signs:
{% if contact.industry == "Technology" %}
  Welcome to our tech newsletter!
{% endif %}

Filters

Filters modify output using the pipe character:
{{ contact.first_name | upcase }}
{{ contact.created_at | date: "%B %d, %Y" }}
Conversion supports all standard Liquid syntax. See Shopify’s Liquid documentation for the complete language reference.

Data Objects

All Liquid in Conversion is written from the contact’s perspective. This provides a consistent mental model: you’re always asking “what data does this contact have access to?”

Contact

Access any field on the contact:
{{ contact.first_name }}
{{ contact.email }}
{{ contact.job_title }}
{{ contact.custom_field_key }}

Company

Access fields from the contact’s associated company:
{{ company.name }}
{{ company.industry }}
{{ company.annual_revenue }}

Opportunities

Access opportunities through two paths, depending on the relationship:
Access opportunities associated with the contact’s company. Returns the 10 most recently created opportunities.
{{ company._opportunities[0].name }}
{{ company._opportunities[0].amount }}
{{ company._opportunities[0].stage }}
Use .first and .last as shortcuts:
{{ company._opportunities.first.name }}
{{ company._opportunities.last.stage }}
Iterate over all opportunities:
{% for opp in company._opportunities %}
  - {{ opp.name }}: {{ opp.stage }}
{% endfor %}
Access the contact’s role on a company opportunity:If the contact has an Opportunity Contact Role (OCR) on an opportunity:
{{ company._opportunities.first._role.role }}
{{ company._opportunities.first._role.is_primary }}

Custom Objects

Access custom objects related to the contact. Each custom object type returns the 10 most recently created objects.
{{ objects.product[0].name }}
{{ objects.product[0].sku }}
{{ objects.subscription.first.plan_name }}
Reserved fields available on all custom objects:
FieldDescription
idUnique identifier
typeThe custom object type name
created_atWhen the object was created
updated_atWhen the object was last updated
Access relationship fields (metadata about the connection between the contact and object):
{{ objects.product[0]._relationship.role }}
{{ objects.product[0]._relationship.assigned_date }}
Iterate over custom objects:
{% for product in objects.product %}
  - {{ product.name }} (SKU: {{ product.sku }})
{% endfor %}

Tokens

Access token values set in your campaigns:
{{ tokens.webinar_date }}
{{ tokens.promotion_code }}

Current Date and Time

Use now to get the current UTC datetime:
{{ now | date: "%B %d, %Y" }}
{{ now | date: "%Y-%m-%d" }}

Trigger Context

When a workflow runs, contextual data about what triggered it is available through the trigger object. Use trigger.type to identify what triggered the workflow.

Form Submissions

{% if trigger.type == "form_submission" %}
  Thanks for submitting {{ trigger.form_submission.form_name }}!
{% endif %}
FieldDescription
trigger.form_submission.form_nameName of the form
trigger.form_submission.form_idUnique identifier of the form
trigger.form_submission.page_urlURL where the form was submitted
trigger.form_submission.submitted_atTimestamp of submission
trigger.form_submission.fields.<field>Any submitted field value
{{ trigger.form_submission.fields.company_size }}
{{ trigger.form_submission.fields.use_case }}

Page Visits

{% if trigger.type == "page_visit" %}
  Thanks for visiting {{ trigger.page_visit.page_url }}
{% endif %}
FieldDescription
trigger.page_visit.page_urlURL of the visited page
trigger.page_visit.referrerReferring URL
trigger.page_visit.utm_sourceUTM source parameter
trigger.page_visit.utm_mediumUTM medium parameter
trigger.page_visit.utm_campaignUTM campaign parameter
trigger.page_visit.visited_atTimestamp of visit

Email Events

All email triggers share common metadata:
FieldDescription
trigger.email.idEmail asset ID
trigger.email.nameName of the email in Conversion
trigger.email.sent_atTimestamp when email was sent
For link clicks, access the clicked URL:
{% if trigger.type == "email_link_clicked" %}
  You clicked {{ trigger.email.link.url }} in "{{ trigger.email.name }}"
{% endif %}

Field Changes

Access previous values when a contact or company field changes:
{% if trigger.type == "contact_updated" %}
  {% if trigger.contact._changed.title %}
    Your title changed from "{{ trigger.contact._previous.title }}" to "{{ contact.title }}"
  {% endif %}
{% endif %}
{% if trigger.type == "company_updated" %}
  {% if trigger.company._changed.industry %}
    {{ company.name }}'s industry is now {{ company.industry }}
  {% endif %}
{% endif %}

Audience Changes

{% if trigger.type == "added_to_audience" %}
  Welcome to {{ trigger.audience.name }}!
{% endif %}

Opportunity Events

{% if trigger.type == "opportunity_updated" %}
  {% if trigger.opportunity._changed.stage %}
    {{ trigger.opportunity.name }} moved to {{ trigger.opportunity.stage }}
  {% endif %}
{% endif %}

Custom Events

{% if trigger.type == "custom_event" %}
  Event: {{ trigger.custom_event.name }}
  {{ trigger.custom_event.data.product_id }}
{% endif %}

API Triggers

{% if trigger.type == "api" %}
  Processing: {{ trigger.api.name }}
  {{ trigger.api.data.campaign_name }}
{% endif %}
For a complete list of trigger types and their available fields, see Trigger Context Reference.

Conditional Logic

Use Liquid tags to add logic to your content:

If/Else Statements

{% if contact.industry == "Technology" %}
  Check out our tech solutions.
{% elsif contact.industry == "Healthcare" %}
  See our healthcare offerings.
{% else %}
  Explore our full product suite.
{% endif %}

Case Statements

{% case trigger.type %}
  {% when "form_submission" %}
    Thanks for filling out {{ trigger.form_submission.form_name }}!
  {% when "email_link_clicked" %}
    Thanks for clicking through from "{{ trigger.email.name }}"
  {% when "opportunity_created" %}
    Excited to kick off {{ trigger.opportunity.name }}!
  {% else %}
    Thanks for your interest!
{% endcase %}

Checking for Empty Values

{% if contact.company %}
  You work at {{ company.name }}
{% else %}
  Tell us about your company
{% endif %}

Filters

Filters transform values. Chain multiple filters with pipes:
{{ contact.first_name | upcase }}
{{ contact.email | split: "@" | last }}
{{ contact.created_at | date: "%B %d, %Y" }}

Common Filters

Converts text to uppercase.
{{ "hello" | upcase }}
Output: HELLO
Converts text to lowercase.
{{ "HELLO" | downcase }}
Output: hello
Capitalizes the first character.
{{ "hello" | capitalize }}
Output: Hello
Provides a fallback value when the input is empty or nil.
{{ contact.middle_name | default: "N/A" }}
Output: N/A (if middle_name is empty)
Formats a date using strftime syntax.
{{ now | date: "%B %d, %Y" }}
Output: January 15, 2025Common format codes:
  • %Y — 4-digit year (2025)
  • %m — Month as number (01–12)
  • %B — Full month name (January)
  • %d — Day of month (01–31)
  • %H — Hour in 24-hour format (00–23)
  • %M — Minute (00–59)
Returns the number of items in an array or characters in a string.
{{ company._opportunities | size }}
Output: 3 (if there are 3 opportunities)
Returns the first or last item of an array.
{{ company._opportunities | first }}
{{ company._opportunities | last }}
Divides a string into an array based on a delimiter.
{{ contact.email | split: "@" | last }}
Output: acme.com (for email [email protected])

Fallback Values

Use the default filter to provide fallback values when data is missing:
Hi {{ contact.first_name | default: "there" }},

Your company, {{ company.name | default: "your organization" }}, is in 
the {{ company.industry | default: "your industry" }} space.

Handling Missing Data

When Liquid syntax is valid but data isn’t available at runtime (for example, a form field wasn’t submitted), the expression evaluates to an empty string. This is not treated as an error.
{{ contact.middle_name }}  <!-- Empty string if not set -->
{{ trigger.form_submission.fields.optional_field }}  <!-- Empty string if not submitted -->
Best practices for handling missing data:
  1. Use fallbacks for values that might be empty:
    {{ contact.first_name | default: "there" }}
    
  2. Use conditionals to show or hide entire sections:
    {% if company.industry %}
      We specialize in {{ company.industry }}.
    {% endif %}
    
  3. Combine both for maximum flexibility:
    {% if contact.first_name %}
      Hi {{ contact.first_name }},
    {% else %}
      Hi there,
    {% endif %}
    

Validation and Errors

Conversion validates Liquid syntax in real-time as you write. Common validation errors include:
Error TypeExampleMessage
Invalid fieldcontact.unknown_field”unknown_field” is not a valid contact field
Invalid object typeobjects.unknown_type”unknown_type” is not a valid custom object type
Invalid indexcompany._opportunities[15]Index must be 0–9
Invalid tokentokens.unknown”unknown” is not a valid token
Syntax errorMissing closing bracesMissing closing braces

Validation by Context

ContextWhat Happens When Validation Fails
EmailEmail cannot be sent; marked as incomplete in workflows
Workflow nodeNode marked as “not set up”; workflow cannot be activated
WebhookWebhook cannot be saved
Validation catches syntax and configuration errors before runtime. Missing data at runtime (like an empty field) is handled gracefully and won’t cause errors.

Examples

Personalized Welcome Email

Hi {{ contact.first_name | default: "there" }},

Welcome to Conversion! 

{% if company.industry %}
We've helped many {{ company.industry }} companies like {{ company.name }} 
streamline their marketing operations.
{% else %}
We're excited to help {{ company.name | default: "your team" }} streamline 
your marketing operations.
{% endif %}

Best,
The Conversion Team

Form Submission Follow-up

{% if trigger.type == "form_submission" %}
Thanks for your interest in {{ trigger.form_submission.fields.product | default: "our solutions" }}, 
{{ contact.first_name }}!

{% if trigger.form_submission.fields.company_size == "Enterprise" %}
I'd love to schedule a call with our enterprise team to discuss your needs.
{% else %}
Check out our self-serve options to get started right away.
{% endif %}
{% endif %}

Opportunity Stage Change Notification

{% if trigger.type == "opportunity_updated" %}
{% if trigger.opportunity._changed.stage %}
Great news! {{ trigger.opportunity.name }} just moved from 
{{ trigger.opportunity._previous.stage }} to {{ trigger.opportunity.stage }}.

{% if trigger.opportunity.stage == "Closed Won" %}
🎉 Congratulations on closing the deal!
{% endif %}
{% endif %}
{% endif %}

Quick Reference

Object Syntax

ObjectDescriptionExample
contactContact fieldscontact.first_name
companyCompany fieldscompany.name
company._opportunitiesCompany’s opportunitiescompany._opportunities.first.name
opportunity_rolesContact’s opportunity rolesopportunity_roles.first.role
objects.<type>Custom objects by typeobjects.product[0].name
tokensGlobal tokenstokens.promo_code
triggerTrigger contexttrigger.type
nowCurrent UTC datetimenow (use with date filter)

Naming Conventions

PatternMeaningExample
object.fieldDirect field accesscontact.email
object._relationshipRelated objectcompany._opportunities
object[n]Array index (0–9)opportunity_roles[0]
object.first / .lastFirst or last itemcompany._opportunities.first
trigger.<type>Trigger-specific datatrigger.form_submission.form_name
trigger.object._previousPrevious valuetrigger.opportunity._previous.stage
trigger.object._changedChanged field flagstrigger.opportunity._changed.stage

Frequently Asked Questions

The underscore prefix (_) indicates a related object rather than a direct field. For example, company._opportunities accesses opportunities related to the company, while company.name accesses the company’s name field directly.
company._opportunities returns all opportunities associated with the contact’s company. opportunity_roles returns only opportunities where this specific contact has an Opportunity Contact Role (OCR). A contact might have access to company opportunities they’re not directly involved with.
Arrays like company._opportunities, opportunity_roles, and objects.<type> return up to 10 items, accessible via indexes 0–9.
Conversion validates your Liquid in real-time. If you reference a field that doesn’t exist (like a typo or deleted field), you’ll see a validation error and won’t be able to save or send until it’s fixed.
If the field exists but is empty for a particular contact, the expression evaluates to an empty string. Use the default filter to provide a fallback value.
Yes! Liquid is supported in both email subject lines and body content. This is a great way to personalize subject lines for better open rates.