Whether you're building transactional emails, in-app notifications, or SMS messages, the ability to create dynamic, personalized content is crucial for engaging your users.

The Liquid templating language is a popular standard for rendering dynamic content, powering everything from simple variable substitution to complex conditional logic and data transformations.

Before diving into syntax and implementation details, let's understand what makes Liquid the preferred choice for notification templating and how its core components work together to create dynamic content.

What is Liquid?

Liquid is an open-source template language created by Shopify that strikes an optimal balance between power and simplicity. Unlike heavyweight templating engines that require extensive programming knowledge, Liquid provides a clean, readable syntax that both developers and non-technical team members can understand and modify.

At its core, Liquid serves as a bridge between your static template content and dynamic user data. When you write a notification template with Liquid, you're creating a blueprint that gets filled in with specific values at runtime.

For example, a simple welcome email template might look like this:

Hello {{ user.name }},
 
Welcome to {{ company.name }}! Your account has been created with the email {{ user.email }}.
 
{% if user.trial_days > 0 %}
Your {{ user.trial_days }}-day trial has started. Make the most of it!
{% else %}
You're all set to start using our platform.
{% endif %}
 
Best regards,
The {{ company.name }} Team

This template demonstrates Liquid's declarative nature. You describe what the output should look like rather than how to generate it. The template engine handles the complexity of parsing, evaluating conditions, and assembling the final output.

The three main components of Liquid

Liquid's syntax revolves around three fundamental components that work together to create dynamic templates: keys, filters, and tags. Understanding how these components interact is essential for mastering Liquid templating.

Keys (also called objects or variables) are placeholders for dynamic data. They're wrapped in double curly braces {{ }}, which are also called outputs, and represent values that will be injected into your template at runtime. Keys can access nested properties using dot notation, making it easy to work with complex data structures.

Filters modify the output of keys, allowing you to transform data without changing the underlying values. Filters are applied using the pipe character | and can be chained together to perform multiple transformations. They handle common formatting tasks like capitalizing text, formatting dates, or performing calculations.

Tags create the logic and control flow in your templates. Wrapped in curly braces with percent signs {% %}, tags enable conditional rendering, loops, variable assignment, and other programmatic features. Tags are what transform Liquid from a simple substitution language into a full templating system.

Real-life examples of Liquid

Liquid becomes most useful when you see how it transforms dynamic data into personalized, context-aware messages. Below, we'll walk you through some real examples so you can see Liquid's flexibility in practice.

Personalizing with simple variables

At its simplest, Liquid lets you inject dynamic data into text using variables.

Hi {{ user.first_name }},

If your data includes a user.first_name field, this automatically becomes:

Hi Sarah,

This pattern—using curly braces {{ }} to render a variable—is the foundation of every Liquid template.

Adding fallback values

Real-world data isn't always complete. Liquid supports filters like default to handle missing variables gracefully.

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

If user.first_name doesn't exist, this becomes:

Hi there,

This small safeguard is crucial for templates that scale across thousands of users.

Formatting data with filters

Filters let you transform raw values before rendering them.

Your subscription renews on {{ subscription.renewal_date | date: "%B %d, %Y" }}.

If renewal_date equals 2025-10-20, Liquid renders:

Your subscription renews on October 20, 2025.

You can also chain together multiple filters in a string for more control—for example, | upcase | truncate: 50, where | upcase converts the string before it to uppercase, and | truncate: 50 limits it to 50 characters followed by an ellipsis (...). Liquid executes each filter from left to right.

Conditional content with tags

Liquid tags add logic to your templates, enabling conditional rendering.

{% if user.is_trial %}
  Your trial ends in {{ user.days_left }} days. Upgrade now to keep access.
{% else %}
  Thanks for being a paying customer!
{% endif %}

Here, {% if %} and {% endif %} wrap logic that determines what appears based on data. This makes your messages adaptive to each user's state.

Iterating over lists

Liquid can loop through arrays, making it easy to display dynamic lists like active projects or notifications.

{% for project in user.projects %}
- {{ project.name }} ({{ project.status }})
{% endfor %}

If user.projects includes three active projects, Liquid outputs a personalized summary for each one.

Combining filters, tags, and operators

As complexity grows, you can combine multiple Liquid concepts for nuanced personalization.

{% if user.last_login_at > 7 | days_ago %}
  Welcome back, {{ user.first_name }}! It's been a while.
{% else %}
  Great to see you again, {{ user.first_name }}.
{% endif %}

This example uses a comparison operator (>) and a filter (days_ago) to create context-aware copy that changes with user behavior.

Building dynamic callouts

You can even embed logic and formatting inside more advanced content structures.

{% assign usage = account.sent | divided_by: account.limit | times: 100 %}
{% if usage > 90 %}
⚠️ You've used {{ usage | round }}% of your limit.
{% elsif usage > 50 %}
You're over halfway to your limit.
{% else %}
You're in great shape this month!
{% endif %}

Here, arithmetic filters, conditional tags, and variables work together to deliver personalized status updates dynamically.

Putting it all together

Let's examine how these components work together in a real notification scenario. Consider a SaaS platform sending a usage alert:

{% assign usage = user.calls | divided_by: user.limit | times: 100 | round %}
{% assign remaining = user.limit | minus: user.calls %}
 
Hi {{ user.name | capitalize }},
 
You've used {{ usage }}% of your API calls this month.
 
{% if usage >= 90 %}
  ⚠️ You're at {{ usage }}% of your limit.
  Consider upgrading to {{ next_plan }}.
{% elsif usage >= 75 %}
  You have {{ remaining | format_number }} calls left.
{% else %}
  You're on track this month!
{% endif %}
 
View details: {{ dashboard_url }}?id={{ user.id | url_encode }}

This example showcases several Liquid concepts working in harmony:

  • Variable assignment using the assign tag to calculate derived values.
  • Mathematical filters to compute usage percentage and remaining calls.
  • Conditional logic with if/elsif statements to customize the message.
  • String filters like capitalize and format_number for consistent formatting.
  • URL building with the url_encode filter for safe parameter passing.

​​These examples demonstrate how Liquid templates evolve from simple text substitution to data-driven personalization and branching logic. Whether you're generating onboarding emails, billing notifications, or in-app messages, mastering these techniques lets you create adaptive templates that respond to real user data with minimal code.

In the following chapters, we'll go deeper into each one of these features of Liquid so that you can feel confident in building dynamic templates that delight your customers.