Liquid & Live Data Reference: Every Variable, Filter and Field

Funnelish pages pull dynamic content from two data contexts. Which context holds your data decides the syntax you write, so start here before writing anything.

Server-side data (Liquid)

  • When it renders: on our servers, while the page is being served. The output is in the HTML source, so search engines read it.

  • Syntax: Liquid, written as {{ }} for output and {% %} for logic.

  • Products: names, titles, prices, images, subscription and trial terms, order bump flags.

  • Blog content: the current post, post collections, categories.

  • Page metadata: title, URL, SEO fields.

  • Customer portal products: the portal’s catalog and its variants.

Covered in Part 1.

Client-side data (x-attributes)

  • When it renders: in the visitor’s browser, after the page loads. Not in the HTML source.

  • Syntax: x-attributes on elements, like x-text and x-show. Not {{ }}.

  • Customer details: name, email, phone, addresses, custom fields, as the visitor enters them.

  • The live order: selected products, purchased products, the running total.

  • Visit context: URL parameters and UTM tags.

  • Portal subscriptions: status, next billing date, items, payment method.

Covered in Part 2.

:warning: The most common mistake: writing {{ customer.first_name }} in Liquid. Customer data doesn’t exist when the page is rendered, so that outputs nothing. Customer, order and subscription data are client-side.

New here? Read Data Binding first. The picker handles most cases with no code at all.


Part 1: Server-side data (Liquid)

Liquid runs on our servers when the page is served, so its output is in the HTML before the browser sees it. That makes it the right context for anything search engines should read.

Where you write it

Liquid works anywhere text can go, across all page types: funnels, stores, blogs and customer portals, including inside shared components.

  • Any text on the page: headlines, paragraphs, button labels. Get {{ product.title }} today inside a button works exactly as you’d expect.

  • Custom HTML elements (recommended): for anything beyond a quick inline value, write Liquid in a custom HTML element. Text elements run through the rich-text editor, which can inject formatting or auto-link dotted paths inside your expression and quietly break it. A custom HTML element is plain code, nothing interferes.

  • Custom code binding slots: any binding slot in the Data tab accepts Liquid as custom code, which is how you put a computed value behind a normal, styleable element.

:warning: Automation emails and SMS are not Liquid. They use a separate engine with their own field inserter.

Syntax

{{ product.name }}              output a value
{% if product.image_url %}      logic, outputs nothing itself

Dot for properties, brackets for array items, pipe for filters chained left to right. Variable names are case-sensitive.

{{ allProducts[0].title }}
{{ blogPost.published_by_author_on | date: "%B %d, %Y" | upcase }}

Funnel pages

Available on checkout, upsell, downsell and store pages.

Variable Type Description
allProducts array Every product on the current page, in display order
currentProduct object The single product a container is bound to. Exists only inside a container with a Product binding
Page object The current page’s metadata
Title text The page’s SEO title
Component object Inside a shared component: that component’s name and id

Page fields

Field Type Description
title text SEO title of the page
name text Internal step name from your funnel
id number Page id
url text Full page URL
path text Path portion of the URL, without the domain
seoTitle text SEO title, same as title
seoDescription text Meta description

Product fields (each item in allProducts, and currentProduct)

Field Type Description
id number Unique product id. Use it to target one specific product
name text Internal product name from product settings
title text Public-facing title. Empty when you haven’t set one, so fall back to name
price number The amount actually charged, unformatted
display_price text The price shown to customers when you’ve set one, for example a formatted or promotional price. Empty when unset
image_url text Product image URL
is_ob boolean true when the product is configured as an order bump
is_physical boolean true when the product requires shipping
is_subscription boolean true when the product bills on a recurring cycle
billing_amount number The recurring charge, for subscription products
billing_cycle number How many units between charges, for example 3
cycle_type number The unit for billing_cycle: 1 days, 2 weeks, 3 months, 4 years
billing_recurring_times number How many times it charges. Empty means until cancelled
has_trial boolean true when the subscription starts with a trial
trial_billing_amount number Amount charged for the trial period
trial_billing_cycle number Length of the trial, in trial_cycle_type units
trial_cycle_type number Unit for the trial length, same mapping as cycle_type
order_index number The product’s position in the funnel’s product list
variants object Variant data for products with variants
options array Variant option sets, for example size and color
step_id number Id of the step the product belongs to
funnel_id number Id of the funnel the product belongs to
created_at date When the product was created
updated_at date When the product was last modified

Blog pages

