Skip to Content
Variable SystemLiquid Templates

Liquid Templates

AppFunnel uses Liquid  as its template language for rendering dynamic content. Liquid templates work in text elements, HTML blocks, custom CSS, and event payloads — anywhere you need to display or compute values from variables.

Basic Interpolation

Wrap a variable name in double curly braces to output its value:

Hello, {{ user.name }}!

If user.name is "Sarah", this renders as: Hello, Sarah!

You can reference any variable namespace:

Your goal: {{ answers.incomeGoal }} Device: {{ device.type }} Browser: {{ browser.name }} Campaign: {{ query.utm_campaign }}

Nested Access

Product variables use dot notation to access properties:

{{ products.monthly.price }} {{ products.selected.period }} {{ products.yearly.trialDays }}

Filters

Filters transform the output of a variable. They are applied with a pipe (|) character after the variable name. Filters can be chained — each filter receives the output of the previous one.

{{ variable | filter }} {{ variable | filter1 | filter2 | filter3 }}

Math Filters

Use math filters for arithmetic operations on numbers.

FilterDescriptionExampleResult
divided_byDivision{{ 100 | divided_by: 3 }}33
timesMultiplication{{ 5 | times: 3 }}15
plusAddition{{ 10 | plus: 5 }}15
minusSubtraction{{ 10 | minus: 3 }}7
moduloRemainder{{ 10 | modulo: 3 }}1

Practical: Calculate monthly price from yearly

{{ products.yearly.rawPrice | divided_by: 12 | round: 2 }}

If the yearly plan is $59.99, this outputs 5.0 (the raw numeric result of 59.99 / 12, rounded to 2 decimal places).

Practical: Calculate savings percentage

{% assign monthlyTotal = products.monthly.rawPrice | times: 12 %} {% assign yearlySavings = monthlyTotal | minus: products.yearly.rawPrice %} {% assign savingsPercent = yearlySavings | divided_by: monthlyTotal | times: 100 | round: 0 %} Save {{ savingsPercent }}% with the yearly plan

Number Formatting Filters

FilterDescriptionExampleResult
roundRound to nearest integer (or N decimal places){{ 3.7 | round }}4
round: 2Round to N decimal places{{ 3.14159 | round: 2 }}3.14
floorRound down{{ 3.9 | floor }}3
ceilRound up{{ 3.1 | ceil }}4
absAbsolute value{{ -5 | abs }}5
to_fixed: NFormat to exactly N decimal places{{ 9.9 | to_fixed: 2 }}9.90

round and to_fixed behave differently. round: 2 returns a number (3.14), while to_fixed: 2 returns a string with trailing zeros (3.14 or 9.90). Use to_fixed when you want consistent decimal formatting for prices.

Practical: Format a per-day price

Only {{ products.yearly.rawPrice | divided_by: 365 | to_fixed: 2 }} per day

Result: Only 0.16 per day (for a $59.99/year plan).

String Filters

FilterDescriptionExampleResult
upcaseUppercase{{ "hello" | upcase }}HELLO
downcaseLowercase{{ "Hello" | downcase }}hello
capitalizeCapitalize first letter{{ "hello world" | capitalize }}Hello world
sizeString or array length{{ "hello" | size }}5

The default Filter

Provide a fallback value when a variable is empty, nil, or false:

Hello, {{ answers.name | default: "friend" }}!

If answers.name hasn’t been set yet, this renders: Hello, friend!

This is especially useful for pages that can be reached before a variable is populated:

Your goal: {{ answers.incomeGoal | default: "not set yet" }} Selected plan: {{ products.selected.displayName | default: "none" }}

Conditionals

Liquid supports if, elsif, else, and endif for conditional rendering. Content inside the tags is only rendered when the condition is true.

Basic If/Else

{% if answers.experience == 'advanced' %} Welcome back, pro! {% else %} Let's get you started! {% endif %}

Multiple Branches

{% if answers.goal == 'profit' %} Let's maximize your earnings. {% elsif answers.goal == 'savings' %} Smart saving starts here. {% elsif answers.goal == 'hobby' %} Enjoy the thrill of the hunt! {% else %} Welcome aboard. {% endif %}

Comparison Operators

Liquid supports these comparison operators inside {% if %} tags:

OperatorMeaning
==Equals
!=Not equals
>Greater than
<Less than
>=Greater than or equal
<=Less than or equal

Boolean Logic

Combine conditions with and / or:

{% if answers.experience == 'advanced' and answers.goal == 'profit' %} You're ready to start flipping for profit. {% endif %}
{% if device.isMobile or device.isTablet %} Tap to continue {% else %} Click to continue {% endif %}

Truthiness

In Liquid, the only falsy values are false and nil. Empty strings, 0, and empty arrays are truthy. To check for an empty string, compare explicitly:

{% if user.email != '' %} Logged in as {{ user.email }} {% endif %}

Or use the size filter:

{% if answers.interests.size > 0 %} You selected {{ answers.interests.size }} interests. {% endif %}

Unless

unless is the inverse of if:

{% unless purchase.success %} Complete your purchase to continue. {% endunless %}

Variables (Assign)

Use assign to create local variables within a template. Useful for intermediate calculations:

{% assign dailyCost = products.yearly.rawPrice | divided_by: 365 %} {% assign formattedCost = dailyCost | to_fixed: 2 %} That's just ${{ formattedCost }} per day.

Assigned variables only exist within the current template evaluation — they are not stored in the variable system.


Using Liquid in Text Elements

Text elements are the most common place for Liquid templates. The entire text content supports Liquid syntax.

Greeting with fallback

Hey {{ answers.name | default: "there" }}, based on your answers, here's your personalized plan.

Product pricing display

{{ products.selected.displayName }}: {{ products.selected.price }}/{{ products.selected.period }}

Result: Monthly Plan: $9.99/month

Trial messaging

{% if products.selected.hasTrial %} Start your {{ products.selected.trialDays }}-day free trial, then {{ products.selected.price }}/{{ products.selected.period }}. {% else %} Get started for {{ products.selected.price }}/{{ products.selected.period }}. {% endif %}

Savings comparison

{% assign monthlyAnnualized = products.monthly.rawPrice | times: 12 %} {% assign savings = monthlyAnnualized | minus: products.yearly.rawPrice %} Save ${{ savings | to_fixed: 2 }} per year with the annual plan.

Dynamic answer summary

Here's what we know: - Goal: {{ answers.goal | capitalize }} - Experience: {{ answers.experience | default: "not specified" }} - Budget: ${{ answers.budget | default: 0 | to_fixed: 2 }}/month

Using Liquid in Custom CSS

Liquid templates work in Custom CSS fields on any component. A quick example:

width: {{ page.progressPercentage }}%; transition: width 0.3s ease;

See Custom CSS for the full guide on pseudo-selectors, media queries, keyframes, and more.


Using Liquid in HTML Blocks

HTML Block elements support full Liquid templates within the raw HTML content. This is useful for embedding third-party widgets or creating custom markup:

<div class="welcome-banner"> <h2>Welcome, {{ user.name | default: "friend" }}!</h2> <p>Your {{ products.selected.periodly }} plan includes:</p> <ul> {% if products.selected.hasTrial %} <li>{{ products.selected.trialDays }}-day free trial</li> {% endif %} <li>Full access to all features</li> <li>Cancel anytime</li> </ul> </div>

For the complete filter reference, see Liquid Filter Reference.


Real-World Examples

Pricing card with savings badge

{% assign monthlyTotal = products.monthly.rawPrice | times: 12 %} {% assign savings = monthlyTotal | minus: products.yearly.rawPrice %} {% assign savingsPercent = savings | divided_by: monthlyTotal | times: 100 | round: 0 %} {{ products.yearly.price }}/year That's {{ products.yearly.rawPrice | divided_by: 12 | to_fixed: 2 }}/month — save {{ savingsPercent }}%

Countdown display from page time

{% assign remaining = 300 | minus: page.timeOnCurrent %} {% assign minutes = remaining | divided_by: 60 | floor %} {% assign seconds = remaining | modulo: 60 | round %} {{ minutes }}:{% if seconds < 10 %}0{% endif %}{{ seconds }}

This counts down from 5 minutes (300 seconds), displaying as 4:59, 4:58, etc.

Personalized summary page

{{ answers.name | default: "Friend" }}, here's your personalized plan: Based on your {{ answers.experience }} level experience and your goal of {{ answers.goal | downcase }}, we recommend the {{ products.selected.displayName }}. {% if products.selected.hasTrial %} Start with a {{ products.selected.trialDays }}-day free trial, then {{ products.selected.price }}/{{ products.selected.period }}. {% else %} Get started today for {{ products.selected.price }}/{{ products.selected.period }}. {% endif %}

Dynamic CSS progress bar

In a stack’s Custom CSS field:

width: {{ page.progressPercentage }}%; height: 4px; background: linear-gradient(90deg, #6366f1, #8b5cf6); border-radius: 2px; transition: width 0.4s ease;

Per-day cost in multiple currencies

{% assign dailyCost = products.yearly.rawPrice | divided_by: 365 | to_fixed: 2 %} Just {{ products.yearly.currencySymbol }}{{ dailyCost }} per day

Conditional discount messaging

{% if data.showDiscount %} Limited time: get {{ data.discountPercent }}% off! {% assign discounted = products.selected.rawPrice | times: data.discountPercent | divided_by: 100 %} {% assign finalPrice = products.selected.rawPrice | minus: discounted %} New price: {{ products.selected.currencySymbol }}{{ finalPrice | to_fixed: 2 }}/{{ products.selected.period }} {% endif %}

Tips and Gotchas

Liquid whitespace can create unexpected gaps in text. Use {%- -%} (with hyphens) to strip whitespace around tags if you see extra spacing in rendered output:

{%- if answers.name -%}Hello, {{ answers.name }}{%- else -%}Hello{%- endif -%}!
  • Undefined variables render as empty strings — the template engine is lenient and will not throw errors for missing variables.
  • Filters can be chained — each filter receives the output of the previous one, evaluated left to right.
  • Numeric strings are auto-converted — if a variable contains "42", math filters will treat it as a number.
  • Assigned variables are local{% assign %} creates template-local variables, not funnel variables.
  • Product variables use formatted stringsproducts.monthly.price returns "$9.99" (with symbol). Use rawPrice when you need a number for calculations.
Last updated on