What You’ll Learn in This Module

Loops let you repeat a block of content once for each item in an array. Instead of writing separate lines for Feature 1, Feature 2, Feature 3, you write one template and Liquid repeats it for every item in the list. This module covers the for loop, its control parameters (limit, offset), and the forloop helper variables that let you customize output based on position.


What Is a Loop?

A loop is an instruction that says: β€œFor each item in this list, do the following.” It takes a template (the body of the loop) and repeats it once per item, substituting the current item each time.

Excel analogy: Imagine you have a column of feature names (A1:A10) and you want to create a numbered list in column B. You’d write a formula in B1 like =ROW()&". "&A1, then copy it down to B10. A for loop does the same thing β€” it takes one template and applies it to every item automatically.

Why it matters in Cast

Customer data often includes lists β€” features used, open tasks, products purchased, regions served. Loops let you display these lists dynamically, adapting to however many items each customer has. A customer with 3 features gets a 3-item list; one with 7 gets a 7-item list β€” all from the same template.


Basic for Loop

Liquid syntax


{% for item in array %}
  {{ item }}
{% endfor %}

  • item is a temporary variable name β€” you choose it. It holds the current item on each pass through the loop.
  • array is the array to loop through.
  • Everything between {% for %} and {% endfor %} is repeated once per item.

Real Cast example


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

Your active features:
{% for feature in features %}
  - {{ feature | strip }}
{% endfor %}

If feature_list is "Analytics,Reporting,Alerts,Dashboards":


Your active features:
  - Analytics
  - Reporting
  - Alerts
  - Dashboards


Numbered Lists with forloop.index

Inside a loop, Liquid provides a special object called forloop with information about the current iteration:

Variable What It Returns First Item Second Item Last Item (of 4)
forloop.index Position starting at 1 1 2 4
forloop.index0 Position starting at 0 0 1 3
forloop.first Is this the first item? true false false
forloop.last Is this the last item? false false true
forloop.length Total number of items 4 4 4

Real Cast example β€” numbered list


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

Your top features this quarter:
{% for feature in features %}
  {{ forloop.index }}. {{ feature | strip }}
{% endfor %}

Output:


Your top features this quarter:
  1. Analytics
  2. Reporting
  3. Alerts
  4. Dashboards

Real Cast example β€” comma-separated with β€œand” before the last item


{%- assign features = feature_list | split: "," -%}
Your features include
{%- for feature in features -%}
  {%- if forloop.last and forloop.length > 1 %} and {% elsif forloop.first == false %}, {% endif -%}
  {{ feature | strip }}
{%- endfor -%}.

If there are 3 features: Your features include Analytics, Reporting and Alerts.

This uses forloop.last to insert β€œand” before the final item and forloop.first to avoid a leading comma.


Limiting Results with limit

The limit parameter stops the loop after a specified number of iterations. Useful when you want to show β€œtop 3” or β€œfirst 5” from a longer list.


{% for feature in features limit: 3 %}
  {{ forloop.index }}. {{ feature | strip }}
{% endfor %}

Even if features has 10 items, only the first 3 are shown.

Real Cast example β€” top 3 with overflow count


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

Your top features this quarter:
{% for feature in features limit: 3 %}
  {{ forloop.index }}. {{ feature | strip }}
{% endfor %}

{% if features.size > 3 %}
  ...and {{ features.size | minus: 3 }} more.
{% endif %}

Output (for 6 features):


Your top features this quarter:
  1. Analytics
  2. Reporting
  3. Alerts

  ...and 3 more.


Skipping Items with offset

The offset parameter skips a specified number of items from the beginning:


{% for feature in features offset: 2 %}
  {{ feature | strip }}
{% endfor %}

This skips the first 2 items and starts from the third.

Combining limit and offset

You can use both together to get a β€œslice” of the array:


{% for feature in features limit: 3 offset: 2 %}
  {{ feature | strip }}
{% endfor %}

This skips 2 items, then shows the next 3 β€” items at positions 3, 4, and 5.


Conditional Content Inside Loops

You can use if/else inside loops to customize each item’s display based on its value or position:


{%- assign tasks = open_tasks | split: "," -%}