Variable Type Description
blogPost object The post being viewed. Only on post template pages
allBlogPosts array Every published post on the blog, newest first, up to 200
latestBlogPosts array The most recent published posts
currentCategoryPosts array Posts in the category being viewed. Only on category pages
allBlogCategories array Every category on the blog
blogPage object The current blog page’s metadata
category object The category being viewed. Only on category pages
Title / title text The page’s SEO title
seoDescription text The page’s meta description

blogPost fields (post items in the collections carry the same fields, minus the body)

Field Type Description
name text Post title
title text Alias of name
html text The full post body as HTML
content text Alias of html
excerpt text Short summary of the post
author_name text Display name of the author
author text Alias of author_name
published_by_author_on date Publication date
updated_by_author_at date Last edit date. Empty when never edited, which makes it a clean condition for an “Updated” badge
reading_time_minutes number Estimated reading time
word_count number Number of words in the post
thumbnail_url text Featured image URL
url text Full post URL, including https://
seo_title text SEO title, may differ from the post title
seo_description text Meta description
categories array Category ids the post belongs to
category_names array Category names, ready to display
post_categories array The full category records linked to the post

blogPage fields

Field Type Description
title text SEO title of the page
name text Internal page name
url text Full page URL
seoDescription text Meta description

category fields

Field Type Description
name text Category name as displayed
slug text URL-safe category name
category_id text Unique category id

Customer portal pages

Variable Type Description
allProducts array Products available in the portal, for upsells and add-ons
customerPortalPage object The current portal page’s metadata: title, name, url, seoDescription
Title / title text The page’s SEO title

Live subscription data is client-side. See Part 2.

Portal product fields

Field Type Description
product_id text Unique product id
name text Internal product name
title text Public-facing title
description text Product description
price number The amount charged
display_price text The price shown to customers when set
image_url text Product image URL
is_ob boolean true when configured as an order bump
product_variant_options array The product’s variants

Variant fields (each item in product_variant_options)

Field Type Description
id text Unique variant id
title text Variant name, for example “Large / Blue”
option1, option2, option3 text The individual option values making up the variant
price number Variant price
image_url text Variant image URL
is_sold_out boolean true when the variant is unavailable

Filters

Text

Filter Description Example
default Value to use when the input is empty {{ blogPost.excerpt | default: "Read more" }}
upcase / downcase Convert to upper or lower case {{ category.name | upcase }}
capitalize Capitalize the first letter {{ category.slug | capitalize }}
truncate Cut to a character count, adding “…” {{ blogPost.excerpt | truncate: 120 }}
truncatewords Cut to a word count {{ blogPost.excerpt | truncatewords: 20 }}
append / prepend Add text after or before {{ product.price | prepend: "$" }}
replace / replace_first Swap text, all occurrences or the first {{ product.name | replace: "Pack", "Bundle" }}
remove / remove_first Delete text, all occurrences or the first {{ blogPost.url | remove_first: "https://" }}
strip / lstrip / rstrip Trim whitespace, both ends or one {{ product.name | strip }}
strip_html Remove all HTML tags {{ blogPost.html | strip_html | truncate: 200 }}
strip_newlines Remove line breaks {{ blogPost.excerpt | strip_newlines }}
newline_to_br Convert line breaks to <br> {{ blogPost.excerpt | newline_to_br }}
escape / escape_once HTML-escape the value {{ product.name | escape }}
url_encode / url_decode Make text URL-safe, or decode it {{ blogPost.name | url_encode }}
slice Take part of a string or array {{ category.slug | slice: 0, 3 }}
split Break text into an array on a separator {{ product.title | split: " - " }}
size Length of a string or array {{ allProducts | size }}

Numbers

Filter Description Example
plus / minus Add or subtract {{ product.price | plus: 5 }}
times / divided_by Multiply or divide {{ product.price | times: 2 }}
round Round, optionally to decimals {{ product.price | times: 0.8 | round: 2 }}
ceil / floor Round up or down to a whole number {{ product.price | ceil }}
abs Absolute value {{ product.price | abs }}
modulo Remainder of a division {{ forloop.index | modulo: 2 }}

Dates

date formats a date using strftime codes:

{{ blogPost.published_by_author_on | date: "%B %d, %Y" }}   March 18, 2026
{{ blogPost.published_by_author_on | date: "%d/%m/%Y" }}    18/03/2026
{{ blogPost.updated_by_author_at | date: "%b %e" }}         Mar 18
Code Output
%Y / %y Four-digit year / two-digit year
%B / %b Full month name / short month name
%m Month number, zero-padded
%d / %e Day, zero-padded / space-padded
%A Weekday name
%H:%M Hours and minutes

