Skip to main content
Control flow is written with brace-delimited block tags. They nest freely and all of them are evaluated once, on the server, during the single render pass.

Conditionals

Any number of {else if} branches and at most one {else}. Neither takes a closing tag; {/if} is required. null, false, 0, the empty string, the empty array and the empty object are falsy. An empty array is falsy, which makes {if cart.items} a valid emptiness check. See Truthiness for the rest, including why the string "false" is truthy. Comparisons do not coerce types, so a numeric value that arrived as a string needs | int before comparison. That is covered in full here and is the most common reason an {if} takes the wrong branch.

Loops

{empty} is the branch taken when there is nothing to iterate. It takes no closing tag, and it is optional: without it an empty collection renders nothing at all. To get the index, name it second:
The item comes first and the index second. If you write {each i, product in products} it parses cleanly and binds the names the wrong way round, so product holds 0 and i holds the object. Nothing is reported.

The loop variable

Inside an {each} body, loop is bound automatically. It has exactly four fields:
Nothing else exists on loop. loop.even, loop.odd, loop.length, loop.index0, loop.revindex and loop.parent all resolve to null and render the empty string, so a habit from Jinja or Twig fails silently here. For alternating rows, compare loop.index % 2. In nested loops the inner loop shadows the outer one, and there is no way to reach the outer loop from inside. Name the outer index instead, because a named binding is not shadowed:

Collections that are not arrays

A null or missing collection is empty and takes the {empty} branch:
That holds whether cart.items is [], null, or absent from the context entirely. A misspelled collection name therefore renders the empty state, not an error. An empty state appearing for no reason is often a misspelled name. A value that is neither an array nor null is wrapped and iterated exactly once, with loop.count of 1. So {each} over a single object makes one pass rather than none.

Filtering and ordering

The {each} header accepts loop filters, which run before iteration:
These are a separate set from the value filters, and mixing them up is a build error rather than a silent one. See Filters.

Matching

An arm is a bare pattern, then ->, then a body. _ is the wildcard.
There is no {case} keyword. Writing {case "a"}...{/case} inside a {match} is a parse error, not a variation in style. The arm syntax above is the only one.
Patterns are string, number or boolean literals only. Matching is type-strict, so a "1" pattern never matches a numeric 1, exactly as == behaves. With no matching arm and no _, the block renders nothing at all. Against a status of archived, this produces an empty file:
This matters when the set of values can grow server-side. Add a _ arm unless the silence is intended.

Repeat

The count is any expression, and a float truncates toward zero.
{repeat} does not coerce a string. {repeat "3"} renders nothing, even though "3" - 0 is 3 elsewhere in the language. A count that came from server data needs | int, because counts are exactly the values that arrive as strings.
With order.total of "3":

Fetch

{fetch} declares a data dependency with branches for each state.
The compiler performs no network request, ever. Rendering makes no I/O at all. {fetch} reads the name out of the render context and picks a branch.
Server-side branch selection: On a static build a {fetch} whose name is absent from the globals renders its {loading} branch. That is the skeleton state, and it is also what a typo in the name produces. The branch order is fixed: body, then {loading}, then {error}, then {empty}. Out of order is not recovered and reports as an unclosed block. All four branches beyond the body are optional. Options are cache(n), retry(n), timeout(n), poll(n), paginate(n), infinite(n) and the flag abort-on-navigate. They are recorded for the browser runtime and have no effect on the server render. An unrecognised option is silently dropped, so check the spelling.

{with} does not exist

Twig, Nunjucks and Blade provide a scoping block. Slurp has none.
The lexer knows the word but no parser supports it, so every {with} is a hard build failure, and the four errors after the first are the parser recovering from it. Write the full path instead. Where the repetition is heavy, {each} over a non-array value iterates it exactly once, which gives the shorter name:

The iteration budget

Every loop is capped at 1000 iterations, and a whole render is capped at 1,000,000 across all loops. Exceeding a cap truncates. It never aborts the render, so a page over budget still builds, still exits 0, and simply loses the tail of the list.
Without --verbose that warning is not printed and the build looks entirely clean. slurp validate will not report it either, because validate is a static check and never renders. A truncated loop is only visible by counting the output or by building with -v.
{repeat} clamps to 1000 with no diagnostic at all, in either mode. Check the count when generating a large number of elements from one. Paginate server-side rather than relying on either cap. The budgets bound a hostile or broken template; they are not a paging strategy. The full list is in Limits.

Next

Filters

The loop filters used in {each} headers, and the value filters.

Displaying data

Truthiness and comparison rules.

Block tags

Every tag, with branch order and closing rules.

Common mistakes

The silent failures, collected in one place.