What You’ll Learn in This Module

You met assign and capture in Module 3. Now that you understand filters, logic, and loops, this module shows you advanced variable patterns for real Cast narrations — building up content conditionally, accumulating values inside loops, chaining conversions, and organizing your narration setup for maintainability.


Recap — assign vs. capture

Before diving into advanced patterns, a quick reminder:

Tool What It Stores Best For
{% assign x = value %} A single value (with optional filter chain) Data conversion, simple values
{% capture x %}...{% endcapture %} Everything between the tags as a string Multi-part text, conditional assembly

Both create variables you can use later with {{ x }}.


Pattern 1 — The Setup Block

The most important advanced pattern is the setup block at the top of a narration. This is where you do all your data preparation — defaults, conversions, and normalization — so the narration body is clean and readable.


{% comment %}
  QBR Welcome Slide
  Sources: Salesforce (contact, ARR), Gainsight (health score)
  Snippets: CustomerSince, IsEMEA
  Updated: 2026-03-20
{% endcomment %}

{%- # === Text fields === -%}
{%- assign name    = contact_first_name | default: "there" -%}
{%- assign company = contact_account_name | default: "your company" | cast_titlecase -%}
{%- assign csm     = csm_name | default: "your account manager" -%}
{%- assign tier    = product_tier | default: "" | strip | downcase -%}

{%- # === Numeric fields === -%}
{%- assign score   = health_score | default: 0 | plus: 0 -%}
{%- assign arr_val = arr | default: 0 | plus: 0 -%}

{%- # === Date fields === -%}
{%- assign renewal = renewal_date | default: "" -%}

{%- # === Snippet results === -%}
{%- assign region  = IsEMEA | strip -%}
{%- assign tenure  = CustomerSince -%}

After this block, the narration body uses only these clean, typed, safe variables. This separation has three major benefits:

  1. Errors are isolated. If a field is missing, the default catches it in one place.
  2. Readability. The narration body reads like plain English with variable insertions.
  3. Maintainability. When a CRM field name changes, you update one line in the setup block.

Pattern 2 — Conditional Assignment

Sometimes you need a variable’s value to depend on a condition. There are two approaches:

Approach A — Assign a default, then overwrite conditionally:


{%- assign message = "We've prepared some recommendations for you." -%}

{%- if score >= 80 -%}
  {%- assign message = "Your account is performing excellently!" -%}
{%- elsif score >= 60 -%}
  {%- assign message = "Your account shows solid engagement with room to grow." -%}
{%- endif -%}

{{ message }}

This sets a default message first, then replaces it if a higher-tier condition is met. The advantage: the variable is always set, so you can safely use it anywhere below.

Approach B — Use capture with conditions inside:


{%- capture message -%}
  {%- if score >= 80 -%}
    Your account is performing excellently!
  {%- elsif score >= 60 -%}
    Your account shows solid engagement with room to grow.
  {%- else -%}
    We've prepared some recommendations for you.
  {%- endif -%}
{%- endcapture -%}

{{ message | strip }}

This builds the message inside a capture block using conditional logic. Use | strip when outputting because the whitespace from the conditional branches can add extra spaces.

💡 Which to choose? If the message is a short, single sentence, Approach A is simpler. If the message is long or includes variables and filters, Approach B keeps everything together.


Pattern 3 — Multi-Part Sentence Assembly

When a sentence has multiple optional parts based on data, capture shines:


{%- assign score   = health_score | default: 0 | plus: 0 -%}
{%- assign arr_val = arr | default: 0 | plus: 0 -%}
{%- assign company = contact_account_name | default: "Your company" | cast_titlecase -%}

{%- capture summary -%}
  {{ company }} 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 -%}

{{ summary | strip }}

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

For Globex Inc with score 0 and no ARR: Globex Inc has no health score on record.

The sentence structure adapts dynamically. The period at the end always appears, regardless of which branches are taken.


Pattern 4 — Pluralization Helper

A common need in narrations: choosing between singular and plural forms based on a count.


{%- assign task_count = tasks | size -%}

{%- capture task_word -%}
  {%- if task_count == 1 -%}task{%- else -%}tasks{%- endif -%}
{%- endcapture -%}

You have {{ task_count }} open {{ task_word }}.

For 1 task: You have 1 open task. For 5 tasks: You have 5 open tasks.

You can also inline this without capture using the snippet pattern from the prompt:


{{ ActiveLearners }} active learner
{%- if ActiveLearners != "1" %}s{% endif %}
completed {{ Courses }} course
{%- if Courses != "1" %}s{% endif %} this month.

⚠️ Note the comparison is against the string "1", not the number 1 — because snippet output and many CRM fields are strings.


Pattern 5 — Accumulating Inside a Loop

Sometimes you need to process each item in a loop and build a result. Since Liquid doesn’t have a “push” or “append to array” operation that works intuitively, the common workaround is to accumulate into a string and optionally split at the end:


{%- assign features = feature_list | split: "," -%}
{%- assign premium_list = "" -%}

{%- for feature in features -%}
  {%- assign clean = feature | strip -%}
  {%- if clean == "Analytics" or clean == "Advanced Reporting" or clean == "API Access" -%}
    {%- if premium_list != "" -%}
      {%- assign premium_list = premium_list | append: "," -%}
    {%- endif -%}
    {%- assign premium_list = premium_list | append: clean -%}
  {%- endif -%}
{%- endfor -%}

{%- assign premium_features = premium_list | split: "," -%}

{% if premium_features.size > 0 %}
  You're using {{ premium_features.size }} premium features:
  {{ premium_features | join: ", " }}.
{% else %}
  You're not currently using any premium features.
{% endif %}

This loops through all features, filters for “premium” ones, accumulates them as a comma-separated string, then splits back into an array for counting and display.


Pattern 6 — Flag Variables

Use a simple variable as a flag to track whether something happened during processing:


{%- assign has_urgent_task = "no" -%}

{%- assign tasks = open_tasks | split: "," -%}
{%- for task in tasks -%}
  {%- assign clean = task | strip | downcase -%}
  {%- if clean contains "renew" or clean contains "urgent" -%}
    {%- assign has_urgent_task = "yes" -%}
  {%- endif -%}
{%- endfor -%}

{% if has_urgent_task == "yes" %}
  ⚠️ You have at least one urgent task that needs attention.
{% endif %}

The flag starts as "no" and flips to "yes" if any task matches the criteria. After the loop, the flag controls whether the warning message appears.


Variable Scope — What You Need to Know

In Liquid, variables created with assign or capture are available everywhere after the point of assignment in the same template. There’s no “block scope” — a variable created inside an if block or for loop is still accessible after the block ends:


{%- assign x = "before" -%}

{% if true %}
  {%- assign x = "inside if" -%}
  {%- assign y = "also inside if" -%}
{% endif %}

{{ x }}    → "inside if"  (changed by the inner assign)
{{ y }}    → "also inside if"  (still accessible outside the block)

This is useful (you can set variables inside conditions and use them later) but also a potential source of confusion. Be intentional about where you assign variables, and use the setup block pattern to keep things organized.


Common Mistakes

Forgetting that capture stores a string, not a number:


{% capture result %}{{ price | plus: tax }}{% endcapture %}
{% if result > 100 %}

result is a string after capture. Compare with == or convert first.

Convert after capturing:


{% capture result %}{{ price | plus: tax }}{% endcapture %}
{%- assign result_num = result | strip | plus: 0 -%}
{% if result_num > 100 %}


Assigning inside a conditional without a fallback:


{% if score >= 80 %}
  {%- assign tier_label = "Top Tier" -%}
{% elsif score >= 60 %}
  {%- assign tier_label = "Mid Tier" -%}
{% endif %}

Your {{ tier_label }} account...

If score is below 60, tier_label is never assigned — causing an error in strict mode.

Always set a default before the conditional:


{%- assign tier_label = "Standard" -%}

{% if score >= 80 %}
  {%- assign tier_label = "Top Tier" -%}
{% elsif score >= 60 %}
  {%- assign tier_label = "Mid Tier" -%}
{% endif %}

Your {{ tier_label }} account...


Over-using capture for simple values:


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

Use assign for simple values:


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


Try It Yourself

Exercise: You have three fields: score (converted to number, value 72), arr_val (converted to number, value 150000), and tier (string, value “Professional”). Using capture, build a single summary sentence that adapts based on the data:

  • Start with the tier name
  • Add a health score descriptor (“excellent” for 80+, “solid” for 60+, “needs attention” otherwise)
  • Add ARR if greater than 100000
  • End with a period

Target output: Your Professional account has solid health and an ARR of $150000.

Click to reveal the answer ```liquid {%- assign score = 72 -%} {%- assign arr_val = 150000 -%} {%- assign tier = "Professional" -%} {%- capture summary -%} Your {{ tier }} account has {%- if score >= 80 %} excellent health {%- elsif score >= 60 %} solid health {%- else %} health that needs attention {%- endif -%} {%- if arr_val > 100000 %} and an ARR of ${{ arr_val }}{%- endif -%} . {%- endcapture -%} {{ summary | strip }} ``` Output: `Your Professional account has solid health and an ARR of $150000.` If the score were 45 and ARR were 50000: `Your Professional account has health that needs attention.`

What’s Next

In Module 16, you’ll learn how snippets work in Cast — how they’re created, how they return values, and the critical patterns for using snippet output correctly in your narrations.


📖 Official documentation:

  • Tags/Blocks: https://school.cast.app/liquid/liquid-blocks.html

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