With no format given, dates render as Wed, Mar 18, 26. On blog and portal pages, a value that isn’t a recognizable date passes through unchanged instead of breaking the page.

Arrays

Filter Description Example
where Keep only items whose field equals a value {{ allProducts | where: "is_ob", true }}
where_exp Keep only items matching an expression {{ allProducts | where_exp: "p", "p.price > 50" }}
map Extract one field from every item {{ allProducts | map: "name" | join: ", " }}
first / last The first or last item {{ allBlogPosts | first }}
join Combine items into one string {{ blogPost.category_names | join: ", " }}
sort / sort_natural Sort by a field, sort_natural ignores case {{ allProducts | sort: "price" }}
reverse Reverse the order {{ allBlogPosts | reverse }}
uniq Remove duplicate items {{ blogPost.categories | uniq }}
compact Remove empty items {{ allProducts | compact }}
concat Join two arrays into one {{ latestBlogPosts | concat: currentCategoryPosts }}
slice Take a range of items {{ allBlogPosts | slice: 0, 3 }}

where and where_exp are Funnelish additions to the standard filter set, and they’re what make product and post lists genuinely programmable.

Debugging

Filter Description
json Output the whole value as JSON. The fastest way to see what a page actually holds
inspect Output a readable dump of the value
type Output the value’s data type
{{ allProducts | json }}

Remove these before publishing.

Tags

Tag Description
if / elsif / else Render a block only when a condition is true
unless Render a block when a condition is false
case / when Choose one block from several based on a value
for Loop over an array
break / continue Exit a loop, or skip to the next item
cycle Alternate between values on each pass of a loop
assign Store a value in a variable
capture Store a whole block of output in a variable
comment A note that never renders
raw Output Liquid syntax literally, without evaluating it
tablerow Generate HTML table rows from an array

Conditions

{% if product.display_price %}
  <span class="sale">{{ product.display_price }}</span> <s>${{ product.price }}</s>
{% else %}
  ${{ product.price }}
{% endif %}

Loops, with forloop exposing index (from 1), index0, first, last and length:

{% for post in allBlogPosts limit: 3 %}
  <h3>{{ post.name }}</h3>
  <p>{{ post.excerpt | truncate: 100 }}</p>
{% endfor %}

Variables

{% assign discounted = product.price | times: 0.75 | round: 2 %}
Today only ${{ discounted }} instead of ${{ product.price }}

:bulb: For anything visual, bind a Repeater to the array instead of writing a loop. You get real, styleable elements rather than raw HTML.

Liquid examples

Subscription terms, only on subscription products

{% if product.is_subscription %}
  Every {{ product.billing_cycle }} months · ${{ product.billing_amount }}
{% endif %}

Free trial line

{% if product.has_trial %}
  Try it for ${{ product.trial_billing_amount }}, then ${{ product.billing_amount }}
{% endif %}

A custom product list with order bumps excluded

{% assign mains = allProducts | where: "is_ob", false %}
{% for p in mains %}
  <div>{{ p.title | default: p.name }} — {{ p.display_price | default: p.price | prepend: "$" }}</div>
{% endfor %}

Just the premium tier

{% assign premium = allProducts | where_exp: "p", "p.price > 100" | first %}
{{ premium.title }}

Blog post meta line

By {{ blogPost.author_name }} · {{ blogPost.published_by_author_on | date: "%B %d, %Y" }} · {{ blogPost.reading_time_minutes }} min read

Excerpt with the post body as a fallback

{{ blogPost.excerpt | default: blogPost.html | strip_html | truncate: 160 }}

Part 2: Client-side data

Some data can’t exist when the page is rendered, because it belongs to the individual visitor: the name they’re typing, the product they just selected, their subscription, the UTM tags on the link they clicked. That data lives in the browser and is read with x-attributes on elements.

:warning: x-attributes are not Liquid. Writing {{ customer.first_name }} outputs those characters literally. Use x-text and its siblings instead.

Customer data

$store.session.customer holds every form field the visitor has filled, on this page or an earlier step. Values persist in the browser and are restored as they move through the funnel, which is how an upsell page greets someone by the name they typed at checkout.

Field Description
email Email address, captured as soon as they type it when auto-optin is on
first_name / last_name Name parts, as entered
phone Phone number
address, city, state, zip, country Billing address parts
shipping_address, shipping_city, shipping_state, shipping_zip, shipping_country Shipping address parts, when your form collects them separately
meta.<name> Any custom field you collect, keyed by the field’s name

:warning: Custom fields are stored flat. A custom field named company is $store.session.customer['meta.company'], not customer.meta.company.

Order data

