What Youβll Learn in This Module
This module covers every number-related filter in Liquid and β most importantly β teaches you the string-to-number conversion patterns that are essential for working with CRM data in Cast. If Module 2 introduced the concept that CRM fields arrive as strings, this module gives you the tools to handle that reality every single time.
By the end of this module, youβll be able to convert strings to numbers, perform arithmetic, round and format results, and avoid the subtle comparison bugs that plague Cast narrations built without proper type conversion.
Why Number Filters Matter in Cast
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:
- Converting strings to real numbers so comparisons and math work correctly
- Calculating β adding, subtracting, multiplying, dividing, rounding β to produce the metrics your narration needs
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:
{% if arr > 50000 %}
High-value account.
{% endif %}
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 inVALUE(). The fix in Liquid is| plus: 0or| times: 1.
The two conversion methods:
{% assign arr_numeric = arr | plus: 0 %} {%- # adds 0 β forces to number -%}
{% assign arr_numeric = arr | times: 1 %} {%- # multiplies by 1 β forces to number -%}
Both produce the same result for integers. The difference matters for decimals:
{% 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)
π‘ Rule of thumb:
- Use
| plus: 0for most conversions β it handles both integers and decimals - Use
| times: 1.0when you want to be explicit about preserving decimals - Use
| times: 1only when you intentionally want integer truncation
Always pair with default to handle missing fields:
{% assign score = health_score | default: 0 | plus: 0 %}
{% assign rate = growth_rate | default: 0 | times: 1.0 %}
Quick Reference β All Number Filters
| Filter | What It Does | Example |
|---|---|---|
plus: n | Add (also converts string β number) | "95000" \| plus: 0 β 95000 |
minus: n | Subtract | 100 \| minus: 25 β 75 |
times: n | Multiply (also converts string β number) | "5" \| times: 1 β 5 |
divided_by: n | Divide | 100 \| divided_by: 4 β 25 |
modulo: n | Remainder after division | 10 \| modulo: 3 β 1 |
round: n | Round to n decimal places | 4.678 \| round: 2 β 4.68 |
floor | Round down to nearest integer | 4.9 \| floor β 4 |
ceil | Round up to nearest integer | 4.1 \| ceil β 5 |
abs | Remove the negative sign | -5 \| abs β 5 |
Now letβs explore each one.
plus β Addition and String-to-Number Conversion
What is it?
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: 0is like=A1 + 0or=VALUE(A1)in Excel β a trick to force a text-formatted cell into a real number.
Why it matters in Cast
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.
Liquid syntax
{{ 10 | plus: 5 }} β 15
{{ "95000" | plus: 0 }} β 95000 (string converted to number)
{{ "95000" | plus: 5000 }} β 100000 (converted AND added)
Real Cast example
{%- 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 }}.
If health_score is "78": Output: Your health score is 78. With this quarter's engagement bonus, your adjusted score is 83.
Common mistakes
β Adding two unconverted strings:
{% assign total = arr | plus: upsell_value %}
If both are strings, the behavior is unpredictable.
β Convert both first, then add:
{%- assign arr_val = arr | default: 0 | plus: 0 -%}
{%- assign upsell = upsell_value | default: 0 | plus: 0 -%}
{%- assign total = arr_val | plus: upsell -%}
minus β Subtraction
What is it?
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
Why it matters in Cast
Use minus for calculating differences β how much a metric changed, how many days until renewal, how much capacity remains.
Liquid syntax
{{ 100 | minus: 25 }} β 75
{{ "200" | minus: 50 }} β 150
Real Cast example
{%- 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.
If LicenseReserved is "100" and LicenseUsed is "73": Output: You have 27 licenses remaining out of 100 reserved.
Common mistakes
β Getting the subtraction order backwards:
{%- assign change = previous | minus: current -%}
If you want how much something increased, subtract the old from the new, not the other way around.
β New minus old for increase:
{%- assign increase = current | minus: previous -%}
times β Multiplication and Conversion
What is it?
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
Why it matters in Cast
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.
Liquid syntax
{{ 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)
Real Cast example
{%- 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 }}%.
If LicenseUsed is "45" and LicenseReserved is "60":
used= 45.0,reserved= 60.045.0 / 60.0= 0.750.75 * 100= 75.0round: 0β 75
Output: Your license utilization is 75%.
Common mistakes
β Using times: 1 with a decimal string:
{% assign rate = "3.75" | times: 1 %} β 3 (decimal truncated!)
β
Use times: 1.0 to preserve decimals:
{% assign rate = "3.75" | times: 1.0 %} β 3.75
β Forgetting to convert strings before multiplying two variables:
{%- assign result = price | times: quantity -%}
If both are strings, this may produce wrong results.
β Convert first:
{%- assign p = price | default: 0 | times: 1.0 -%}
{%- assign q = quantity | default: 0 | plus: 0 -%}
{%- assign result = p | times: q -%}
divided_by β Division
What is it?
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.
Why it matters in Cast
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.
Liquid syntax
{{ 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
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.
Real Cast example
Calculating a percentage correctly:
{%- 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 -%}
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.
Common mistakes
β Getting zero from a percentage calculation because of integer division:
{%- 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)
β Ensure decimal division:
{%- assign used = 45 -%}
{%- assign total = 60.0 -%}
{%- assign pct = used | divided_by: total | times: 100 | round: 0 -%}
{{ pct }} β 75
Or convert both to decimals earlier with | times: 1.0.
β Dividing by zero:
{%- assign pct = used | divided_by: reserved -%}
If reserved is 0, this causes an error.
β Always guard against division by zero:
{%- assign reserved = LicenseReserved | default: 0 | times: 1.0 -%}
{% if reserved > 0 %}
{%- assign pct = used | divided_by: reserved | times: 100 | round: 0 -%}
Your utilization is {{ pct }}%.
{% else %}
No reserved capacity on file.
{% endif %}
modulo β Remainder After Division
What is it?
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).
Why it matters in Cast
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.
Liquid syntax
{{ 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)
Real Cast example
Picking a name from a list based on contact ID (from the Avatar Name Generator):
{%- assign names = "Amy,Bella,Chloe,Diana,Emily" | split: "," -%}
{%- assign contactId = contact_id | default: 1 | plus: 0 -%}
{%- assign nameIndex = contactId | modulo: names.size -%}
{{ names[nameIndex] }}
If contact_id is "7" and there are 5 names:
7 modulo 5= 2names[2]="Chloe"
The contact always gets the same name (deterministic), and it wraps around no matter how large the contact ID is.
Common mistakes
β Confusing modulo with divided_by:
{{ 10 | modulo: 3 }} β 1 (the remainder)
{{ 10 | divided_by: 3 }} β 3 (the quotient)
Theyβre complementary β together they fully describe a division: 10 Γ· 3 = 3 remainder 1.
round β Round to n Decimal Places
What is it?
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).
Why it matters in Cast
Percentages, averages, and financial calculations often produce long decimal results like 73.333333. Rounding presents clean, professional numbers in your narration.
Liquid syntax
{{ 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)
Real Cast example
{%- 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.
Output: Your usage changed by 12.5% compared to last quarter. (not 12.4999999)
Common mistakes
β Rounding before doing all your math:
{%- 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
β Do all math first, round last:
{%- assign pct = used | divided_by: total | times: 100 | round: 1 -%}
{{ pct }}
floor β Round Down to Integer
What is it?
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).
Why it matters in Cast
Use floor when you need a conservative (lower) estimate β for example, calculating whole months remaining on a contract or complete units consumed.
Liquid syntax
{{ 4.1 | floor }} β 4
{{ 4.9 | floor }} β 4
{{ 5.0 | floor }} β 5
{{ -4.1 | floor }} β -5 (rounds toward negative infinity)
Real Cast example
{%- 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.
If there are 95 days remaining: 95 / 30.0 = 3.166β¦, floor β 3. Output: You have at least 3 full months until renewal.
ceil β Round Up to Integer
What is it?
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).
Why it matters in Cast
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.
Liquid syntax
{{ 4.1 | ceil }} β 5
{{ 4.9 | ceil }} β 5
{{ 5.0 | ceil }} β 5
{{ -4.9 | ceil }} β -4 (rounds toward zero)
Real Cast example
From the benchmarking pattern in the prompt:
{%- 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 -%}
Using ceil here ensures the stated percentage is never lower than the actual value β a safe choice when highlighting positive change.
abs β Absolute Value
What is it?
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).
Why it matters in Cast
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.
Liquid syntax
{{ -5 | abs }} β 5
{{ 5 | abs }} β 5
{{ -3.7 | abs }} β 3.7
Real Cast example
{%- 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 -%}
Without abs, the narration might say βdecreased by -15%β which reads awkwardly. With abs, it cleanly says βdecreased by 15%.β
Common mistakes
β Forgetting abs when the subtraction order might produce a negative:
{%- assign diff = current | minus: previous -%}
Usage changed by {{ diff }}%. β "Usage changed by -15%."
β
Use abs and handle the sign in the wording:
{%- assign diff = current | minus: previous -%}
{%- if diff >= 0 -%}
Usage increased by {{ diff }}%.
{%- else -%}
Usage decreased by {{ diff | abs }}%.
{%- endif -%}
Putting It All Together β The Complete Numeric Pipeline
Hereβs the pattern youβll follow for virtually every numeric calculation in Cast:
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
Complete example β license utilization with benchmarking:
{% 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 -%}
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:
- Converts all three to numbers (preserving the decimal in
growth_rate) - Calculates the projected ARR:
arr * (1 + growth_rate) - Calculates the year-over-year change percentage:
(arr - previous_arr) / previous_arr * 100 - 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
```liquid {%- 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 }}. ``` Output: ``` 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. ``` 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 - Division by zero is guarded with `{% if prev_arr_val > 0 %}` - Rounding happens at the very end, after all math is doneWhatβs Next
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:
- Standard Filters: https://school.cast.app/liquid/liquid-filters.html
- Snippet Library (numeric patterns): https://school.cast.app/liquid/liquid-library.html