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:

  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

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 in VALUE(). The fix in Liquid is | plus: 0 or | 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: 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:


{% 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: 0 is like =A1 + 0 or =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.0
  • 45.0 / 60.0 = 0.75
  • 0.75 * 100 = 75.0
  • round: 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 = 2
  • names[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:

  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 ```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 done

What’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

This site uses Just the Docs, a documentation theme for Jekyll.