Expressions

Expressions

Liquid is good at showing values and bad at working with them. Dividing watts by a thousand, choosing a word from a temperature or finding out how long ago something happened takes a long chain of filters. Expressions do it in one line, with the same small language that handlers use.

You can write an expression in three places:

  • {% let name = expression %} stores the result under a name, the way assign does.
  • {{= expression }} writes the result into the page.
  • {% if expression %} uses it as a condition, and so do elsif and unless.

Calculating

{% let kw = cap.measure_power.value / 1000 %}<p>{{= round(kw, 1) }} kW</p>

let takes a name, an equals sign and an expression. The name can be used in the rest of the template, in Liquid and in other expressions. A let works exactly as far as assign does: a for loop doesn’t hide it, and a partial doesn’t see it.

{{= }} writes what the expression gives. Liquid filters still work after it, so these are the same:

{{= round(kw, 1) }}{{= kw | round: 1 }}

Output is escaped like every other output.

Operators

OperatorsWhat they do
+ - * / %Arithmetic. + joins text when either side is text.
< <= > >=Compare numbers and times. Nothing is smaller or larger than a missing value.
== !=Equal and not equal. They are strict: 21 == '21' is false.
and or notCombine conditions. &&, || and ! mean the same.
??The left side, or the right one when the left is missing.
condition ? a : bOne of two values.
( )Group parts.

Text is written in single or double quotes, and [1, 2, 3] is a list. and, or and not come in the usual order: not first, then and, then or.

A condition can stack up:

{% let feel = temp < 19 ? 'Cool' : temp > 24 ? 'Warm' : 'Comfortable' %}

Reading values

An expression reads any variable of the template: cap, device, settings, state, now, a name you gave with assign or let, or the loop variable inside a for. Reach into a value with a dot, cap.measure_power.value, or with brackets when the name has odd characters, cap['meter_power.T1-high'].

A value that isn’t there is missing, not an error. Anything you read from it is missing too, and a comparison with a missing value is false. Use ?? to say what to show instead:

{{= cap.measure_power.value ?? 0 }}

An expression can’t change anything and can’t call JavaScript. There are no assignments, no loops and no method calls such as device.name.toUpperCase(). Only the functions below can be called.

In conditions

{% if %} uses Liquid’s own rules whenever Liquid can read the condition by itself, so every template written without expressions keeps working exactly as before. A condition Liquid can’t read is an expression, such as one that calls a function or calculates:

{% if cap.measure_power.value / 1000 > 2.5 %}  <p>Heavy use</p>{% elsif until(settings.pickup, 'days') < 2 %}  <p>Pick-up soon</p>{% endif %}

The two follow different rules for true and false, which matters for the number 0. In Liquid only false and nothing count as false, so {% if 0 %} is true. In an expression, 0, empty text, nothing and false all count as false.

Functions

Expressions can call a short list of functions, grouped by what they work on:

  • Maths: round, floor, ceil, abs, min, max and clamp.
  • Dates: since, until, isBefore, isAfter, isToday and daysBetween.
  • Text and lists: upper, lower, contains, startsWith, length and format.

Each one has a page in the expressions reference with what it takes and gives. They are pure: they read their arguments and give back a value, and never change anything.

Dates

The date functions take an ISO time such as 2026-12-24T18:00:00Z, an ISO date such as 2026-12-24, the template’s now, and a reading’s lastUpdated. A date without a time counts from the start of that day.

{% if since(cap.alarm_motion.lastUpdated, 'minutes') < 5 %}  <p>Motion a moment ago</p>{% endif %}<p>{{= daysBetween(now, settings.holiday) }} days to go</p>

since and until count in 'seconds', 'minutes', 'hours' or 'days', and give fractions: ten minutes is 0.1667 hours. Calendar days, as in isToday and daysBetween, are counted on the screen’s own clock.

A tile that reads the clock this way is drawn again every minute, so the words move on without anything else changing. A tile that doesn’t read the clock has no timer.

Text and numbers

format writes a number with a pattern: zeros are digits that are always written, a comma adds thousands separators, and decimals follow the point.

{{= format(cap.measure_power.value, '#,##0') }} W{{= format(cap.measure_temperature.value, '0.0') }}°

Limits

An expression can be 500 characters long, hold up to 120 parts (a number, a name or an operator each counts as one) and nest 24 levels deep. An expression that fails, for example by calling a function with the wrong number of arguments, is an error when you save. If one fails while a tile is drawn, it writes nothing, and a condition that fails is false.

An example

This tile chooses a word and a colour from the temperature, shows the temperature in Fahrenheit and adds a note when it is warm and humid:

comfort.liquid
{% let temp = cap.measure_temperature.value %}{% let humidity = cap.measure_humidity.value %}{% let tone = temp < 19 ? 'cool' : temp > 24 ? 'warm' : 'ok' %}{% let feel = temp < 19 ? 'Cool' : temp > 24 ? 'Warm' : 'Comfortable' %}<header class="head">{{ device.name }}</header><p class="big">{{= round(temp, 1) }}°</p><p class="feel" data-tone="{{ tone }}">{{ feel }}</p><p class="sub">  {{= round(temp * 9 / 5 + 32) }} °F  {% if humidity > 60 and temp > 22 %}· muggy{% endif %}</p>

The tone variable goes into a data-tone attribute, so the style can colour the word without any more Liquid. The Syntax panel in the editor lists every function, each with an example you can put at the cursor.