HTML templates support a small template language for repeating content: cart rows in an abandoned cart email, product recommendation grids, conditional sections. This reference covers all of it. Single-value personalization is covered separately in the merge tags reference.
Loop over cart items
In automation emails triggered by store events, line_items contains the products from the triggering cart or order:
{% for item in line_items %}
<img src="{{ item.image }}" width="90" alt="">
<p>{{ item.name }} × {{ item.quantity }}</p>
<p>{{ item.line_total }} {{currency}}</p>
{% endfor %}
Available fields on each item:
| Field | Value |
|---|---|
item.name | Product name |
item.quantity | Quantity |
item.price | Unit price |
item.line_total | Price × quantity |
item.url | Product page URL |
item.image | Product image URL |
item.currency | Currency code (from the event or your store settings) |
Missing product images, URLs and currencies are filled in from your synced product catalog automatically when possible.
Loop over your catalog
products contains recent in-stock products from your connected store, for recommendation sections:
{% for item in products limit:4 %}
<a href="{{ item.url }}">
<img src="{{ item.image }}" width="140" alt="">
<p>{{ item.name }}</p>
<p>{{ item.price }}</p>
</a>
{% endfor %}
limit:N caps how many products render. Without it, up to 8 products are used. Catalog items support the same fields as cart items, including item.currency with the product's store currency.
Loop over a product feed (personalized per recipient)
feed.<slug> resolves a dynamic product feed for the recipient at send time: what they viewed, what is in their cart, recommendations, best sellers, or a feed you defined yourself. Wrap it in {% if %} so the heading disappears when the feed is empty:
{% if feed.recently_viewed %}
<h2>Still thinking about these?</h2>
{% for item in feed.recently_viewed limit:4 %}
<a href="{{ item.url }}"><img src="{{ item.image }}" width="140" alt=""></a>
<p>{{ item.name }} - {{ item.price }} {{ item.currency }}</p>
{% endfor %}
{% endif %}
Built-in slugs: recently_viewed, cart, recommended, best_sellers, new_arrivals. Feed items add item.compare_at_price, item.vendor, item.category and item.product_id; the cart feed also has item.quantity and item.line_total. The merge tag menu in the HTML editor inserts these loops for you.
Multilingual store? Items come out in each recipient's language automatically. Add lang:xx to fix one language for a loop, for example {% for item in feed.best_sellers limit:4 lang:et %}. See Dynamic product feeds for how the language is chosen.
Conditional sections
Wrap content in {% if %} so a missing value does not leave a broken layout:
{% if item.image %}
<img src="{{ item.image }}" width="90" alt="">
{% endif %}
Conditions check whether the field has a value. They work on item fields inside loops and on merge tags outside them.
Where this works
- HTML editor templates and automation emails written as HTML
- Templates saved in Templates and picked on a Send Email step
The drag-and-drop block editor has its own product blocks, including a Product feed (dynamic) block, and does not need this syntax.
Previews render with real products
Wherever a template is previewed (the Templates page, the preview dialog, the side panel in the automation builder, the campaign workspace), loops and tags are rendered with sample products from your connected store, so you see the real result instead of raw code. Without a connected store, neutral placeholder products are used.
Common mistakes
{% for item in line_items %}outside a store-triggered automation renders nothing - there is no cart in a newsletter- Typos in
{% endfor %}or{% endif %}leave raw syntax visible in the email; the preview will show this immediately - Use
item.inside loops:{{ name }}alone will not resolve to the product name