Skip to content

Module 5: Standard Filters — Numbers

Almost every Cast narration involves at least one number: a health score, an ARR figure, a license count, a usage percentage, a days-until-renewal calculation. And as you learned in Module 2, these values arrive from your CRM as strings — text that happens to contain digits.

Number filters serve two purposes:

  1. Converting strings to real numbers so comparisons and math work correctly
  2. Calculating — adding, subtracting, multiplying, dividing, rounding — to produce the metrics your narration needs

The Critical Lesson — String-to-Number Conversion

Section titled “The Critical Lesson — String-to-Number Conversion”

Before we cover any individual filter, let’s revisit why conversion matters with a concrete example.

Suppose arr arrives from Salesforce with the value "95000" (a string). You write:

{% raw %}
High-value account.
{% endraw %}

This looks correct but is unreliable. Here’s why: Liquid sees a string on the left ("95000") and a number on the right (50000). When types don’t match, the comparison may fall back to string (alphabetical) rules. Under alphabetical comparison, "9" comes after "5", so "95000" > "50000" happens to be true. But "6" > "50000" would also be true alphabetically — because "6" comes after "5" in character order. That’s clearly wrong numerically.

Excel analogy: This is exactly what happens if you try =IF(A1>50000, ...) in Excel where cell A1 is formatted as Text. Excel may silently treat it as a string comparison instead of a numeric one, giving you wrong results. The fix in Excel is to change the cell format or wrap it in VALUE(). The fix in Liquid is | plus: 0 or | times: 1.

The two conversion methods:

{% raw %}
{% assign arr_numeric = arr | plus: 0 %} {%- # adds 0 → forces to number -%}
{% assign arr_numeric = arr | times: 1 %} {%- # multiplies by 1 → forces to number -%}
{% endraw %}

Both produce the same result for integers. The difference matters for decimals:

{% raw %}
{% assign rate = "3.75" | plus: 0 %} → 3.75 (preserves decimal)
{% assign rate = "3.75" | times: 1 %} → 3 (integer multiplication — truncates!)
{% assign rate = "3.75" | times: 1.0 %} → 3.75 (decimal multiplication — preserves)
{% endraw %}

💡 Rule of thumb:

  • Use | plus: 0 for most conversions — it handles both integers and decimals
  • Use | times: 1.0 when you want to be explicit about preserving decimals
  • Use | times: 1 only when you intentionally want integer truncation

Always pair with default to handle missing fields:

{% raw %}
{% assign score = health_score | default: 0 | plus: 0 %}
{% assign rate = growth_rate | default: 0 | times: 1.0 %}
{% endraw %}

Filter What It Does Example
plus: n Add (also converts string → number) "95000" | plus: 095000
minus: n Subtract 100 | minus: 2575
times: n Multiply (also converts string → number) "5" | times: 15
divided_by: n Divide 100 | divided_by: 425
modulo: n Remainder after division 10 | modulo: 31
round: n Round to n decimal places 4.678 | round: 24.68
floor Round down to nearest integer 4.9 | floor4
ceil Round up to nearest integer 4.1 | ceil5
abs Remove the negative sign -5 | abs5

Now let’s explore each one.


plus — Addition and String-to-Number Conversion

Section titled “plus — Addition and String-to-Number Conversion”

The plus filter adds a number to the value. When applied to a string, it converts the string to a number first and then adds. This dual behavior makes plus: 0 the most common way to convert strings to numbers — adding zero changes nothing mathematically but forces the type conversion.

Excel analogy: plus: 0 is like =A1 + 0 or =VALUE(A1) in Excel — a trick to force a text-formatted cell into a real number.

plus is both your primary addition tool and your primary conversion tool. You’ll use | plus: 0 at the top of nearly every narration that involves numeric data.

{% raw %}
{{ 10 | plus: 5 }} → 15
{{ "95000" | plus: 0 }} → 95000 (string converted to number)
{{ "95000" | plus: 5000 }} → 100000 (converted AND added)
{% endraw %}
{% raw %}
{%- assign score = health_score | default: 0 | plus: 0 -%}
{%- assign bonus = 5 -%}
{%- assign adjusted = score | plus: bonus -%}
Your health score is {{ score }}. With this quarter's engagement bonus,
your adjusted score is {{ adjusted }}.
{% endraw %}

If health_score is "78": Output: Your health score is 78. With this quarter's engagement bonus, your adjusted score is 83.

Adding two unconverted strings:

{% raw %}
{% assign total = arr | plus: upsell_value %}
{% endraw %}

If both are strings, the behavior is unpredictable.

Convert both first, then add:

{% raw %}
{%- assign arr_val = arr | default: 0 | plus: 0 -%}
{%- assign upsell = upsell_value | default: 0 | plus: 0 -%}
{%- assign total = arr_val | plus: upsell -%}
{% endraw %}

The minus filter subtracts a number from the value. Like plus, it converts strings to numbers implicitly, but explicit conversion is always safer.

Excel analogy: =A1 - B1

Use minus for calculating differences — how much a metric changed, how many days until renewal, how much capacity remains.

{% raw %}
{{ 100 | minus: 25 }} → 75
{{ "200" | minus: 50 }} → 150
{% endraw %}
{% raw %}
{%- assign reserved = LicenseReserved | default: 0 | plus: 0 -%}
{%- assign used = LicenseUsed | default: 0 | plus: 0 -%}
{%- assign remaining = reserved | minus: used -%}
You have {{ remaining }} licenses remaining out of {{ reserved }} reserved.
{% endraw %}

If LicenseReserved is "100" and LicenseUsed is "73": Output: You have 27 licenses remaining out of 100 reserved.

Getting the subtraction order backwards:

{% raw %}
{%- assign change = previous | minus: current -%}
{% endraw %}

If you want how much something increased, subtract the old from the new, not the other way around.

New minus old for increase:

{% raw %}
{%- assign increase = current | minus: previous -%}
{% endraw %}

The times filter multiplies the value by a number. Like plus, it also converts strings to numbers, making | times: 1 another conversion option.

Excel analogy: =A1 * B1

Use times for percentage calculations, scaling values, and converting between units. The times: 1.0 pattern is critical when working with decimal values like growth rates or utilization percentages.

{% raw %}
{{ 10 | times: 5 }} → 50
{{ "5" | times: 1 }} → 5 (integer conversion)
{{ "3.75" | times: 1.0 }} → 3.75 (decimal-preserving conversion)
{{ 0.75 | times: 100 }} → 75 (decimal to percentage)
{% endraw %}
{% raw %}
{%- assign used = LicenseUsed | default: 0 | times: 1.0 -%}
{%- assign reserved = LicenseReserved | default: 1 | times: 1.0 -%}
{%- assign pct = used | divided_by: reserved | times: 100 | round: 0 -%}
Your license utilization is {{ pct }}%.
{% endraw %}

If LicenseUsed is "45" and LicenseReserved is "60":

  • used = 45.0, reserved = 60.0
  • 45.0 / 60.0 = 0.75
  • 0.75 * 100 = 75.0
  • round: 0 → 75

Output: Your license utilization is 75%.

Using times: 1 with a decimal string:

{% raw %}
{% assign rate = "3.75" | times: 1 %} → 3 (decimal truncated!)
{% endraw %}

Use times: 1.0 to preserve decimals:

{% raw %}
{% assign rate = "3.75" | times: 1.0 %} → 3.75
{% endraw %}

Forgetting to convert strings before multiplying two variables:

{% raw %}
{%- assign result = price | times: quantity -%}
{% endraw %}

If both are strings, this may produce wrong results.

Convert first:

{% raw %}
{%- assign p = price | default: 0 | times: 1.0 -%}
{%- assign q = quantity | default: 0 | plus: 0 -%}
{%- assign result = p | times: q -%}
{% endraw %}

The divided_by filter divides the value by a number. The critical subtlety: integer division drops the decimal part unless you use a decimal divisor.

Excel analogy: =A1 / B1 — except Excel always gives you a decimal result, while Liquid only does if at least one side is a decimal.

Division is essential for percentages, averages, per-unit calculations, and benchmarking. The integer-vs-decimal distinction is one of the most common gotchas in Cast arithmetic.

{% raw %}
{{ 100 | divided_by: 4 }} → 25
{{ 100 | divided_by: 3 }} → 33 (integer division — remainder dropped)
{{ 100 | divided_by: 3.0 }} → 33.333... (decimal division — fraction preserved)
{{ 10 | divided_by: 4.0 }} → 2.5
{% endraw %}

The rule is simple: if the divisor is a whole number (3), you get integer division. If the divisor has a decimal point (3.0), you get decimal division.

Calculating a percentage correctly:

{% raw %}
{%- assign current = analysis_current_qtr_avg | default: 0 | times: 1.0 -%}
{%- assign previous = analysis_prev_qtr_avg | default: 0 | times: 1.0 -%}
{%- if previous > 0 -%}
{%- assign change_pct = current | minus: previous | divided_by: previous | times: 100 | round: 1 -%}
{%- if change_pct > 0 -%}
Usage increased {{ change_pct }}% compared to last quarter.
{%- elsif change_pct < 0 -%}
Usage decreased {{ change_pct | abs }}% compared to last quarter.
{%- else -%}
Usage remained the same as last quarter.
{%- endif -%}
{%- endif -%}
{% endraw %}

Notice | times: 1.0 on the initial conversions — this ensures that when we later divide current by previous, we get decimal division, not integer division.

Getting zero from a percentage calculation because of integer division:

{% raw %}
{%- assign used = 45 -%}
{%- assign total = 60 -%}
{%- assign pct = used | divided_by: total | times: 100 -%}
{{ pct }} → 0 (because 45 / 60 = 0 in integer division, then 0 * 100 = 0)
{% endraw %}

Ensure decimal division:

{% raw %}
{%- assign used = 45 -%}
{%- assign total = 60.0 -%}
{%- assign pct = used | divided_by: total | times: 100 | round: 0 -%}
{{ pct }} → 75
{% endraw %}

Or convert both to decimals earlier with | times: 1.0.


Dividing by zero:

{% raw %}
{%- assign pct = used | divided_by: reserved -%}
{% endraw %}

If reserved is 0, this causes an error.

Always guard against division by zero:

{% raw %}
{%- assign reserved = LicenseReserved | default: 0 | times: 1.0 -%}
{%- assign pct = used | divided_by: reserved | times: 100 | round: 0 -%}
Your utilization is {{ pct }}%.
{% else %}
No reserved capacity on file.
{% endraw %}

The modulo filter returns the remainder when dividing by a number. If you divide 10 by 3, the answer is 3 with a remainder of 1 — modulo gives you that 1.

Excel analogy: Identical to =MOD(A1, 3).

modulo is used in several Cast snippet patterns: the Avatar Name Generator uses it to pick a name from a list deterministically, and the Customer Since snippet uses it to calculate remaining days after dividing by weeks or months.

{% raw %}
{{ 10 | modulo: 3 }} → 1 (10 ÷ 3 = 3 remainder 1)
{{ 12 | modulo: 4 }} → 0 (12 ÷ 4 = 3 remainder 0)
{{ 7 | modulo: 2 }} → 1 (odd number — remainder 1 when divided by 2)
{% endraw %}

Picking a name from a list based on contact ID (from the Avatar Name Generator):

{% raw %}
{%- assign names = "Amy,Bella,Chloe,Diana,Emily" | split: "," -%}
{%- assign contactId = contact_id | default: 1 | plus: 0 -%}
{%- assign nameIndex = contactId | modulo: names.size -%}
{{ names[nameIndex] }}
{% endraw %}

If contact_id is "7" and there are 5 names:

  • 7 modulo 5 = 2
  • names[2] = "Chloe"

The contact always gets the same name (deterministic), and it wraps around no matter how large the contact ID is.

Confusing modulo with divided_by:

{% raw %}
{{ 10 | modulo: 3 }} → 1 (the remainder)
{{ 10 | divided_by: 3 }} → 3 (the quotient)
{% endraw %}

They’re complementary — together they fully describe a division: 10 ÷ 3 = 3 remainder 1.


The round filter rounds a number to a specified number of decimal places. Without an argument, it rounds to the nearest whole number.

Excel analogy: Identical to =ROUND(A1, 2).

Percentages, averages, and financial calculations often produce long decimal results like 73.333333. Rounding presents clean, professional numbers in your narration.

{% raw %}
{{ 4.678 | round: 2 }} → 4.68
{{ 4.678 | round: 1 }} → 4.7
{{ 4.678 | round: 0 }} → 5
{{ 4.678 | round }} → 5 (default: 0 decimal places)
{{ 4.5 | round }} → 5 (rounds up at .5)
{% endraw %}
{% raw %}
{%- assign current = analysis_current_qtr_avg | default: 0 | times: 1.0 -%}
{%- assign previous = analysis_prev_qtr_avg | default: 1 | times: 1.0 -%}
{%- assign change = current | minus: previous | divided_by: previous | times: 100 | round: 1 -%}
Your usage changed by {{ change }}% compared to last quarter.
{% endraw %}

Output: Your usage changed by 12.5% compared to last quarter. (not 12.4999999)

Rounding before doing all your math:

{% raw %}
{%- assign ratio = used | divided_by: total | round: 2 -%}
{%- assign pct = ratio | times: 100 -%}
{{ pct }} → might produce 75.0 or imprecise results due to early rounding
{% endraw %}

Do all math first, round last:

{% raw %}
{%- assign pct = used | divided_by: total | times: 100 | round: 1 -%}
{{ pct }}
{% endraw %}

The floor filter always rounds down to the nearest whole number, regardless of the decimal part. 4.1 and 4.9 both become 4.

Excel analogy: Identical to =FLOOR(A1, 1) or =INT(A1).

Use floor when you need a conservative (lower) estimate — for example, calculating whole months remaining on a contract or complete units consumed.

{% raw %}
{{ 4.1 | floor }} → 4
{{ 4.9 | floor }} → 4
{{ 5.0 | floor }} → 5
{{ -4.1 | floor }} → -5 (rounds toward negative infinity)
{% endraw %}
{% raw %}
{%- assign days = diffDays | default: 0 | plus: 0 -%}
{%- assign whole_months = days | divided_by: 30.0 | floor -%}
You have at least {{ whole_months }} full months until renewal.
{% endraw %}

If there are 95 days remaining: 95 / 30.0 = 3.166…, floor → 3. Output: You have at least 3 full months until renewal.


The ceil filter always rounds up to the nearest whole number, regardless of the decimal part. 4.1 and 4.01 both become 5.

Excel analogy: Identical to =CEILING(A1, 1) or =ROUNDUP(A1, 0).

Use ceil when you need a generous (upper) estimate — for example, calculating how many licenses a team needs (you can’t buy half a license) or presenting a percentage change where rounding down would understate the situation.

{% raw %}
{{ 4.1 | ceil }} → 5
{{ 4.9 | ceil }} → 5
{{ 5.0 | ceil }} → 5
{{ -4.9 | ceil }} → -4 (rounds toward zero)
{% endraw %}

From the benchmarking pattern in the prompt:

{% raw %}
{%- assign current = analysis_current_qtr_avg | default: 0 | times: 1.0 -%}
{%- assign previous = analysis_prev_qtr_avg | default: 0 | times: 1.0 -%}
{%- if current > previous -%}
{%- assign pct_increase = current | minus: previous | divided_by: previous | times: 100 | ceil -%}
Usage increased by at least {{ pct_increase }}% from the previous quarter.
{%- endif -%}
{% endraw %}

Using ceil here ensures the stated percentage is never lower than the actual value — a safe choice when highlighting positive change.


The abs filter removes the negative sign from a number, returning its absolute (positive) value. Positive numbers and zero are unaffected.

Excel analogy: Identical to =ABS(A1).

When you calculate a difference (current minus previous), the result may be negative. If your narration says “usage decreased by X%”, you want to show 15, not -15. abs strips the negative sign so you can word the sentence naturally.

{% raw %}
{{ -5 | abs }} → 5
{{ 5 | abs }} → 5
{{ -3.7 | abs }} → 3.7
{% endraw %}
{% raw %}
{%- assign current = analysis_current_qtr_avg | default: 0 | times: 1.0 -%}
{%- assign previous = analysis_prev_qtr_avg | default: 0 | times: 1.0 -%}
{%- if previous > 0 -%}
{%- if current < previous -%}
{%- assign decrease = previous | minus: current | divided_by: previous | times: 100 | ceil | abs -%}
Your average monthly usage decreased from the previous quarter by {{ decrease }}%.
{%- endif -%}
{%- endif -%}
{% endraw %}

Without abs, the narration might say “decreased by -15%” which reads awkwardly. With abs, it cleanly says “decreased by 15%.”

Forgetting abs when the subtraction order might produce a negative:

{% raw %}
{%- assign diff = current | minus: previous -%}
Usage changed by {{ diff }}%. → "Usage changed by -15%."
{% endraw %}

Use abs and handle the sign in the wording:

{% raw %}
{%- assign diff = current | minus: previous -%}
{%- if diff >= 0 -%}
Usage increased by {{ diff }}%.
{%- else -%}
Usage decreased by {{ diff | abs }}%.
{%- endif -%}
{% endraw %}

Putting It All Together — The Complete Numeric Pipeline

Section titled “Putting It All Together — The Complete Numeric Pipeline”

Here’s the pattern you’ll follow for virtually every numeric calculation in Cast:

{% raw %}
Step 1: Default → handle nil/empty fields
Step 2: Convert → string to number (plus: 0 or times: 1.0)
Step 3: Calculate → arithmetic filters (plus, minus, times, divided_by)
Step 4: Format → round, floor, ceil, abs
Step 5: Display → output with context
{% endraw %}

Complete example — license utilization with benchmarking:

{% raw %}
{% comment %} Step 1 & 2: Safe defaults and conversion {% endcomment %}
{%- assign used = LicenseUsed | default: 0 | times: 1.0 -%}
{%- assign reserved = LicenseReserved | default: 0 | times: 1.0 -%}
{%- assign benchmark = avgUsage | default: 0 | times: 1.0 -%}
{% comment %} Step 3 & 4: Calculate and format {% endcomment %}
{%- if reserved > 0 -%}
{%- assign pct = used | divided_by: reserved | times: 100 | round: 0 -%}
{%- else -%}
{%- assign pct = 0 -%}
{%- endif -%}
{% comment %} Step 5: Display with context {% endcomment %}
You used {{ used | round: 0 }} of {{ reserved | round: 0 }} reserved licenses
({{ pct }}% utilization).
{%- if benchmark > 0 and reserved > 0 -%}
{%- assign diff = pct | minus: benchmark | round: 0 -%}
{%- if diff > 0 %}
That's {{ diff }}% above the industry average.
{%- elsif diff < 0 %}
That's {{ diff | abs }}% below the industry average.
{%- else %}
That's right at the industry average.
{%- endif -%}
{%- endif -%}
{% endraw %}

Try It Yourself — Module 5 Capstone Exercise

Section titled “Try It Yourself — Module 5 Capstone Exercise”

Exercise: You have three CRM fields:

  • arr = "320000" (string)
  • growth_rate = "0.15" (string, represents 15%)
  • previous_arr = "280000" (string)

Write Liquid that:

  1. Converts all three to numbers (preserving the decimal in growth_rate)
  2. Calculates the projected ARR: arr * (1 + growth_rate)
  3. Calculates the year-over-year change percentage: (arr - previous_arr) / previous_arr * 100
  4. Displays both results, with the projected ARR rounded to the nearest whole number and the change percentage rounded to one decimal place
Click to reveal the answer
{% raw %}
{%- comment -%} Convert strings to numbers {%- endcomment -%}
{%- assign arr_val = arr | default: 0 | plus: 0 -%}
{%- assign growth = growth_rate | default: 0 | times: 1.0 -%}
{%- assign prev_arr_val = previous_arr | default: 0 | plus: 0 -%}
{%- comment -%} Projected ARR: arr * (1 + growth_rate) {%- endcomment -%}
{%- assign multiplier = 1.0 | plus: growth -%}
{%- assign projected = arr_val | times: multiplier | round: 0 -%}
{%- comment -%} YoY change: (current - previous) / previous * 100 {%- endcomment -%}
{%- if prev_arr_val > 0 -%}
{%- assign yoy_change = arr_val | minus: prev_arr_val | divided_by: prev_arr_val | times: 100.0 | round: 1 -%}
{%- else -%}
{%- assign yoy_change = 0 -%}
{%- endif -%}
Your current ARR of ${{ arr_val }} represents a {{ yoy_change }}% increase year over year.
At your current growth rate of {{ growth | times: 100 | round: 0 }}%, your projected ARR
next year is ${{ projected }}.
{% endraw %}

Output:

{% raw %}
Your current ARR of $320000 represents a 14.3% increase year over year.
At your current growth rate of 15%, your projected ARR next year is $368000.
{% endraw %}

Key details:

  • times: 1.0 preserves the decimal in growth_rate
  • divided_by: prev_arr_val works as decimal division because we converted with times: 1.0 where needed {% raw %}- Division by zero is guarded with ``{% endraw %}
  • Rounding happens at the very end, after all math is done

You can now handle any number that comes your way in Cast. In Module 6, you’ll learn the list and array filters — sorting, deduplicating, joining, and extracting items from arrays to build dynamic content from multi-value data.


📖 Official documentation: