What You’ll Learn in This Module

In Module 1, you met the three building blocks of Liquid: output, filters, and tags. In Module 2, you learned the five data types. Now it’s time to put those together and master the two things you’ll do most often in Cast narrations: displaying values (output) and storing values for later use (variables).

By the end of this module, you’ll understand exactly how {{ }} output tags work, how to create and use variables with assign and capture, how whitespace control keeps your output clean, and how comments help you document your work.


Output — Displaying Values with {{ }}

What is it?

An output tag is how you tell Liquid: “Put this value right here in the narration.” Anything between double curly braces — {{ }} — gets evaluated and replaced with its result when Cast generates the presentation.

Analogy: Output tags are exactly like merge fields in Word mail merge («First_Name») or cell references in an Excel formula (=A1). They’re placeholders that say “go get this value and put it here.”

Why it matters in Cast

Every piece of personalized content in a Cast narration — the contact’s name, their company, their health score, their renewal date — is inserted using output tags. Without them, every customer would see identical, generic text.

Liquid syntax

The simplest output is just a variable name between double curly braces:


{{ contact_first_name }}

You can also output a literal value (though this is less common, since you could just type the text directly):


{{ "Hello, welcome!" }}

And you can apply one or more filters to transform the value before it’s displayed:


{{ contact_account_name | cast_titlecase }}
{{ contact_account_name | cast_titlecase | cast_apostrophe }}
{{ health_score | plus: 0 | round: 0 }}

Filters are chained left to right with the pipe character (|). The value flows through each filter in order, like water through a series of pipes — each one transforms it and passes the result to the next.

Real Cast example


Hi {{ contact_first_name | default: "there" }},

Welcome to {{ contact_account_name | default: "your" | cast_titlecase | cast_apostrophe }}
quarterly business review. Your health score is currently
{{ health_score | default: "not available" }}.

For a contact named Priya at GLOBEX INC with a health score of 82:


Hi Priya,

Welcome to Globex Inc's quarterly business review. Your health score
is currently 82.

For a contact with missing data:


Hi there,

Welcome to your quarterly business review. Your health score is
currently not available.

Common mistakes

Using tag syntax {% %} when you mean output {{ }}:


{% contact_first_name %}

This tells Liquid to execute contact_first_name as a command, which it isn’t. You’ll get an error.

Use double curly braces for displaying values:


{{ contact_first_name }}


Forgetting the closing braces:


{{ contact_first_name }

Always match your braces — two opening, two closing:


{{ contact_first_name }}


Putting logic inside output tags:


{{ if health_score > 80 }}

Output tags are for displaying values, not for logic. Logic uses {% %}.

Use tag syntax for logic:


{% if health_score > 80 %}


Try it yourself

Exercise: Write a single output tag that takes contact_email, converts it to lowercase, and provides a fallback of “no email on file” if the field is missing.

Click to reveal the answer ```liquid {{ contact_email | default: "no email on file" | downcase }} ``` The value flows left to right: take `contact_email` → if nil or empty, use `"no email on file"` → convert to lowercase. If the email exists, it gets lowercased. If it's missing, the fallback text gets lowercased (which is fine — it's already lowercase).

Variables with assign — Storing Values for Later

What is it?

A variable is a named container that holds a value. The assign tag lets you create a variable and store something in it — a piece of text, a number, or the result of a filter chain. Once assigned, you can use that variable by name anywhere later in the narration.

Excel analogy: Assigning a variable is like naming a cell in Excel. Instead of referring to cell B7 every time, you give it the name health_score and use that name in your formulas. In Liquid, {% assign greeting = "Welcome back" %} is like naming a cell greeting and putting the text “Welcome back” in it.

Why it matters in Cast

Variables let you do three critical things in Cast narrations:

  1. Convert data once and reuse it. Instead of writing | plus: 0 every time you use a numeric field, convert it once at the top and use the variable throughout.
  2. Break complex logic into readable steps. Instead of one enormous, unreadable filter chain, assign intermediate results to named variables.
  3. Avoid repeating yourself. If the same transformed value appears in multiple places, assign it once and reference the variable.

Liquid syntax


{% assign greeting = "Welcome back" %}
{% assign score = health_score | plus: 0 %}
{% assign name = contact_first_name | default: "there" %}
{% assign company = contact_account_name | default: "your company" | cast_titlecase %}

The pattern is always: {% assign variable_name = value %}, where the value can be a literal, another variable, or a variable with filters applied.

After assignment, use the variable in output tags just like any other value:


{{ greeting }}, {{ name }}! Let's review {{ company | cast_apostrophe }} performance.

Real Cast example

Here’s a pattern you’ll use at the top of many Cast narrations — a “setup block” that prepares all your variables:


{% comment %} === Variable Setup === {% endcomment %}
{% assign name    = contact_first_name | default: "there" %}
{% assign company = contact_account_name | default: "your company" | cast_titlecase %}
{% assign score   = health_score | default: 0 | plus: 0 %}
{% assign arr_val = arr | default: 0 | plus: 0 %}
{% assign renewal = renewal_date | default: "" %}

{% comment %} === Narration Content === {% endcomment %}
Hi {{ name }},

Welcome to {{ company | cast_apostrophe }} Executive Business Review.

{% if score >= 80 %}
  Your health score of {{ score }} is excellent.
{% elsif score > 0 %}
  Your health score of {{ score }} shows solid engagement.
{% else %}
  We don't have a health score on file yet.
{% endif %}

{% unless renewal == "" %}
  Your contract renews on {{ renewal | date: "%B %d, %Y" }}.
{% endunless %}

This approach has several advantages. All your data preparation happens in one place at the top, making it easy to review and update. The narration body is clean and readable, using simple variable names instead of repeated filter chains. And every potentially missing field has a safe default.

Rules for variable names

Variable names in Liquid:

  • Can contain letters, numbers, and underscores
  • Cannot contain spaces or special characters
  • Are case-sensitive (Score and score are different variables)
  • Should be descriptive (arr_numeric is better than x)

💡 Naming convention: Use snake_case (lowercase with underscores) for variables you create. This keeps them visually consistent with the CRM variables Cast provides, like contact_first_name and health_score.

Reassigning variables

You can change a variable’s value at any point by assigning to it again:


{% assign message = "Your account is in good standing." %}

{% assign score = health_score | plus: 0 %}
{% if score < 60 %}
  {% assign message = "We'd like to discuss some areas for improvement." %}
{% endif %}

{{ message }}

The second assign overwrites the first value of message. This is useful for setting a default message and then changing it based on conditions.

Common mistakes

Using assign inside output tags:


{{ assign name = contact_first_name }}

assign is a tag (logic), not an output. It needs {% %}.

Use tag syntax for assign:


{% assign name = contact_first_name %}


Forgetting quotes around string literals:


{% assign greeting = Welcome back %}

Liquid will try to interpret Welcome as a variable name. Since no variable called Welcome exists, this will error in strict mode.

Always quote string literals:


{% assign greeting = "Welcome back" %}


Trying to assign multiple variables on one line:


{% assign name = "Jason", score = 85 %}

Liquid doesn’t support this. Each assignment needs its own tag.

One assign per line:


{% assign name = "Jason" %}
{% assign score = 85 %}


Try it yourself

Exercise: You have two CRM fields: arr (value: "250000") and nrr (value: "112"). Write a setup block that converts both to numbers (preserving decimals for nrr), then write an output sentence that says: “Your ARR is [arr] with a net revenue retention of [nrr]%.”

Click to reveal the answer ```liquid {% assign arr_val = arr | default: 0 | plus: 0 %} {% assign nrr_val = nrr | default: 0 | times: 1.0 %} Your ARR is {{ arr_val }} with a net revenue retention of {{ nrr_val }}%. ``` Output: `Your ARR is 250000 with a net revenue retention of 112.0%.` Key details: `plus: 0` converts the ARR string to an integer (no decimals needed for whole dollar amounts). `times: 1.0` converts NRR to a decimal, preserving any fractional values. Both have `default: 0` as a safety net for missing data.

Variables with capture — Building Complex Text

What is it?

The capture tag is like assign, but instead of storing a single value or filter result, it captures everything between the opening and closing tags — including text, output tags, and even logic blocks — and stores it all as a single string variable.

Excel analogy: If assign is like putting a formula result into a named cell, capture is like using CONCATENATE (or &) to build a long text string from multiple pieces and then naming the result. It lets you assemble complex content step by step.

Why it matters in Cast

capture is invaluable when you need to build up a message that includes conditional parts, multiple variables, and formatted text — and then use that assembled message in one place. It’s especially useful for building sentences where the structure changes based on data.

Liquid syntax


{% capture renewal_message %}
  Your contract renews on {{ renewal_date | date: "%B %d, %Y" }}.
{% endcapture %}

{{ renewal_message }}

Everything between {% capture renewal_message %} and {% endcapture %} is evaluated — variables are resolved, filters are applied, logic is executed — and the final result is stored in renewal_message as a string.

Real Cast example

Here’s a scenario where capture truly shines: building a dynamic summary sentence that varies based on multiple conditions.


{% assign score = health_score | default: 0 | plus: 0 %}
{% assign arr_val = arr | default: 0 | plus: 0 %}

{% capture account_summary %}
  {{ contact_account_name | default: "Your company" | cast_titlecase }} has
  {%- if score >= 80 %} a strong health score of {{ score }}
  {%- elsif score > 0 %} a health score of {{ score }}
  {%- else %} no health score on record
  {%- endif %}
  {%- if arr_val > 0 %} and an ARR of ${{ arr_val }}
  {%- endif -%}
  .
{% endcapture %}

{{ account_summary | strip }}

For ACME LLC with a score of 85 and ARR of 200000:


Acme LLC has a strong health score of 85 and an ARR of $200000.

For GLOBEX INC with a score of 45 and no ARR on file:


Globex Inc has a health score of 45.

The capture block assembles the right sentence structure based on the data, and the result is stored as a clean string you can output wherever you need it. Notice | strip at the end to clean up any extra whitespace the conditional branches might introduce.

When to use assign vs. capture

Use assign when… Use capture when…
Storing a single value Building multi-part text
Applying filters to one variable Combining variables, text, and logic
Converting data types Assembling conditional sentences
Setting a simple default Creating reusable message blocks

A practical guideline: if what you’re storing fits on one line, use assign. If you need multiple lines, conditionals, or mixed text and variables, use capture.

Common mistakes

Forgetting endcapture:


{% capture message %}
  Your renewal is on {{ renewal_date | date: "%B %d, %Y" }}.

Without {% endcapture %}, Liquid doesn’t know where the captured content ends. You’ll get an error.

Always close your capture blocks:


{% capture message %}
  Your renewal is on {{ renewal_date | date: "%B %d, %Y" }}.
{% endcapture %}


Using capture when assign would be simpler:


{% capture name %}{{ contact_first_name | default: "there" }}{% endcapture %}

This works, but it’s needlessly complex for a single value.

Use assign for simple values:


{% assign name = contact_first_name | default: "there" %}


Forgetting that capture stores a string (not a number):


{% capture total %}{{ price | plus: tax }}{% endcapture %}
{% if total > 100 %}    ← total is a string, not a number!

Convert after capturing if you need to do math:


{% capture total %}{{ price | plus: tax }}{% endcapture %}
{% assign total_num = total | strip | plus: 0 %}
{% if total_num > 100 %}


Try it yourself

Exercise: Using capture, build a greeting that says “Good morning” if the variable time_of_day is "morning", “Good afternoon” if it’s "afternoon", and “Hello” otherwise. Store the result in a variable called greeting, then display it followed by the contact’s first name.

Click to reveal the answer ```liquid {% capture greeting %} {%- if time_of_day == "morning" -%} Good morning {%- elsif time_of_day == "afternoon" -%} Good afternoon {%- else -%} Hello {%- endif -%} {% endcapture %} {{ greeting | strip }}, {{ contact_first_name | default: "there" }}! ``` Output (if `time_of_day` is `"morning"` and `contact_first_name` is `"Jason"`): ``` Good morning, Jason! ``` Note the `-` in `{%- -%}` — that's whitespace control, which we'll cover next. And `| strip` on the output removes any extra spaces the `capture` block might introduce.

Whitespace Control — Keeping Output Clean

What is it?

When Liquid processes your template, the tags themselves ({% %}) disappear from the output — but the blank lines and spaces around them don’t. Every newline and space you put around your Liquid tags shows up in the final output. This can result in awkward blank lines and extra spacing in your narration.

Whitespace control lets you tell Liquid: “Remove the extra blank space around this tag.” You do this by adding a hyphen (-) inside the tag.

Analogy: Think of it like the “Show Formatting Marks” button in Word (¶). All those invisible paragraph marks and spaces are there in your document — whitespace control in Liquid is like selectively deleting them so your output looks clean.

Why it matters in Cast

Cast narrations are spoken aloud by an audio narrator and displayed as text. Extra blank lines can cause awkward pauses in audio and unsightly gaps in text. Whitespace control ensures your output is clean and professional, regardless of how much Liquid logic you’ve written behind the scenes.

Liquid syntax

Standard tags (no whitespace control):


{% assign name = "Jason" %}
Hello, {{ name }}!

Output (note the blank line where the assign tag was):



Hello, Jason!

Tags with whitespace stripping:


{%- assign name = "Jason" -%}
Hello, {{ name }}!

Output (clean — no extra blank line):


Hello, Jason!

The rules are simple:

Syntax What it strips
{%- Strips whitespace and newlines before the tag
-%} Strips whitespace and newlines after the tag
{%- ... -%} Strips on both sides
{{- and -}} Same, but for output tags

Real Cast example

Without whitespace control:


{% assign score = health_score | plus: 0 %}
{% if score >= 80 %}
Excellent score!
{% endif %}
More content here.

Output:




Excellent score!

More content here.

Those blank lines come from the lines where assign and if/endif tags were. With whitespace control:


{%- assign score = health_score | plus: 0 -%}
{%- if score >= 80 -%}
Excellent score!
{%- endif %}
More content here.

Output:


Excellent score!
More content here.

Clean and compact.

When to use whitespace control

💡 Practical guideline: Add {%- -%} (both sides) to assign tags, since they should never produce visible output. For if/elsif/else/endif tags, use it when you want the conditional content to flow seamlessly into the surrounding text. Leave it off when you want the line break — for example, between paragraphs.

A common pattern is to use whitespace control on all “structural” tags and leave it off where you want natural paragraph breaks:


{%- assign name = contact_first_name | default: "there" -%}
{%- assign score = health_score | default: 0 | plus: 0 -%}

Hi {{ name }},

{%- if score >= 80 %}
Your account is doing great!
{%- else %}
Let's look at some areas for improvement.
{%- endif %}

Common mistakes

Over-using whitespace control and collapsing everything together:


{%- if score >= 80 -%}
Great score!
{%- endif -%}
{%- if arr_val > 100000 -%}
High value account.
{%- endif -%}

Output: Great score!High value account. — no space between the sentences.

Leave strategic whitespace where you want it:


{%- if score >= 80 -%}
Great score!
{%- endif %}
{%- if arr_val > 100000 -%}
High value account.
{%- endif %}

Output:


Great score!
High value account.


Forgetting the hyphen inside, putting it outside:


{%- assign name = "Jason" %}   ← only strips before, not after
-%}                             ← this is wrong — hyphen goes inside the tag

Hyphen always goes inside the brace-percent pair:


{%- assign name = "Jason" -%}


Try it yourself

Exercise: The following Liquid produces output with unwanted blank lines. Add whitespace control to fix it so the output reads as a single clean paragraph with no extra blank lines at the top.


{% assign name = contact_first_name | default: "there" %}
{% assign score = health_score | default: 0 | plus: 0 %}
Hi {{ name }}, your health score is {{ score }}.

Click to reveal the answer ```liquid {%- assign name = contact_first_name | default: "there" -%} {%- assign score = health_score | default: 0 | plus: 0 -%} Hi {{ name }}, your health score is {{ score }}. ``` By adding `-` to both sides of both `assign` tags, the blank lines they would normally leave behind are removed. The output is just: ``` Hi Jason, your health score is 85. ```

Comments — Documenting Your Work

What is it?

A comment is text in your Liquid that is completely invisible in the output. It exists only for the human reading or editing the template — to explain what the code does, why a decision was made, or to leave notes for teammates.

Excel analogy: Comments in Liquid are like cell comments (or notes) in Excel — they help you and your colleagues understand what’s going on, but the customer never sees them.

Why it matters in Cast

Cast narrations can get complex, especially when they include multiple snippets, conditional logic, and data conversions. Comments help you (and whoever works on the narration after you) understand why the Liquid was written a certain way. They’re especially valuable for documenting snippet behavior, noting which CRM fields need specific formatting, or explaining business logic.

Liquid syntax

Block comments — for multi-line explanations:


{% comment %}
  This section shows different messaging based on health score.
  Score arrives as a string from Salesforce and must be converted.
  Updated by: Jamie, 2026-03-15
{% endcomment %}

Everything between {% comment %} and {% endcomment %} is completely removed from the output.

Inline comments — for quick single-line notes:


{%- # Convert health score from string to number -%}
{% assign score = health_score | default: 0 | plus: 0 %}

{% assign arr_val = arr | plus: 0 %}   {%- # ARR comes from Salesforce as string -%}

Inline comments use # after the opening {% or {%-. They’re shorter and can sit on their own line or at the end of another tag line.

Real Cast example

Here’s a well-documented narration setup:


{% comment %}
  QBR Welcome Slide — Narration Script
  Data sources: Salesforce (contact fields), Gainsight (health score)
  Snippets used: CustomerSince, IsEMEA
  Last updated: 2026-03-20 by Taylor
{% endcomment %}

{%- # === Variable Setup === -%}
{%- assign name    = contact_first_name | default: "there" -%}
{%- assign company = contact_account_name | default: "your company" | cast_titlecase -%}
{%- assign score   = health_score | default: 0 | plus: 0 -%}
{%- # score: string → number conversion required (Salesforce text field) -%}

{%- # === EMEA Check — snippet returns "yes" or "no" as string === -%}
{%- assign region = IsEMEA | strip -%}

Hi {{ name }},

Welcome to {{ company | cast_apostrophe }} Executive Business Review.

{%- if region == "yes" %}
Your dedicated EMEA support team is available for any questions.
{%- endif %}

None of the comment text appears in the output. But six months from now, when someone else needs to update this narration, they’ll immediately understand the data flow, the snippets involved, and the conversion requirements.

Best practices for comments

💡 What to comment:

  • The purpose of the narration or section
  • Which CRM system each field comes from
  • Which snippets are used and what they return
  • Any data type conversions and why they’re needed
  • Business logic explanations (“score >= 80 = top tier per CS team definition”)
  • Who last updated and when

💡 What NOT to comment:

  • Obvious things like {%- # assign name -%} above an assign tag — the code already says that
  • Every single line — over-commenting makes the template harder to read, not easier

Common mistakes

Forgetting endcomment for block comments:


{% comment %}
  This is my note about the narration.

Without {% endcomment %}, everything after this point becomes a comment — your entire narration disappears.

Always close block comments:


{% comment %}
  This is my note about the narration.
{% endcomment %}


Putting comments inside output tags:


{{ contact_first_name {{ contact_first_name {% comment %}This is the name{% endcomment %} }}

You can’t nest tags inside output tags.

Put comments on their own line:


{%- # Display the contact's first name with fallback -%}
{{ contact_first_name | default: "there" }}


Try it yourself

Exercise: Add appropriate comments to the following Liquid to explain what each section does. Use a block comment at the top and inline comments for the individual lines.


{% assign score = health_score | default: 0 | plus: 0 %}
{% assign arr_val = arr | default: 0 | plus: 0 %}
{% assign name = contact_first_name | default: "there" %}

{% if score >= 80 %}
  {{ name }}, your account is thriving with a health score of {{ score }}.
{% else %}
  {{ name }}, let's work together to improve your health score of {{ score }}.
{% endif %}

Click to reveal the answer There's no single "right" answer for comments — the goal is clarity. Here's one good version: ```liquid {% comment %} Health Score Messaging Block Displays personalized message based on health score tier. health_score and arr come from Salesforce as strings. {% endcomment %} {%- # Convert CRM strings to numbers for comparison -%} {%- assign score = health_score | default: 0 | plus: 0 -%} {%- assign arr_val = arr | default: 0 | plus: 0 -%} {%- assign name = contact_first_name | default: "there" -%} {%- # Threshold: 80+ = top tier (defined by CS team) -%} {% if score >= 80 %} {{ name }}, your account is thriving with a health score of {{ score }}. {% else %} {{ name }}, let's work together to improve your health score of {{ score }}. {% endif %} ``` Notice the addition of whitespace control (`{%- -%}`) on the assign lines as well — that's a bonus improvement.

Putting It All Together — The Complete Output and Variables Toolkit

Here’s a summary of everything you learned in this module:

Tool Syntax Purpose
Output {{ value }} Display a value in the narration
Output with filters {{ value \| filter1 \| filter2 }} Transform then display
Assign {% assign x = value %} Store a value in a named variable
Assign with filters {% assign x = value \| filter %} Transform, then store
Capture {% capture x %}...{% endcapture %} Build complex text and store it
Whitespace control {%- ... -%} Remove blank lines around tags
Block comment {% comment %}...{% endcomment %} Multi-line invisible note
Inline comment {%- # note -%} Single-line invisible note

And here’s the pattern you’ll use at the start of nearly every Cast narration:


{% comment %}
  [Narration name and purpose]
  [Data sources and snippets used]
  [Last updated: date, author]
{% endcomment %}

{%- # === Variable Setup === -%}
{%- assign name    = contact_first_name | default: "there" -%}
{%- assign company = contact_account_name | default: "your company" | cast_titlecase -%}
{%- assign score   = health_score | default: 0 | plus: 0 -%}
{%- assign arr_val = arr | default: 0 | plus: 0 -%}

{%- # === Narration Body === -%}
Hi {{ name }},

Welcome to {{ company | cast_apostrophe }} Executive Business Review.

This setup gives you safe defaults, proper type conversions, clean whitespace, and documentation — all before the customer sees a single word.


What’s Next

Now that you can display values and store them in variables, you’re ready to start transforming those values. In Module 4, you’ll learn the standard text filters — the tools for changing capitalization, trimming spaces, replacing text, and more.


📖 Official documentation:

  • Overview: https://school.cast.app/liquid.html
  • Tags/Blocks (assign, capture, comment): https://school.cast.app/liquid/liquid-blocks.html
  • Standard Filters: https://school.cast.app/liquid/liquid-filters.html
  • Contact Variables: https://school.cast.app/fields-snippets-data-validation/contact-variables.html

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