$store.session.order tracks what the visitor is buying, live.

Field Type Description
selected array Ids of the products currently selected on the order form. Updates as they change their choice
purchased array Ids of the products they actually bought. Populated on thank you and upsell pages
total number The running order total on the order form, the placed-order total afterwards

Subscription data (customer portal)

$store.cp holds the signed-in customer’s portal data.

Field Type Description
subscriptions array All of the customer’s subscriptions
orders array Their order history
currentSubscription object The subscription currently open
currentId text Id of the open subscription

currentSubscription fields

Field Type Description
status text active, trial, paused or past_due
next_billing date When the next charge is due
billing_cycle number How many units between charges
cycle_type number Unit for the cycle: 1 days, 2 weeks, 3 months, 4 years
gateway_id number Payment gateway handling it: 1 Stripe, 7 PayPal
items array The products in the subscription
items_count number How many items it contains
customer object The subscriber’s details
credit_card object The payment method on file

Reading it in custom HTML

Attribute Description
x-text Output a value as text
x-html Output a value as HTML
x-show Show the element only when the expression is true
<span x-text="$store.session.customer.first_name"></span>
<span x-text="$store.session.customer.first_name || 'there'"></span>
<div x-show="$store.session.order.selected.includes('12345')">
  Great choice, the 3-pack ships free.
</div>
<span x-text="$store.session.customer['meta.company']"></span>

These update live: as the visitor types their name, anything bound to it re-renders immediately.

Conditions, without writing code

You rarely need to hand-write the above. The builder can show, hide or restyle an element from the same live data using conditions on element states, and it writes the expression for you. Add a state to an element, give it a condition, and the state turns itself on whenever the condition holds.

Source Fields Where
Order Selected product, purchased product, order total Funnel pages
URL parameter Any query parameter, by name Everywhere
UTM tags utm_source, utm_medium, utm_campaign, utm_term, utm_content Everywhere
Customer metadata Any custom field you collect Everywhere
Current subscription Status, gateway, next billing date, billing cycle, cycle type, items count Portal pages
Subscription Status, name, next billing date, items count, whether this card is the selected one Inside a subscriptions list
Subscription item Name, status, quantity Inside a subscription’s items
Customer Shipping country Portal pages

Operators: is, is not, contains, does not contain, includes, does not include, greater than, greater than or equal, less than, less than or equal, is blank, is not blank.

What people build with it:

  • A bonus banner that appears only when a specific product is selected.

  • Different copy for visitors arriving from a particular utm_source.

  • An upgrade CTA shown only when the order total is below a threshold.

  • A “Resume subscription” button that appears only when the status is paused.

Conditions re-evaluate as the data changes, so a banner tied to a product selection appears and disappears as the visitor changes their mind.

Custom JavaScript

For anything further, the page exposes a global funnelish object.

Read the current session

const { customer, order } = funnelish.session()
console.log(customer.email, order.total)

Hook into lifecycle events with funnelish.on(event, handler)

Event Fires when
load The page runtime has finished loading
optin An optin form is submitted
of_purchase A purchase completes on the order form
oto_yes An upsell offer is accepted
oto_no An upsell offer is declined
next_step The visitor moves to the next step
summary_loaded The thank you page order summary has loaded
discount_applied A discount code is successfully applied
app_loaded An app finished loading. Receives the app’s name and id
request_login_otp A portal login code is requested
submit_login A portal login is submitted
funnelish.on('of_purchase', () => {
  // fire your own tracking, show a message, whatever you need
})

Raw page data is also on the window: PRODUCTS, FUNNEL, STEP, PAGE, CUSTOMER_PORTAL.


When nothing renders

Server-side, in order of likelihood:

  1. Wrong page type. blogPost doesn’t exist on a checkout page, allProducts doesn’t exist on a blog post. Check the tables above.

  2. Empty value. The field exists but has no content. Add a default.

  3. Wrong case or name. allProducts, not allproducts. Page, not page.

  4. Syntax error. A missing %} or endfor fails the whole expression. If a section renders as visible Liquid code, that’s the giveaway.

Drop {{ allProducts | json }} into a paragraph to see exactly what the page holds.

Client-side:

  1. You used {{ }} instead of x-text. By far the most common mistake.

  2. The field is still empty. Customer fields fill in as the visitor types, so there’s nothing there on the first step.

  3. Wrong key for a custom field. It’s customer['meta.company'], not customer.meta.company.

Open the browser console and run funnelish.session() to see what’s available right now.

Related articles


Written an expression that won’t cooperate? Paste it in Ask the community with your page type, someone will spot it.