{% for task in tasks %}
  {%- assign clean_task = task | strip -%}
  {% if forloop.first %}
    πŸ”΄ Priority: {{ clean_task }}
  {% else %}
    βšͺ {{ clean_task }}
  {% endif %}
{% endfor %}

Output:


    πŸ”΄ Priority: Renew contract
    βšͺ Schedule QBR
    βšͺ Update health score


Empty Arrays β€” The for/else Pattern

What if the array is empty? You can handle this with an else clause directly inside the for block β€” this else runs only if the array has zero items:


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

{% for feature in features %}
  {{ forloop.index }}. {{ feature | strip }}
{% else %}
  No features are currently active on your account.
{% endfor %}

If feature_list is empty, the else content is shown instead of the loop body.

πŸ’‘ This is cleaner than wrapping the whole thing in {% if features.size > 0 %}.


Nested Loops

You can put loops inside other loops, though this is less common in Cast narrations:


{%- assign categories = "Features,Integrations" | split: "," -%}
{%- assign feature_items = "Analytics,Reporting" | split: "," -%}
{%- assign integration_items = "Salesforce,Slack,Jira" | split: "," -%}

{% for category in categories %}
{{ category }}:
  {% if category == "Features" %}
    {% for item in feature_items %}
      - {{ item }}
    {% endfor %}
  {% else %}
    {% for item in integration_items %}
      - {{ item }}
    {% endfor %}
  {% endif %}
{% endfor %}

⚠️ Keep nesting to one level when possible. Deeply nested loops are hard to read and maintain.


Whitespace Control in Loops

Loops can produce a lot of unwanted blank lines. Use whitespace control (-) to keep output clean:

Without whitespace control:


{% for feature in features %}
  {{ forloop.index }}. {{ feature | strip }}
{% endfor %}

Produces blank lines around each item.

With whitespace control:


{%- for feature in features %}
  {{ forloop.index }}. {{ feature | strip }}
{%- endfor %}

Cleaner output with no extra blank lines.

πŸ’‘ Experiment with whitespace control placement to get the exact spacing you want. The - on {%- for %} strips the blank line before each item; the - on {%- endfor %} strips the blank line after the last item.


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), the loop iterates over individual characters.

βœ… Split first:


{%- assign features = feature_list | split: "," -%}
{% for feature in features %}
  {{ feature | strip }}
{% endfor %}


❌ Forgetting endfor:


{% for feature in features %}
  {{ feature }}

βœ… Every for needs an endfor:


{% for feature in features %}
  {{ feature }}
{% endfor %}


❌ Using the wrong variable name inside the loop:


{% for feature in features %}
  {{ item | strip }}    ← should be "feature", not "item"
{% endfor %}

βœ… Use the variable name you declared in the for tag:


{% for feature in features %}
  {{ feature | strip }}
{% endfor %}


Try It Yourself

Exercise: You have open_tasks with the value "Renew contract,Schedule QBR,Update health score,Review usage data,Prepare expansion proposal". Write Liquid that:

  1. Splits it into an array
  2. Shows the total count of open tasks
  3. Lists the first 3 as a numbered list
  4. If there are more than 3, shows a message β€œβ€¦and X more tasks to address”
  5. If the list is empty, shows β€œNo open tasks β€” great job!”
Click to reveal the answer ```liquid {%- assign tasks = open_tasks | default: "" | split: "," -%} {% if tasks.size > 0 %} You have {{ tasks.size }} open tasks: {%- for task in tasks limit: 3 %} {{ forloop.index }}. {{ task | strip }} {%- endfor %} {% if tasks.size > 3 %} ...and {{ tasks.size | minus: 3 }} more tasks to address. {% endif %} {% else %} No open tasks β€” great job! {% endif %} ``` Output: ``` You have 5 open tasks: 1. Renew contract 2. Schedule QBR 3. Update health score ...and 2 more tasks to address. ``` Key details: - `default: ""` handles a nil field (empty split produces an empty array) - `tasks.size > 0` check wraps the entire loop for the empty case - `limit: 3` restricts the numbered display - `tasks.size | minus: 3` calculates the overflow count

What’s Next

In Module 15, you’ll get a deeper look at assign and capture in the context of building complex, reusable content β€” including patterns for assembling dynamic messages and the variable scoping rules you need to know.


πŸ“– Official documentation:

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

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