Module 14: Loops — for / endfor
What Is a Loop?
Section titled “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. Aforloop does the same thing — it takes one template and applies it to every item automatically.
Why it matters in Cast
Section titled “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
Section titled “Basic for Loop”Liquid syntax
Section titled “Liquid syntax”{% raw %}{% for item in array %} {{ item }}{% endfor %}{% endraw %}itemis a temporary variable name — you choose it. It holds the current item on each pass through the loop.arrayis the array to loop through. {% raw %}- Everything between{% for %}and{% endfor %}is repeated once per item.{% endraw %}
Real Cast example
Section titled “Real Cast example”{% raw %}{%- assign features = feature_list | split: "," -%}
Your active features:{% for feature in features %} - {{ feature | strip }}{% endfor %}{% endraw %}If feature_list is "Analytics,Reporting,Alerts,Dashboards":
{% raw %}Your active features: - Analytics - Reporting - Alerts - Dashboards{% endraw %}Numbered Lists with forloop.index
Section titled “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
Section titled “Real Cast example — numbered list”{% raw %}{%- assign features = feature_list | split: "," -%}
Your top features this quarter:{% for feature in features %} {{ forloop.index }}. {{ feature | strip }}{% endfor %}{% endraw %}Output:
{% raw %}Your top features this quarter: 1. Analytics 2. Reporting 3. Alerts 4. Dashboards{% endraw %}Real Cast example — comma-separated with “and” before the last item
Section titled “Real Cast example — comma-separated with “and” before the last item”{% raw %}{%- 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 -%}.{% endraw %}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
Section titled “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.
{% raw %}{% for feature in features limit: 3 %} {{ forloop.index }}. {{ feature | strip }}{% endfor %}{% endraw %}Even if features has 10 items, only the first 3 are shown.
Real Cast example — top 3 with overflow count
Section titled “Real Cast example — top 3 with overflow count”{% raw %}{%- assign features = feature_list | split: "," -%}
Your top features this quarter:{% for feature in features limit: 3 %} {{ forloop.index }}. {{ feature | strip }}{% endfor %}
...and {{ features.size | minus: 3 }} more.
{% endraw %}Output (for 6 features):
{% raw %}Your top features this quarter: 1. Analytics 2. Reporting 3. Alerts
...and 3 more.{% endraw %}Skipping Items with offset
Section titled “Skipping Items with offset”The offset parameter skips a specified number of items from the beginning:
{% raw %}{% for feature in features offset: 2 %} {{ feature | strip }}{% endfor %}{% endraw %}This skips the first 2 items and starts from the third.
Combining limit and offset
Section titled “Combining limit and offset”You can use both together to get a “slice” of the array:
{% raw %}{% for feature in features limit: 3 offset: 2 %} {{ feature | strip }}{% endfor %}{% endraw %}This skips 2 items, then shows the next 3 — items at positions 3, 4, and 5.
Conditional Content Inside Loops
Section titled “Conditional Content Inside Loops”You can use if/else inside loops to customize each item’s display based on its value or position:
{% raw %}{%- assign tasks = open_tasks | split: "," -%}
{% for task in tasks %} {%- assign clean_task = task | strip -%}
🔴 Priority: {{ clean_task }} {% else %} ⚪ {{ clean_task }}
{% endfor %}{% endraw %}Output:
{% raw %} 🔴 Priority: Renew contract ⚪ Schedule QBR ⚪ Update health score{% endraw %}Empty Arrays — The for/else Pattern
Section titled “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:
{% raw %}{%- assign features = feature_list | default: "" | split: "," -%}
{% for feature in features %} {{ forloop.index }}. {{ feature | strip }}{% else %} No features are currently active on your account.{% endfor %}{% endraw %}If feature_list is empty, the else content is shown instead of the loop body.
{% raw %}💡 This is cleaner than wrapping the whole thing in {% if features.size > 0 %}.{% endraw %}
Nested Loops
Section titled “Nested Loops”You can put loops inside other loops, though this is less common in Cast narrations:
{% raw %}{%- assign categories = "Features,Integrations" | split: "," -%}{%- assign feature_items = "Analytics,Reporting" | split: "," -%}{%- assign integration_items = "Salesforce,Slack,Jira" | split: "," -%}
{% for category in categories %}{{ category }}:
{% for item in feature_items %} - {{ item }} {% endfor %} {% else %} {% for item in integration_items %} - {{ item }} {% endfor %}
{% endfor %}{% endraw %}⚠️ Keep nesting to one level when possible. Deeply nested loops are hard to read and maintain.
Whitespace Control in Loops
Section titled “Whitespace Control in Loops”Loops can produce a lot of unwanted blank lines. Use whitespace control (-) to keep output clean:
Without whitespace control:
{% raw %}{% for feature in features %} {{ forloop.index }}. {{ feature | strip }}{% endfor %}{% endraw %}Produces blank lines around each item.
With whitespace control:
{% raw %}{%- for feature in features %} {{ forloop.index }}. {{ feature | strip }}{%- endfor %}{% endraw %}Cleaner output with no extra blank lines.
{% raw %}💡 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.{% endraw %}
Common Mistakes
Section titled “Common Mistakes”❌ Looping over a string instead of an array:
{% raw %}{% for item in feature_list %} {{ item }}{% endfor %}{% endraw %}If feature_list is a comma-separated string (not an array), the loop iterates over individual characters.
✅ Split first:
{% raw %}{%- assign features = feature_list | split: "," -%}{% for feature in features %} {{ feature | strip }}{% endfor %}{% endraw %}❌ Forgetting endfor:
{% raw %}{% for feature in features %} {{ feature }}{% endraw %}✅ Every for needs an endfor:
{% raw %}{% for feature in features %} {{ feature }}{% endfor %}{% endraw %}❌ Using the wrong variable name inside the loop:
{% raw %}{% for feature in features %} {{ item | strip }} ← should be "feature", not "item"{% endfor %}{% endraw %}✅ Use the variable name you declared in the for tag:
{% raw %}{% for feature in features %} {{ feature | strip }}{% endfor %}{% endraw %}Try It Yourself
Section titled “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:
- Splits it into an array
- Shows the total count of open tasks
- Lists the first 3 as a numbered list
- If there are more than 3, shows a message “…and X more tasks to address”
- If the list is empty, shows “No open tasks — great job!”
Click to reveal the answer
{% raw %}{%- assign tasks = open_tasks | default: "" | split: "," -%}
You have {{ tasks.size }} open tasks: {%- for task in tasks limit: 3 %} {{ forloop.index }}. {{ task | strip }} {%- endfor %}
...and {{ tasks.size | minus: 3 }} more tasks to address.
{% else %} No open tasks — great job!
{% endraw %}Output:
{% raw %} You have 5 open tasks: 1. Renew contract 2. Schedule QBR 3. Update health score ...and 2 more tasks to address.{% endraw %}Key details:
default: ""handles a nil field (empty split produces an empty array)tasks.size > 0check wraps the entire loop for the empty caselimit: 3restricts the numbered displaytasks.size | minus: 3calculates the overflow count
What’s Next
Section titled “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