Nested Loops

A nested loop is a loop inside another loop — for example a list of asset categories where each category contains a list of assets. AdviceDocs walks each level, records the full path to every field, and tracks the relationship between the outer and inner collections so that the field configuration UI shows them as a clean parent / child / grandchild tree.

How to write nested loops

Two levels

Open the outer loop, open the inner loop, write the placeholders, then close the inner loop and the outer loop. Indent so each opening tag has a matching closing tag at the same level.

Two-level nested loop

{% for category in categories %}

### {{ category.name }}

{% for asset in category.assets %}

- {{ asset.name }}: {{ asset.value }}

{% endfor %}

{% endfor %}

Three or more levels

There is no fixed depth limit. Each level adds another {% endfor %} at the bottom and another segment to the dot-path that AdviceDocs records.

Three-level nested loop

{% for client in clients %}

{% for account in client.accounts %}

{% for transaction in account.transactions %}

{{ transaction.date }} — {{ transaction.amount }}

{% endfor %}

{% endfor %}

{% endfor %}

Mixing with conditionals

You can wrap an inner loop in an {% if %} block. Fields inside the inner loop accumulate both the outer collection path and the condition dependency, so they only appear when the condition is true.

Conditional inner loop

{% for client in clients %}

{% if client.has_super %}

{% for fund in client.super_funds %}

- {{ fund.name }} ({{ fund.balance }})

{% endfor %}

{% endif %}

{% endfor %}

Nested loops and table rows

If the inner loop produces table rows, use {% tr for %} at the row level — not plain {% for %} — so Word’s row XML stays intact. A regular outer loop wrapped around a table-row loop works fine.

Outer for, inner tr for

{% for category in categories %}

**{{ category.name }}**

Asset
Value
{% tr for asset in category.assets %}
{{ asset.name }}
{{ asset.value }}
{% endtr %}

{% endfor %}

How nested fields are recorded

At parse time AdviceDocs keeps a stack of active loop variables. As each loop opens, it pushes the variable name and the collection it iterates over. When a field reference starts with one of those variables, it is rewritten to the full collection path.

For the two-level example above, three fields are recorded:

Recorded fields

categories (is_array = true)

categories.assets (is_array = true)

categories.assets.name (scalar leaf)

categories.assets.value (scalar leaf)

The dot-notation path is what shows up in the field configuration tree. Each parent appears as an array; each leaf inherits its type from the standard inference rules (currency, date, etc.).

Loop variable scoping

Standard Jinja2 scoping applies:

  • The inner loop can reference the outer loop’s variable. Inside the inner block, both category and asset are visible.
  • The outer loop variable is not visible after the outer loop ends.
  • Both loop.index, loop.first, etc. refer to the nearest enclosing loop. To reference the outer loop counter from inside an inner loop, capture it before opening the inner loop, or use {% set %}... except — AdviceDocs does not support {% set %}. Workaround: include the outer index in the data shape so it’s available as a field.

Don't shadow loop variables

Re-using a loop variable name at multiple depths (e.g. {% for product in products %}{% for product in product.alternatives %}) is allowed by Jinja2 but silently confusing. Once the inner loop opens, the outer product is no longer reachable. Use distinct names: {% for product in products %}{% for alt in product.alternatives %}.

Configuration after upload

Each level shows up in the field tree under its parent. You can give the parents descriptions, set types on the leaves (currency, date, etc.), and attach condition dependencies. There are no nested-loop-specific settings — the loop is purely a template-time construct.

Common pitfalls

Match every endfor to its for

Each {% for %} must close with its own {% endfor %}. Each {% tr for %} must close with {% endtr %}. Crossed or missing closes are syntax errors that block the field list from generating.

Plain {% for %} doesn't work for nested table rows

Inside a Word table, the inner row loop must use {% tr for %}, not plain {% for %}. The outer wrapper can be either, depending on whether it spans rows or paragraphs.

Loop variable typo warnings work across nesting

AdviceDocs warns if a field reference inside a loop uses a name that looks likethe loop variable but isn’t. The same check runs at every depth, so a typo deep inside a three-level nest still produces a useful suggestion.

No depth limit, but watch performance

There is no built-in cap on how deep nested loops can go. In practice, more than three levels is hard to reason about and slows rendering on large datasets. Flatten or pre-process the data when you can.

Syntax to copy

Two levels

{% for category in categories %}

{{ category.name }}

{% for asset in category.assets %}

- {{ asset.name }}: {{ asset.value }}

{% endfor %}

{% endfor %}

Three levels

{% for client in clients %}

{% for account in client.accounts %}

{% for transaction in account.transactions %}

{{ transaction.date }} — {{ transaction.amount }}

{% endfor %}

{% endfor %}

{% endfor %}

Outer loop, inner table-row loop

{% for category in categories %}

**{{ category.name }}**

{% tr for asset in category.assets %}
{{ asset.name }}
{{ asset.value }}
{% endtr %}

{% endfor %}

Conditional inner loop

{% for client in clients %}

{% if client.has_super %}

{% for fund in client.super_funds %}

- {{ fund.name }}: {{ fund.balance }}

{% endfor %}

{% endif %}

{% endfor %}