What You’ll Learn in This Module
This module covers the single most important foundational concept for working with Liquid in Cast. Understanding data types will prevent the majority of errors new users encounter — and once you understand them, every filter, comparison, and logic block in later modules will make intuitive sense.
A “data type” is simply the kind of value something is. Just as Excel treats a cell formatted as Text differently from a cell formatted as Number (even if both contain 95000), Liquid treats different kinds of values differently. If you try to do math on text, or compare a number to a word, things go wrong — silently and confusingly.
By the end of this module, you’ll know the five data types in Liquid, how to tell them apart, and how to convert between them when needed.
Why Data Types Matter in Cast
Here’s the scenario that trips up almost every new Cast user:
Your Salesforce instance has a field called arr (Annual Recurring Revenue) with the value 95000. You write this in a Cast narration:
{% if arr > 50000 %}
This is a high-value account.
{% endif %}
It looks perfectly reasonable. But it doesn’t work correctly — because arr arrived from Salesforce as the text string "95000", not the number 95000. When Liquid compares a string to a number, it does an alphabetical comparison instead of a numeric one, and you get unexpected results.
This module teaches you how to recognize and handle situations like this so your Cast narrations always behave the way you intend.
The Five Data Types
Liquid has five data types. Let’s walk through each one.
| Data Type | What It Is | Example | Excel Analogy |
|---|---|---|---|
| String | Text | "Hello", "95000" | A cell formatted as Text |
| Number | A value you can do math with | 72, 1.15 | A cell formatted as Number |
| Boolean | True or false | true, false | A TRUE/FALSE cell in Excel |
| Array | An ordered list of items | ["Analytics", "Reporting", "Alerts"] | A column of cells |
| Nil / Empty | Nothing, or an empty value | nil, "" | A blank cell |
Now let’s explore each one in depth.
Strings — Text Values
What is it?
A string is text — any sequence of characters between quotes. It can contain letters, digits, spaces, punctuation, or anything else. In Liquid, anything between quotes is a string.
Here’s the crucial insight: almost everything that arrives from your CRM into Cast is a string, even if it looks like a number, a date, or a true/false value. Salesforce, Gainsight, HubSpot, Totango — they all send data as text by default.
Excel analogy: A string is like a cell formatted as “Text” in Excel. Even if you type
95000into a Text-formatted cell, Excel treats it as the characters 9-5-0-0-0, not as a number you can add or subtract. You’ll see that little green triangle warning in the corner. Same idea in Liquid — a string that looks like a number isn’t one until you convert it.
Why it matters in Cast
Every contact field — names, account names, email addresses, health scores, ARR values, dates — arrives as a string from your CRM. Before you can do math or numeric comparisons on fields like health_score or arr, you must convert them from strings to numbers first.
Liquid syntax
{{ "Hello" }} → Hello
{{ contact_first_name }} → whatever text is in that field
{{ "95000" }} → 95000 (looks like a number, but it IS text)
You can create a string directly by putting text in quotes, or you can reference a variable that holds a string (like contact_first_name).
Real Cast example
Hi {{ contact_first_name | default: "there" }},
Your account name is {{ contact_account_name | default: "Unknown" }}.
Both contact_first_name and contact_account_name are strings. The default filter provides a fallback string in case the original is empty or missing.
Common mistakes
❌ Trying to do math on a string:
{% assign total = arr | plus: 5000 %}
This might work (Liquid will try to convert arr for you), but it’s fragile and unreliable. If arr contains any non-numeric characters like $ or ,, it will fail.
✅ Explicitly convert first, then do math:
{% assign arr_clean = arr | remove: "$" | remove: "," %}
{% assign arr_numeric = arr_clean | plus: 0 %}
{% assign total = arr_numeric | plus: 5000 %}
❌ Assuming a number-looking string is a number in comparisons:
{% if arr > 50000 %}
If arr is the string "95000", this does an alphabetical comparison, not a numeric one. Alphabetically, "9" comes after "5", so it might happen to work — but "6" also comes after "50000" alphabetically, so "6" > "50000" would be true. That’s clearly wrong.
✅ Convert to a number before comparing:
{% assign arr_numeric = arr | plus: 0 %}
{% if arr_numeric > 50000 %}
💡 Quick rule of thumb: If a value came from your CRM and you plan to compare it with >, <, >=, or <=, or do any math on it, convert it to a number first with | plus: 0 or | times: 1. You’ll learn this in depth in Module 5.
Try it yourself
Exercise: You have a variable called nps_score that arrives from Salesforce as the string "72". Write the Liquid to convert it to a number and then display the message “Great NPS!” if the score is 50 or above.
Click to reveal the answer
```liquid {% assign nps_numeric = nps_score | plus: 0 %} {% if nps_numeric >= 50 %} Great NPS! {% endif %} ``` The key step is `| plus: 0` — adding zero converts the string to a number without changing its value. Then the `>=` comparison works correctly as a numeric comparison.Numbers — Values You Can Calculate With
What is it?
A number is a value Liquid can do arithmetic with — addition, subtraction, multiplication, division, rounding, and comparisons like greater-than or less-than. Numbers are written without quotes.
There are two kinds of numbers:
- Integers — whole numbers like
72,0,-15 - Decimals (also called floats) — numbers with a decimal point like
1.15,99.5,0.03
Excel analogy: A number in Liquid is like a cell formatted as Number or Currency in Excel. You can use it in formulas, compare it to other numbers, and the results will be mathematically correct.
Why it matters in Cast
Health scores, ARR, NRR, usage metrics, license counts, percentages — all the quantitative data in your Cast narrations needs to be a number before you can compare it, do math on it, or format it. Since CRM fields arrive as strings, converting to numbers is one of the most common operations you’ll perform.
Liquid syntax
{% assign score = 72 %} {%- # This is an integer -%}
{% assign rate = 1.15 %} {%- # This is a decimal -%}
{% assign total = 100 | plus: 50 %} {%- # Math produces a number: 150 -%}
Converting a string to a number:
{% assign score = health_score | plus: 0 %} {%- # string → integer -%}
{% assign rate = growth_rate | times: 1.0 %} {%- # string → decimal -%}
Both | plus: 0 and | times: 1 convert a string to a number. Use | times: 1.0 (with a decimal point) when you need to preserve decimal precision.
Real Cast example
{% assign current_arr = arr | plus: 0 %}
{% assign growth = current_arr | times: 0.15 | round: 0 %}
At your current growth rate, your ARR could increase by approximately
${{ growth }} next year.
If arr is "200000", this produces:
At your current growth rate, your ARR could increase by approximately
$30000 next year.
Common mistakes
❌ Dividing and expecting a decimal result with integers:
{% assign result = 10 | divided_by: 3 %}
{{ result }} → 3 (not 3.33 — integer division drops the decimal)
✅ Use a decimal divisor to get a decimal result:
{% assign result = 10 | divided_by: 3.0 %}
{{ result }} → 3.3333...
❌ Forgetting that times: 1 and times: 1.0 behave differently:
{% assign rate = "3.75" | times: 1 %}
{{ rate }} → 3 (integer multiplication truncates the decimal)
✅ Use 1.0 to preserve decimals:
{% assign rate = "3.75" | times: 1.0 %}
{{ rate }} → 3.75
💡 When in doubt, use | times: 1.0 for conversion — it handles both integers and decimals correctly.
Try it yourself
Exercise: You have two variables: LicenseUsed (value: "45") and LicenseReserved (value: "60"). Write Liquid that calculates the usage percentage and displays it rounded to one decimal place.
Click to reveal the answer
```liquid {% assign used = LicenseUsed | times: 1.0 %} {% assign reserved = LicenseReserved | times: 1.0 %} {% assign pct = used | divided_by: reserved | times: 100 | round: 1 %} You are using {{ pct }}% of your reserved licenses. ``` Output: `You are using 75.0% of your reserved licenses.` Key details: `times: 1.0` ensures decimal division. Without it, `45 / 60` would give `0` (integer division), then `0 * 100` = `0%` — clearly wrong.Booleans — True and False (and Why Cast Is Different)
What is it?
A boolean is a value that is either true or false — nothing else. Booleans are the foundation of all decision-making in Liquid. Every {% if %} block ultimately evaluates to a boolean: is this condition true, or is it false?
Excel analogy: Booleans are like the TRUE and FALSE values in Excel. When you write
=IF(A1>100, "Big", "Small"), Excel evaluatesA1>100to either TRUE or FALSE, then picks the right branch. Liquid’s{% if %}works the same way.
In standard Liquid (outside Cast), you can assign boolean values directly:
{% assign is_active = true %}
{% if is_active %}
This account is active.
{% endif %}
This works perfectly in standard Liquid because true is a real boolean value.
Why Cast is different — the most important rule in this module
In Cast, snippets always return strings. This is the single most important Cast-specific rule for non-technical users.
When a Cast snippet outputs the word true, what actually comes back is the text string "true" — not the boolean value true. They look identical on screen, but Liquid treats them very differently.
Excel analogy: Imagine a cell that displays the word “TRUE” but is formatted as Text. If you use that cell in an IF formula, Excel won’t treat it as a real TRUE value — it’s just five characters that happen to spell “TRUE”. You’d get unexpected results. Same thing in Cast.
Why it matters in Cast
This boolean/string mismatch is one of the most common sources of subtle bugs in Cast narrations. A narration might seem to work during testing but then behave incorrectly for certain customers — all because a snippet returns the string "true" and the narration compares it against the boolean true.
Liquid syntax — the problem
{% comment %} A snippet called IsEnterprise returns "true" or "false" {% endcomment %}
{% if IsEnterprise == true %} ← comparing string "true" to boolean true
Enterprise content here.
{% endif %}
This comparison is unreliable. The string "true" and the boolean true are different types, and the comparison may not behave as expected.
Liquid syntax — the solution
Option 1 — Compare string to string (minimum fix):
{% if IsEnterprise == "true" %}
Enterprise content here.
{% endif %}
By quoting "true", you’re comparing the string "true" to the string "true" — which works reliably.
Option 2 — Use “yes” / “no” pattern (recommended best practice):
Design your snippets to return "yes" or "no" instead of "true" or "false". This makes the string nature obvious and avoids the boolean confusion entirely.
Inside the snippet:
{% comment %} Snippet Name: IsEnterprise {% endcomment %}
{% if product_tier == "Enterprise" %}yes{% else %}no{% endif %}
In the narration:
{% if IsEnterprise == "yes" %}
You have access to all features including advanced analytics.
{% endif %}
Nobody will accidentally confuse the string "yes" with a boolean, because yes isn’t a boolean keyword in Liquid. The pattern is self-documenting and unambiguous.
Real Cast example
Here’s a complete example using the recommended pattern:
{% comment %} Snippet: IsEMEA — returns "yes" or "no" {% endcomment %}
{% if EMEA_Countries contains country %}yes{% else %}no{% endif %}
{% comment %} In the narration: {% endcomment %}
{% assign region = IsEMEA | strip %}
{% if region == "yes" %}
This content is tailored for EMEA customers.
Your dedicated regional support team is available 8 AM–6 PM CET.
{% else %}
Your dedicated support team is available 8 AM–6 PM in your local time zone.
{% endif %}
Notice the | strip — this removes any accidental whitespace the snippet might include. It’s a small but important safety measure you’ll want to use whenever comparing snippet output.
Common mistakes
❌ Testing a snippet result as a bare boolean:
{% if IsEMEA %}
EMEA content
{% endif %}
This will always be true. In Liquid, any non-nil, non-false value is “truthy” — and the string "false" is still a non-empty string, so it’s truthy. This block would execute even when IsEMEA returns "false" or "no".
✅ Always compare snippet output to a specific string:
{% if IsEMEA == "yes" %}
EMEA content
{% endif %}
❌ Comparing against an unquoted boolean keyword:
{% if IsEMEA == true %}
✅ Compare strings to strings:
{% if IsEMEA == "true" %}
Or better yet, use the yes/no pattern.
❌ Forgetting to strip whitespace from snippet output:
{% if IsEMEA == "yes" %} ← might fail if snippet returns " yes " with spaces
✅ Strip before comparing:
{% assign region = IsEMEA | strip %}
{% if region == "yes" %}
💡 Summary rule: In Cast, treat snippet output as a string. Always compare it with == against a quoted string value. Design your snippets to return "yes" or "no" for clarity.
Try it yourself
Exercise: You have a snippet called IsHighValue that contains this logic:
{% assign revenue = arr | plus: 0 %}
{% if revenue > 100000 %}true{% else %}false{% endif %}
Write the narration code that uses this snippet to display different messages for high-value vs. standard accounts. Then rewrite the snippet using the yes/no pattern.
Click to reveal the answer
**Using the snippet as-is (returns "true" / "false"):** ```liquid {% assign value_tier = IsHighValue | strip %} {% if value_tier == "true" %} As one of our highest-value accounts, you have priority access to new features. {% else %} Contact your CSM to explore options for expanding your account. {% endif %} ``` **Rewritten snippet using yes/no (recommended):** ```liquid {% comment %} Snippet Name: IsHighValue {% endcomment %} {% assign revenue = arr | plus: 0 %} {% if revenue > 100000 %}yes{% else %}no{% endif %} ``` **Updated narration:** ```liquid {% assign value_tier = IsHighValue | strip %} {% if value_tier == "yes" %} As one of our highest-value accounts, you have priority access to new features. {% else %} Contact your CSM to explore options for expanding your account. {% endif %} ```Arrays — Ordered Lists of Items
What is it?
An array is an ordered list of items. Think of it as a collection of values stored together, where each item has a position (starting from 0).
You cannot type an array directly in Liquid the way you can type a string or number. Instead, arrays are created in one of two ways: by splitting a string with the split filter, or they come pre-built from a multi-value field in your dataset.
Excel analogy: An array is like a column of cells in a spreadsheet. You can count how many items there are (like
COUNTA), grab the first or last one, sort them, loop through all of them, or pick one by its position. The position number is the “row” in the column — except it starts at 0, not 1.
Why it matters in Cast
Many CRM fields contain lists: features a customer uses, products they’ve purchased, open support tickets, team members, regions. These often arrive as comma-separated text strings like "Analytics,Reporting,Alerts". To work with the individual items — display them in a list, count them, or check if a specific item is present — you need to split the string into an array first.
Liquid syntax
Creating an array by splitting a string:
{% assign features = "Analytics,Reporting,Alerts" | split: "," %}
Accessing array properties and items:
{{ features | size }} → 3 (how many items)
{{ features | first }} → Analytics (the first item)
{{ features | last }} → Alerts (the last item)
{{ features[0] }} → Analytics (item at position 0)
{{ features[1] }} → Reporting (item at position 1)
{{ features[2] }} → Alerts (item at position 2)
⚠️ Array positions start at 0, not 1. The first item is [0], the second is [1], and so on.
Looping through an array:
{% for feature in features %}
- {{ feature }}
{% endfor %}
This produces:
- Analytics
- Reporting
- Alerts
Real Cast example
{% assign features = feature_list | split: "," %}
You're currently using {{ features | size }} key features.
Your top features this quarter:
{% for feature in features limit: 3 %}
{{ forloop.index }}. {{ feature | strip }}
{% endfor %}
If feature_list is "Analytics, Reporting, Alerts, Dashboards, API Access", this produces:
You're currently using 5 key features.
Your top features this quarter:
1. Analytics
2. Reporting
3. Alerts
Notice the | strip inside the loop — this removes any extra spaces that might be around each item after splitting (e.g., " Reporting" becomes "Reporting").
Common mistakes
❌ Looping over a string instead of an array:
{% for item in feature_list %}
{{ item }}
{% endfor %}
If feature_list is a comma-separated string (not an array), this loops over each character in the string, not each feature. You’d get A, n, a, l, y, t, i, c, s, ,, etc.
✅ Split first, then loop:
{% assign items = feature_list | split: "," %}
{% for item in items %}
{{ item | strip }}
{% endfor %}
❌ Accessing position 1 expecting the first item:
{{ features[1] }} → Reporting (the SECOND item, not the first)
✅ Remember: positions start at 0:
{{ features[0] }} → Analytics (the first item)
{{ features | first }} → Analytics (clearer alternative)
❌ Forgetting | strip after splitting a string with spaces:
{% assign items = "Analytics, Reporting, Alerts" | split: "," %}
{{ items[1] }} → " Reporting" (note the leading space)
✅ Strip whitespace from each item:
{% assign items = "Analytics, Reporting, Alerts" | split: "," %}
{{ items[1] | strip }} → "Reporting"
💡 Checking if an array contains a specific value:
{% assign features = feature_list | split: "," %}
{% if features contains "Analytics" %}
You have access to Analytics.
{% endif %}
This uses the contains operator, which checks whether an array includes a specific item. Be aware that contains is case-sensitive — "analytics" would not match "Analytics".
Try it yourself
Exercise: You have a variable open_tasks with the value "Renew contract,Schedule QBR,Update health score,Review usage data". Write Liquid that splits this into an array, displays how many open tasks there are, and then lists only the first two.
Click to reveal the answer
```liquid {% assign tasks = open_tasks | split: "," %} You have {{ tasks | size }} open tasks. Here are your top priorities: {% for task in tasks limit: 2 %} {{ forloop.index }}. {{ task | strip }} {% endfor %} ``` Output: ``` You have 4 open tasks. Here are your top priorities: 1. Renew contract 2. Schedule QBR ``` Key details: `split: ","` breaks the string at each comma. `limit: 2` stops the loop after two iterations. `forloop.index` gives the 1-based position number. `| strip` cleans up any whitespace.Nil, Null, and Empty Strings — When Nothing Is Something
What is it?
Sometimes a field has no value at all. In Liquid, there are two flavors of “nothing,” and they behave slightly differently:
- Nil (also called null) — The field doesn’t exist or was never set. There is nothing there at all.
- Empty string (
"") — The field exists, but its value is an empty piece of text — zero characters long.
Excel analogy: Nil is like a cell that has never been touched — completely blank, no formatting, nothing. An empty string is like a cell where someone typed something, then deleted it — the cell has been edited, but it contains nothing visible. In most practical situations, you want to handle both the same way, and Liquid gives you a tool to do exactly that.
Why it matters in Cast
Cast’s strict mode will raise an error if you try to display a nil variable directly. And CRM data is messy — fields are missing, contacts have incomplete profiles, optional fields are blank. If you don’t handle nil and empty values, your Cast narrations will break for any customer with incomplete data.
Liquid syntax
Displaying a value with a fallback:
{{ contact_first_name | default: "there" }}
The default filter catches both nil and empty strings. If contact_first_name is nil or "", it outputs "there" instead.
Testing for blank (catches both nil and empty string):
{% if contact_first_name == blank %}
The name field is missing or empty.
{% endif %}
The special keyword blank matches both nil and empty strings in a single test. This is the most defensive check and the one you should use most often.
Testing for nil specifically:
{% if contact_first_name == nil %}
The field does not exist at all.
{% endif %}
Testing for empty string specifically:
{% if contact_first_name == "" %}
The field exists but is empty.
{% endif %}
Real Cast example
Here’s a pattern you’ll use constantly — safe variable setup at the top of a narration:
{% assign name = contact_first_name | default: "there" %}
{% assign company = contact_account_name | default: "your company" %}
{% assign score = health_score | default: 0 | plus: 0 %}
Hi {{ name }},
Welcome to {{ company | cast_titlecase | cast_apostrophe }} quarterly review.
{% if score >= 80 %}
Your health score of {{ score }} is excellent.
{% elsif score > 0 %}
Your health score of {{ score }} shows room for improvement.
{% else %}
We don't have a health score on file yet — let's set one up.
{% endif %}
Notice the third line: | default: 0 | plus: 0. This does two things in sequence — if health_score is nil or empty, it falls back to 0, then | plus: 0 converts whatever we have to a number. This is a very common and reliable pattern for numeric fields.
Snippet output and blank checks
Because snippets return strings, a snippet that produces no output actually returns an empty string "", not nil. If you need to check whether a snippet returned anything useful:
{% assign result = MySnippet | strip %}
{% if result == "" %}
The snippet returned no output.
{% else %}
The snippet returned: {{ result }}
{% endif %}
💡 Use | strip before testing — the snippet might return whitespace characters that look empty but technically aren’t "".
Common mistakes
❌ Outputting a potentially nil variable without a fallback:
Hi {{ contact_first_name }},
In Cast’s strict mode, this errors if the field is nil or missing.
✅ Always use default for fields that might be missing:
Hi {{ contact_first_name | default: "there" }},
❌ Using == nil when you should use == blank:
{% if contact_first_name == nil %}
Show fallback
{% endif %}
This only catches nil — if the field exists but is "", the fallback won’t trigger.
✅ Use == blank to catch both:
{% if contact_first_name == blank %}
Show fallback
{% endif %}
❌ Doing math on a nil value without a default:
{% assign score = health_score | plus: 0 %}
If health_score is nil, this may still error.
✅ Chain default before the conversion:
{% assign score = health_score | default: 0 | plus: 0 %}
Try it yourself
Exercise: You have three variables from your CRM: contact_first_name (might be nil), csm_name (might be empty string ""), and renewal_date (might be nil). Write Liquid that safely displays a sentence using all three, with appropriate fallbacks. The sentence should read like: “Hi [name], your CSM [csm] will reach out before your renewal on [date].”
Click to reveal the answer
```liquid {% assign name = contact_first_name | default: "there" %} {% assign csm = csm_name | default: "your account manager" %} {% assign renewal = renewal_date | default: "a date to be confirmed" %} Hi {{ name }}, your CSM {{ csm }} will reach out before your renewal on {{ renewal }}. ``` If all fields are missing, this produces: ``` Hi there, your CSM your account manager will reach out before your renewal on a date to be confirmed. ``` The key principle: every variable that might be nil or empty gets a `default` applied *before* it's used in the output.How the Five Types Interact — A Summary
Here’s a reference table showing how the five data types relate to each other and the most common conversions you’ll need:
| From | To | How | Example |
|---|---|---|---|
| String → Number | plus: 0 or times: 1 | {% assign n = "95000" \| plus: 0 %} | |
| String → Number (decimal) | times: 1.0 | {% assign r = "3.75" \| times: 1.0 %} | |
| String → Array | split: "," | {% assign items = "a,b,c" \| split: "," %} | |
| Array → String | join: ", " | {{ items \| join: ", " }} | |
| Nil/Empty → String | default: "fallback" | {{ name \| default: "there" }} | |
| Nil/Empty → Number | default: 0 \| plus: 0 | {% assign n = score \| default: 0 \| plus: 0 %} | |
| Snippet → String comparison | strip + == "yes" | {% assign r = MySnippet \| strip %} |
💡 The golden rule: Know what type your data is, know what type you need, and convert before you use it.
What’s Next
Now that you understand the five data types, you’re ready to start working with them. In Module 3, you’ll learn about output and variables — how to display values in your narration, store intermediate results, and build up complex content piece by piece.
📖 Official documentation:
- Overview: https://school.cast.app/liquid.html
- Standard Filters (including
default,plus,split): https://school.cast.app/liquid/liquid-filters.html - Contact Variables: https://school.cast.app/fields-snippets-data-validation/contact-variables.html