Skip to content

The data in a template

What record and variants contain, how to read an attribute by its code, a measurement set and its unit, a product's categories, and which page covers each of the other value types.

What a template can read, and how to read a plain attribute. For the syntax see template language basics; for the functions, the function reference.

Not sure what is available in your account? debug.dump prints the whole data structure.

record The current record — a product, or one custom entity record for a custom entity feed. In the record template and in the Text template node.
variants Every variant of the current product. Only in the record template.
sources The inputs of a field processing pipeline. Only inside the Text template node, not in a channel template.

Header and footer templates can use only the Scriban built-ins. The three variables above and the heavendata functions — export.*, asset.*, i18n.* and debug.* — are not available there.

To group variants — the usual case being color, with sizes underneath — use export.variants_by.

Products and their variants can belong in categories. The categories a product is in are in a list on record._categories, and per variant as variant._categories. If the product has no categories, the list is empty. What a category key and the sort index are, and how products get their categories: Categories.

{{ for c in record._categories }}
<category key="{{ c.key }}" sort="{{ c.sort_index }}">{{ c.name }}</category>
{{ end }}

The underscore is important, it exists to differentiate the product’s categories from a possible attribute named categories.

Each entry is one category:

Property Description
key The category key — the identifier the category is known by for data exchange. It’s set under Settings → Categories or by an import; see Setting a category key. Missing when the category has no key.
name The category name
sort_index The category’s place among all your categories, read from top to bottom, counted from 1. No two categories share a number.
parent_key The parent category’s key. Missing for a category tree’s own top-level category, and when the parent has no key.
path The category and every category above it, the category tree’s top-level category first — each with key, name and sort_index, and nothing else

key and parent_key are left out rather than set to an empty text, so you can test using {{ if c.key }}.

Only the categories a product is actually assigned to are listed, each once. The parent categories don’t appear unless the product is directly assigned to the parent categories as well. Use the path field to walk through a category’s parents.

A category deleted after a product was assigned to it is simply absent from the list; it is not an error and not an empty entry.

The examples below are deliberately bare, because the Try it links run them in Scriban’s own playground, which has none of our functions. A category can be called Shoes & Boots, so in a real template pass every value through export.xmlize.

variant._categories is the variant’s own categories plus everything it inherits from the product and from any variant above it, in the same tree order. record._categories is the product’s:

{{ for variant in variants }}
<variant ean="{{ variant.ean }}">
{{- for c in variant._categories }}
<category key="{{ c.key }}">{{ c.name }}</category>
{{- end }}
</variant>
{{ end }}

The product is in Sale; the variant adds Shirts, and its list carries both.

Most receiving systems want a breadcrumb rather than a single name. Map the path to its names and join them:

{{ for c in record._categories }}
<category>{{ c.path | array.map "name" | array.join "//" }}</category>
{{ end }}

A product assigned to Shirts under Clothing gives Clothing//Shirts. Use array.map "key" instead for a path of keys, and any separator you like as the second argument of array.join. A category with no key is left out of a path of keys, not left empty, so the result has fewer levels than the path, and nothing in it shows which one is missing: Shirts with the key shirts, under a Clothing that has no key, gives just shirts.

The entries already come in the order the app shows: a category before its subcategories, sibling categories in the order they sit in. That is ascending sort_index order all the way down, so writing sort_index out, as the first example does, is all a receiving system needs to reproduce it.

sort_index is the category’s place across all your category trees, read from top to bottom, counted from 1 — and no two categories share a number. So a receiving system that holds categories as a flat list can sort them by it and get the order the app shows back — a category before its subcategories. (To rebuild the nesting itself, rather than just the order, use parent_key or path.) In the example above Clothing is 1, Shirts — under it — is 2, and Sale, another category tree that comes after Clothing, is 3.

The numbers are positions the PIM maintains, not the numbering a source system sent. An import applies the order it is given and then keeps its own, so a category dragged in the app gets a new sort_index and an imported one will not match the number in the source.

Sort indexes shift when a category tree changes. Inserting a category renumbers every category after it in reading order, because the numbers have to stay in that order. They order categories; they do not identify them — that is what key is for.

Is the product in one particular category?

Section titled “Is the product in one particular category?”
{{ if record._categories | array.map "key" | array.contains "sale" }}
<sale>true</sale>
{{ end }}

This tests the categories the product is assigned to. To match a category anywhere above it as well, test the path of each entry instead.

A Measurement set attribute holds two to four measurements in one unit — a product’s or a package’s dimensions, for example length, width and height in cm (Measurements and units). In a template it is an object with one member per measurement code, each a number in the attribute’s unit. Printed whole, record.‹code› gives the text the CSV and Excel formats write, the unit once at the end: 20 x 10 x 5 cm.

{{ if record.package_size }}
<dimensions>{{ record.package_size }}</dimensions>
<length>{{ record.package_size.length }}</length>
<width>{{ record.package_size.width }}</width>
<height>{{ record.package_size.height }}</height>
{{ end }}

The if matters: on a product with no value, record.package_size does not exist and reading .length from it stops the run (see reading errors). A measurement left empty is simply missing from the object, so its member renders nothing, and printed whole the set keeps its place empty — 20 x x 5 cm.

The unit and the measurements are on the attribute, not on the value. Read them from export.attribute:

Expression Gives
export.attribute.package_size.configuration.unit The unit every measurement is in, by its name in the unit list — Length.Centimeter, not cm. export.convert_unit accepts it as a unit.
export.attribute.package_size.configuration.measurements The measurements, in the attribute’s order, each with a code and the label the app shows

configuration.unit is a name, not a symbol, so it is not what a receiving system wants to read. To write a unit, convert to that unit and write its symbol yourself — the output stays in that unit even if the attribute’s unit is changed later. Changing an attribute’s unit converts none of its stored values, though: after such a change, the numbers already entered are read in the new unit. Each measurement with its label, in millimeters:

{{ if record.package_size }}
{{ for m in export.attribute.package_size.configuration.measurements }}
<dimension name="{{ m.label | export.xmlize }}" unit="mm">{{ export.convert_unit(record.package_size[m.code], export.attribute.package_size.configuration.unit, 'mm') }}</dimension>
{{ end }}
{{ end }}

For a set of 20 x – x 5.5 cm this gives 200 for Length, an empty Width and 55 for Height.

An Integer number or Number with decimals attribute with a unit is still a plain number in a template, in that unit, and its unit is read the same way: export.attribute.weight.configuration.unit.

Each needs a function to read, and each has its own page covering the attribute and the functions together:

Attribute type Read it with Page
Translatable text i18n.t Translatable attributes in a template
Assets — images, PDFs, files asset.url Assets in a template
References to custom entity records export.load_custom_entities Custom entities in a template

export.attribute gives access to every configured attribute. (The channel’s languages — export.culture_codes — are on the translatable attributes page.)

Property Description
id Internal attribute id
name Attribute name, as shown in the app
required True if a value is required
translatable True if the value is language-specific
labels Label translations, keyed by language code
configuration The settings of the attribute’s type. For a measurement set, unit and measurements; for a number with a unit, unit — see Measurement sets
{{ export.attribute.product_name.name }}
{{ export.attribute['my-attribute'].name }}

For a translated label use export.attr_label.