Customer.io Liquid Personalisation: The Practical Marketer's Tutorial
When Amazon Taught the World What "Personal" Really Means
In the summer of 2003, Amazon quietly rolled out something that would reshape ecommerce forever. They called it the collaborative filtering engine — a recommendation system that tracked what every single customer browsed, purchased, and rated, then used that data to predict what each person would want next.
The famous "Customers who bought this also bought..." strip wasn't a design flourish. It was a calculated bet that relevance would beat volume every time. Within two years, Amazon's own researchers reported that 35% of their total revenue came directly from the recommendation engine. Not from more products, not from a bigger catalogue — from showing each customer something that actually matched them.
Here's the thing: Amazon didn't get there with a fancy AI black box. They got there with structured data and logic. If customer bought this, show them that. If customer is in this segment, surface this content. Sound familiar?
That's exactly what Customer.io's Liquid personalisation lets you do inside every email you send.
You already have the customer data. You're already sending the emails. Liquid is the missing piece that connects the two — turning a broadcast into a conversation, and a template into a personal message. And according to SQ Magazine's 2025 email personalisation report, personalised emails now deliver $52 return for every $1 spent in Q1 2025, with clear personalised CTAs performing 202% better than generic ones.
This tutorial goes deep on four things: conditional content, personalised product recommendations, dynamic CTAs, and bulletproof default value handling. Every code block is copy-paste ready for Customer.io. No coding background required.
What Is Liquid and Why Does Customer.io Use It?
Liquid is an open-source templating language originally built by Shopify in 2006. Customer.io adopted it because it's readable, flexible, and designed for exactly this use case: injecting dynamic data into text-based content at the moment of sending.
When Customer.io delivers an email, it processes every Liquid expression in the template and replaces it with real data for that specific recipient. The result is a fully rendered, personalised message — not a template with visible brackets.
Liquid has three building blocks. That's it.
Keys — Placeholders that pull in data. They use double curly braces:
{{ customer.first_name }}
Filters — Modifiers that transform output. They come after a pipe |:
{{ customer.first_name | capitalize }}
Tags — Logic and control flow. They use curly braces with percent signs:
{% if customer.plan == "pro" %}
You're on Pro!
{% endif %}
Everything in this tutorial builds on those three things. If you can understand plain English instructions, you can write Liquid.
Which Liquid version are you on? Customer.io has two versions. If your account was created on or after November 28, 2023, you're on the latest version (which supports the
defaultfilter). Older accounts may be on legacy Liquid. Check under Settings > Workspace Settings. All examples in this tutorial work on the latest version; legacy alternatives are noted where relevant.
Part 1: Handling Default Values (Start Here — It's That Important)
Before you write a single conditional or product loop, you need to understand default values. This isn't an advanced topic. It's the foundation. Get this wrong and your emails either fail to send entirely, or they land in inboxes looking broken.
Why Missing Attributes Break Emails
Customer.io's Liquid is strict: if you reference an attribute that doesn't exist on a profile, the message fails to send. No blank space. No empty field. A complete delivery failure, silently logged in your activity feed.
Every customer profile in your workspace is different. Some people signed up with full details. Others gave you just an email. You can't assume everyone has the attribute you're trying to use.
The default Filter (Latest Liquid — Recommended)
The cleanest way to handle missing data:
Hi {{ customer.first_name | default: "there" }},
- Sarah gets: Hi Sarah,
- Someone with no
first_namegets: Hi there,
The default filter accepts any string as the fallback. Use it on every customer attribute you reference.
Your {{ customer.plan | default: "current" }} plan renews on
{{ customer.renewal_date | date: "%B %-d, %Y" | default: "the date shown in your account" }}.
You can chain multiple filters together. The default filter catches the result if everything before it returns blank.
The if/else Fallback (Works on All Liquid Versions)
If you're on legacy Liquid or want maximum compatibility:
Hi {% if customer.first_name %}{{ customer.first_name | capitalize }}{% else %}there{% endif %},
It's more verbose, but it's the same logic — check if the value exists, show it if yes, show the fallback if no.
Assigning a Default with assign
Sometimes you want to set a fallback variable once and use it multiple times:
{% assign name = customer.first_name | default: "there" %}
{% assign plan = customer.plan | default: "free" %}
Hi {{ name }},
Your {{ plan }} plan comes with the following benefits...
This keeps your template clean. Define your fallbacks at the top, reference the variables below.
Default Values for Numbers
Numbers need special handling. Customer.io stores all attributes as strings — even ones that look like numbers. If you reference a missing numeric attribute without a fallback, the message fails.
{% assign score = customer.engagement_score | default: 0 | plus: 0 %}
The | default: 0 provides the fallback. The | plus: 0 converts the string to an actual number so you can use it in comparisons. Without the conversion, "90" > "100" evaluates as true (because "9" sorts after "1" alphabetically) — which is obviously not what you want.
A Complete Default Values Reference
| Attribute Type | Safe Pattern |
|---|---|
| Text (name, plan) | {{ customer.field | default: "fallback text" }} |
| Number (score, count) | {% assign n = customer.field | default: 0 | plus: 0 %} |
| Boolean flag | {% if customer.flag == true %} — false/nil both hit the else |
| Date | {{ customer.date | date: "%B %-d, %Y" | default: "soon" }} |
| Nested attribute | {{ customer.address.city | default: "your city" }} |
| Array (loop) | {% if customer.items.size > 0 %}...{% else %}fallback{% endif %} |
Rule of thumb: If you're unsure whether every profile has a given attribute, add a default. It takes three seconds to type and prevents delivery failures for an unknown chunk of your audience.
Part 2: Conditional Content — One Template, Many Experiences
Conditional content is where Liquid delivers its biggest payoff. Instead of building separate campaigns for every customer segment, you write one template that branches automatically based on user attributes.
Basic if/elsif/else — The Workhorse Conditional
{% if customer.plan == "enterprise" %}
As an Enterprise customer, you get dedicated support and a personal account manager.
Your manager {{ customer.account_manager | default: "will reach out" }} can help you
get the most from your setup.
{% elsif customer.plan == "pro" %}
You're on Pro — great choice.
One more step: Enterprise gives you dedicated infrastructure and custom integrations.
Want to see if it makes sense for your team?
{% elsif customer.plan == "starter" %}
You're on Starter. Here are three things you can do right now to get more value.
{% else %}
Start your free trial and see what fits your team.
{% endif %}
One email. Four completely different blocks of content. The recipient only sees the block that matches their plan attribute — everything else is stripped at send time.
Note the order: most specific condition first (enterprise), catch-all else last. Liquid evaluates from top to bottom and stops at the first match.
case/when — Cleaner for Multiple Options
When you have more than four or five options, if/elsif chains get messy. case/when is cleaner:
{% case customer.industry %}
{% when "saas" %}
Here's how SaaS teams use automated onboarding to reduce time-to-value.
{% when "ecommerce" %}
See how online retailers use post-purchase sequences to drive repeat orders.
{% when "fintech" %}
Discover how fintech brands personalise without compromising compliance.
{% when "healthcare" %}
HIPAA-friendly engagement strategies that keep patients informed.
{% when "agency" %}
Scale your client campaigns with multi-brand automation.
{% else %}
Here's how companies like yours are getting results.
{% endcase %}
case/when checks a single variable against multiple values. It's faster to read and harder to break than a stack of elsif statements.
Conditional Content Based on Behaviour
Attributes aren't just plan types and locations. Track any behaviour you care about and branch on it:
{% if customer.has_connected_integration == true %}
Your integration is live — here's how to get even more from it.
{% else %}
One quick step: connect your first integration and unlock the full workflow.
It takes about three minutes.
{% endif %}
{% assign logins = customer.logins_last_30_days | default: 0 | plus: 0 %}
{% if logins == 0 %}
We haven't seen you in a while. Here's a quick way to pick up where you left off.
{% elsif logins < 5 %}
You've logged in {{ logins }} times this month — here are features you haven't tried yet.
{% else %}
You're a power user. Here's what's coming in our next release.
{% endif %}
This is the foundation of behaviour-triggered journeys — sending messages based on what people actually do, not arbitrary timelines.
Combining Conditions with and / or
Check multiple attributes at once:
{% if customer.plan == "enterprise" and customer.days_since_last_login | plus: 0 > 14 %}
We noticed some inactivity on your Enterprise account.
Your account manager {{ customer.account_manager | default: "would love" }} to help.
{% endif %}
{% if customer.role == "admin" or customer.role == "owner" %}
As a team admin, here's your team's activity summary for this month.
{% endif %}
One gotcha with and/or: Liquid evaluates left-to-right with no operator precedence. a and b or c means (a and b) or c. Use nested if statements if you need more complex logic.
Conditional Sections Based on Lifecycle Stage
One of the most useful patterns for SaaS and subscription businesses:
{% assign days = customer.days_as_customer | default: 0 | plus: 0 %}
{% if days <= 7 %}
**You're in your first week.** Let's make sure you're set up properly.
Most new customers who complete setup in week one are still customers a year later.
{% elsif days <= 30 %}
**One month in — nice.** Here's what experienced users do differently.
{% elsif days <= 365 %}
**You've been with us {{ days }} days.** Here are features that match where you are now.
{% else %}
**Over a year.** Thank you for sticking with us. Genuinely.
As a long-term customer, here's something exclusive for you.
{% endif %}
For a deeper look at lifecycle segmentation, see our guide on lifecycle email marketing.
Part 3: Personalised Product Recommendations
This is the Amazon play. You already have purchase history, browsing data, and product preferences stored against customer profiles. Liquid lets you surface that data directly inside an email — no third-party tool required.
Setting Up Product Recommendation Data
For Liquid product loops to work, you need product data stored as an array attribute on customer profiles (or passed via event payload). An array looks like this in your customer profile:
{
"recently_viewed": [
{ "name": "Merino Wool Tee", "price": "49.00", "url": "https://shop.com/merino-tee" },
{ "name": "Running Cap", "price": "28.00", "url": "https://shop.com/cap" },
{ "name": "Trail Shorts", "price": "65.00", "url": "https://shop.com/trail-shorts" }
]
}
You can populate this via Customer.io's Track API, a reverse ETL tool like Hightouch or Census, or directly through your backend. Once it's there, Liquid can loop through it.
The Basic Product Recommendation Loop
{% if customer.recently_viewed.size > 0 %}
## You left these behind
{% for product in customer.recently_viewed limit: 3 %}
**{{ product.name | default: "Product" }}**
${{ product.price | default: "—" }}
[View Product]({{ product.url }})
{% endfor %}
{% else %}
## Trending this week
You haven't browsed recently — here's what's popular right now.
[See What's Trending](https://shop.example.com/trending)
{% endif %}
Key details:
limit: 3caps the loop at three items — keep emails concise- The
elseblock ensures everyone sees something, even without browsing data - Fallbacks inside the loop protect against incomplete product records
Event-Triggered Recommendations (Post-Purchase)
When a purchase event triggers the campaign, use the event object to reference what the customer just bought:
Thanks for your order, {{ customer.first_name | default: "there" }}!
You picked up: **{{ event.product_name | default: "your item" }}**
{% if event.category == "footwear" %}
### Complete the look
Most customers who buy footwear also pick up:
- Performance running socks — $12
- Shoe care kit — $18
- Lace-up bag — $35
[Shop Footwear Accessories](https://shop.example.com/accessories/footwear)
{% elsif event.category == "outerwear" %}
### Stay warm out there
Pair your new outerwear with:
- Merino base layer — $65
- Thermal gloves — $32
- Neck gaiter — $20
[Shop Layers](https://shop.example.com/layers)
{% else %}
### Customers like you also love
[Browse Bestsellers](https://shop.example.com/bestsellers)
{% endif %}
Segment-Based Curated Recommendations
If you don't have array-based product data, curate recommendations manually by segment:
{% assign ltv = customer.lifetime_value | default: 0 | plus: 0 %}
{% assign category = customer.favourite_category | default: "general" %}
{% if ltv > 500 %}
### Your VIP picks this season
{% case category %}
{% when "running" %}
- [Carbon-plate race shoe — $240](https://shop.example.com/race-shoe)
- [GPS running watch — $299](https://shop.example.com/gps-watch)
{% when "yoga" %}
- [Natural rubber mat — $120](https://shop.example.com/premium-mat)
- [Alignment blocks set — $45](https://shop.example.com/blocks)
{% else %}
- [View your personalised recommendations](https://shop.example.com/for-you?id={{ customer.id }})
{% endcase %}
{% else %}
### Popular in your category
[See what's trending in {{ category | replace: "-", " " | capitalize }}](https://shop.example.com/trending/{{ category }})
{% endif %}
Looping Through an Order Summary
## Your order summary
{% for item in event.items %}
- {{ item.name | default: "Item" }} × {{ item.quantity | default: 1 }} — ${{ item.price | default: "—" }}
{% endfor %}
**Total:** ${{ event.total | default: "—" }}
{% if event.items.size >= 3 %}
You qualified for free shipping on this order. 🎉
{% endif %}
Part 4: Dynamic CTAs Based on User Attributes
A CTA that says "Get Started" to someone already three months into their subscription is wasted. Liquid lets you match the CTA to exactly where each customer is in their journey — automatically.
Plan-Based CTA Branching
{% if customer.plan == "free" or customer.plan == nil %}
[Start Your Free Trial — No Card Required](https://app.example.com/trial)
{% elsif customer.plan == "trial" %}
[Subscribe Before Your Trial Expires](https://app.example.com/subscribe)
{% elsif customer.plan == "starter" %}
[Upgrade to Pro — Unlock Advanced Features](https://app.example.com/upgrade?from=starter&to=pro)
{% elsif customer.plan == "pro" %}
[Explore Enterprise — Dedicated Support & Scale](https://app.example.com/enterprise)
{% else %}
[Go to Your Dashboard](https://app.example.com/dashboard)
{% endif %}
Urgency-Driven CTAs with Date Logic
{% assign now_ts = 'now' | date: '%s' | plus: 0 %}
{% assign trial_end_ts = customer.trial_end_date | plus: 0 %}
{% assign seconds_left = trial_end_ts | minus: now_ts %}
{% assign days_left = seconds_left | divided_by: 86400 %}
{% if days_left <= 0 %}
Your trial has ended. [Reactivate your account today](https://app.example.com/reactivate)
{% elsif days_left <= 3 %}
⚠️ {{ days_left }} day{% if days_left != 1 %}s{% endif %} left on your trial.
[Subscribe now — pick the plan that fits](https://app.example.com/subscribe?urgent=true)
{% elsif days_left <= 7 %}
Your trial ends in {{ days_left }} days.
[Compare plans and lock in your rate](https://app.example.com/pricing)
{% else %}
[Explore everything you can do before your trial ends](https://app.example.com/features)
{% endif %}
The 86400 is the number of seconds in a day. The {% if days_left != 1 %}s{% endif %} handles singular/plural — "1 day" vs "3 days."
Role-Based CTAs for B2B Products
{% case customer.role %}
{% when "owner" %}
[Manage your organisation and billing settings](https://app.example.com/settings/org)
{% when "admin" %}
[Review your team's activity and permissions](https://app.example.com/admin/team)
{% when "manager" %}
[See your team's performance this month](https://app.example.com/reports/team)
{% when "member" %}
[Jump back into your workspace](https://app.example.com/workspace)
{% else %}
[Go to your dashboard](https://app.example.com/dashboard)
{% endcase %}
Engagement-Score-Triggered CTAs
{% assign score = customer.engagement_score | default: 0 | plus: 0 %}
{% if score >= 80 %}
You're one of our most engaged customers. Want to join our beta programme?
[Apply for early access](https://app.example.com/beta?ref=engaged-email)
{% elsif score >= 50 %}
Here's a feature you might not have tried yet.
[Explore {{ customer.suggested_feature | default: "Advanced Reporting" }}](https://app.example.com/features)
{% elsif score >= 20 %}
We want to make sure you're getting value. Can we help?
[Book a 15-minute onboarding call](https://calendly.example.com/onboarding?user={{ customer.id }})
{% else %}
We haven't seen you in a while. Let's fix that.
[Here's what changed since you last logged in](https://app.example.com/whats-new)
{% endif %}
Common Mistakes (and How to Avoid Them)
1. No Fallback on Critical Attributes
What happens: Customer.io can't render the template — delivery failure — that customer gets nothing.
Fix: Use | default: "fallback" on every attribute you reference. Takes five seconds. Prevents 100% delivery failure for everyone missing that attribute.
2. Comparing Numbers Stored as Strings
What happens: {% if customer.score > 50 %} returns unexpected results because "9" > "50" in string comparison.
Fix:
{% assign score = customer.score | default: 0 | plus: 0 %}
{% if score > 50 %}
Always convert with | plus: 0 before numeric comparisons.
3. Forgetting Closing Tags
What happens: Liquid throws a parse error and the entire message fails.
Fix: Every {% if %} needs {% endif %}. Every {% for %} needs {% endfor %}. Every {% case %} needs {% endcase %}.
4. Using event Outside an Event-Triggered Campaign
What happens: {{ event.product_name }} renders blank or causes a failure.
Fix: The event object only works in event-triggered campaigns. For other campaign types, use customer attributes.
5. Broken Conditionals with > or < in the Visual Editor
What happens: The drag-and-drop editor interprets > and < as HTML tags and corrupts your Liquid.
Fix: Use the "Add Liquid" button inside text blocks when writing conditions with comparison operators.
How to Test Liquid Before You Send
Step 1: Preview with a Real Profile
Open any message in Customer.io and use the Preview panel. Search for a specific customer by email or name. The preview replaces all Liquid with that person's real data.
Test with at least three profiles: one with all attributes filled in, one with minimal data (email only), and one with unusual values.
Step 2: Send a Test Email
Use Send Test to deliver the email to yourself using a specific customer's data. Check every conditional branch.
Step 3: Check the Activity Log After Sending
After any campaign send, check Activity Logs for individual recipients. The log shows exactly what Liquid rendered for that person, and flags which expression caused any failure.
Frequently Asked Questions
What is Liquid personalisation in Customer.io?
Liquid personalisation in Customer.io is the use of Liquid templating language to insert dynamic, customer-specific content into emails, SMS, push notifications, and in-app messages. Rather than sending the same static message to everyone, Liquid lets you reference customer attributes, event data, and logical conditions directly in your templates. When Customer.io sends the message, it replaces every Liquid expression with real data for that specific recipient.
Do I need to know how to code to use Liquid in Customer.io?
No. Liquid is designed to be readable and writable without a programming background. The syntax mirrors plain English logic — "if this, show that, otherwise show something else." Most marketers get comfortable with basic personalisation (names, plan-based conditionals, default values) within an hour of practice.
What happens if a customer is missing an attribute I reference?
If Liquid can't evaluate an expression — because the attribute is missing, null, or the wrong type — the message fails to send entirely. It won't show a blank space or an error message to the recipient. It simply won't deliver. This is why fallback values are non-negotiable. Use | default: "fallback" on every attribute, or use {% if customer.attribute %}...{% else %}...{% endif %} to handle missing values gracefully.
What's the difference between customer, event, and trigger data?
The customer object contains attributes stored on a person's profile — name, plan, signup date, any custom data you track. It works in any message. The event object contains properties from the specific event that triggered an event-triggered campaign — like event.product_name from a purchase event. It only works inside that triggered campaign. The trigger object contains payload data sent with API-triggered broadcasts and transactional messages.
How do I show different products to different customers?
Store product data as an array attribute on customer profiles, then use a Liquid for loop to iterate through it. Use limit: to cap the number of items displayed. Include conditional blocks using customer attributes (category preference, purchase history, lifetime value) to surface manually curated recommendations when you don't have array data. Always include an else block with a fallback link for customers who have no product data.
How do I create a personalised CTA?
Use {% if/elsif/else %} or {% case/when %} tags to branch on a customer attribute — most commonly plan, role, lifecycle_stage, or days_since_trial_end. Write a different link text and URL for each branch. You can also embed customer attributes directly inside URLs for deep links, pre-filled forms, or tracked attribution. Every CTA branch should have an else fallback so no customer ends up with a broken or irrelevant button.
How do I handle numbers in Liquid conditions?
Customer.io stores all attributes as strings, so you must convert them to numbers before using comparison operators. Add | plus: 0 after any numeric attribute: {% assign score = customer.score | plus: 0 %}. Then use {% if score > 50 %} normally. Without this conversion, string comparison rules apply — and "9" > "50" evaluates as true, which produces wrong results.
Can I use Liquid in email subject lines and preview text?
Yes. Liquid works in subject lines, preview text, the sender name field, and any text field in Customer.io — not just the email body. Personalised subject lines are one of the highest-impact uses of Liquid. According to 2025 data from SQ Magazine, personalised subject lines see up to 35% higher open rates. Remember to include fallbacks — a Liquid failure in a subject line prevents the entire message from sending.
What is the default filter and when should I use it?
The default filter is a Liquid filter available in Customer.io's latest Liquid version. It provides a fallback value when an attribute is missing, null, or empty: {{ customer.first_name | default: "there" }}. Use it on every attribute you reference. It replaces the older approach of wrapping everything in {% if %}{% else %}{% endif %} statements, making templates cleaner and easier to maintain.
How do I format dates in Customer.io Liquid?
Use the date filter: {{ customer.renewal_date | date: "%B %-d, %Y" }} outputs "October 1, 2025". Customer.io stores dates as Unix timestamps (seconds since January 1, 1970). Common format codes: %B (full month name), %-d (day without zero padding), %Y (four-digit year), %A (day of week). To get today's date in any format, use {{ 'now' | date: "%B %-d, %Y" }}.
What's the | plus: 0 trick and why do I need it?
Customer.io stores all customer attributes as strings, even numeric ones. The | plus: 0 filter converts a string to a number by adding zero to it — "42" | plus: 0 becomes 42. You need this before any numeric comparison (>, <, >=) or arithmetic operation. Without it, comparisons use alphabetical string rules rather than numeric rules, which gives wrong results for values like "9" vs "50".
How do I loop through a list and show only the first three items?
Use a for loop with the limit: parameter: {% for item in customer.items limit: 3 %}. Access the item's properties with dot notation: {{ item.name }}, {{ item.price }}, {{ item.url }}. Use forloop.index for a 1-based counter, forloop.first and forloop.last to detect the start and end of the loop.
Can I nest if conditions inside loops?
Yes. You can nest conditions inside loops and loops inside conditions. Common use case: loop through products but only display items above a certain price, or only show a "bestseller" badge for the first item. Keep nesting to two or three levels maximum for readability.
How do I test Liquid without sending to real customers?
Use Customer.io's Preview panel to view a message as a specific customer, with all Liquid rendered using their real data. Use Send Test to deliver the rendered email to your own inbox using a chosen customer's profile. Create dedicated test profiles with different attribute configurations — complete data, minimal data, edge cases — and run each through the preview.
What's the difference between legacy and latest Liquid in Customer.io?
The latest Liquid version (for accounts created after November 28, 2023, or after a manual upgrade) supports the default filter, improved error messages, and better filter support. Legacy Liquid requires {% if %}{% else %}{% endif %} for fallbacks instead of the clean | default: syntax. Check Settings > Workspace Settings to see which version you're on and to upgrade.
Quick Reference Card
| What You Want | Liquid Code |
|---|---|
| Name with fallback | {{ customer.first_name | default: "there" }} |
| Capitalise | {{ customer.first_name | capitalize }} |
| Assign a safe variable | {% assign plan = customer.plan | default: "free" %} |
| Convert string to number | {% assign n = customer.score | default: 0 | plus: 0 %} |
| Basic conditional | {% if customer.plan == "pro" %}...{% endif %} |
| If/else | {% if condition %}A{% else %}B{% endif %} |
| Multiple plan branches | {% case customer.plan %}{% when "pro" %}...{% endcase %} |
| Loop through array | {% for item in customer.items %}{{ item.name }}{% endfor %} |
| Limit loop | {% for item in list limit: 3 %}...{% endfor %} |
| Loop counter | {{ forloop.index }} (1-based), {{ forloop.index0 }} (0-based) |
| Format date | {{ customer.date | date: "%B %-d, %Y" }} |
| Today's date | {{ 'now' | date: "%B %-d, %Y" }} |
| Current year | {{ 'now' | date: "%Y" }} |
| Days calculation | {% assign days = end_ts | minus: now_ts | divided_by: 86400 %} |
| Singular/plural | {{ count }} item{% if count != 1 %}s{% endif %} |
| Dynamic URL | https://app.com/dashboard?user={{ customer.id }} |
| Empty array check | {% if customer.items.size > 0 %} |
| Event data | {{ event.product_name | default: "your item" }} |
Your Next Step
Amazon's recommendation engine drove 35% of their revenue not with magic — with logic. The same logic is sitting inside Customer.io right now, waiting for you to use it.
Start with one thing: open your highest-volume email campaign and add a default filter to every attribute you're using. That alone will prevent delivery failures you probably don't know are happening.
Then add one conditional. Then try a product loop. The patterns in this tutorial are modular — you can introduce them one at a time without rebuilding anything from scratch.
If you'd rather hand this over to someone who does it every day, NerveCentral is a Customer.io Certified Partner. We build Liquid-powered campaigns that make every message feel personal — and we measure the results so you can see exactly what's working.
For more on building the full customer journey around personalisation, read our guides on behaviour-triggered journeys, onboarding email sequences, and omnichannel messaging strategy.
Sources & Citations
-
Shopify. Liquid Open-Source Templating Language. Shopify GitHub.
-
Customer.io. Personalize messages with Liquid. Customer.io Documentation, 2025.
-
Customer.io. Liquid syntax list. Customer.io Documentation, December 2024.
-
Customer.io. Email personalization gone wrong (and how to fix it). Customer.io Blog, May 2024.
-
SQ Magazine. Personalized Email Marketing Statistics 2026. SQ Magazine, May 2025.
-
MoEngage. Email Marketing Benchmarks 2025. MoEngage, 2025.
-
Customer.io. 32 Personalized Marketing Messages Best Practices. Customer.io Learn, September 2024.
-
Harvard Business Review. How Amazon Uses Data to Sell More. HBR, 2010.
-
SalesSo. Email Marketing ROI Statistics 2025. SalesSo Blog, June 2025.


