Skip to main content
TOML

Schema 0.3.0 · 7 September 2026

Vazen TOML Specification

This is the data contract. Software packages have their own release versions and schema support.

Introduction

A Vazen TOML file describes one or more retail fixtures and the products laid out on them. It is plain text in TOML 1.0 (opens in a new tab), UTF-8 encoded, with the extension .vazen.toml, and is parsed by the standard TOML library of any language.

A file represents one of two stages of a plan, declared by its schema field:

  • vazen/layout: fully laid out. Every physical element carries a complete position and dimensions, so the file can be rendered, measured or analysed directly.
  • vazen/spec: intent. The same structure, but any position or dimensions may be omitted. A layout engine fills in the missing numbers to produce a layout.

This document defines the shape of the data. How a layout engine turns a spec into a layout, and how a renderer draws one, are left to their implementers.

Structure

A file holds one project. A project holds a flat list of products and a list of fixtures. A fixture holds a tree of equipment, and equipment holds the sites where products are placed:

Project
├── Product (list)          products available to this plan
└── Fixture (list)           a physical unit, e.g. one gondola or end of aisle
    └── Equipment (tree)      typed elements: bay, shelf, peg, ...
        ├── Equipment         equipment can nest, e.g. shelves in a bay
        └── Site (list)       a placement of one product, with facings
  • Product: something that can be placed. Defined once, referenced from any number of sites.
  • Fixture: a self-contained physical unit of shelving.
  • Equipment: the parts a fixture is built from. Each has a type, for example bay or shelf. Equipment may contain further equipment or sites. The set of types is open: consumers must accept values they do not recognise.
  • Site: an instance of a product placed on equipment, with facing counts and, in a layout, a position and dimensions.

Fields

A layout is complete. Every field a layout engine could supply is written, defaults included, so a consumer of a layout never fills anything in. A spec may omit any of them. A few fields are required at both stages because nothing can supply them: the schema, a product’s name, an equipment’s type, and the product a site places. The Required column in each table says which; a blank cell is optional at both stages. Unique means unique among its kind within the file.

A list field (products, fixtures, equipment, sites) may be absent or empty, and the two mean the same.

Any key not listed in the tables below is a descriptive or namespaced attribute; see Attributes.

Project

Field Type Required Notes
schema string always "vazen/spec" or "vazen/layout"
schema_version string always The version of this specification the file follows, as a Semantic Versioning string, e.g. "0.3.0"
name string Project name
products array See Product
fixtures array See Fixture

Product

Field Type Required Notes
ref string layout Short for reference: an opaque string for use in selectors. Unique. See Product selectors
name string always The short human-readable label, as on a shelf edge label (GS1 descriptionShort)
description string The long description, where one exists (GS1 tradeItemDescription)
gtin string A string of 8, 12, 13 or 14 digits. Not required to be unique within a file: a shelf test can hold two variants of one GTIN
dimensions table layout Physical dimensions of the product in mm: { width, height, depth }. Not the pack size. Required in a layout only for a product a site selects
form string What kind of object this is, e.g. "tray", "case", "display". Open vocabulary, descriptive only. See Packaging levels
images table Images of the product, keyed by face: front, back, left, right, top, base, as defined under Orientation. Each value is either a URL string or a table with a url key and any further keys, e.g. images = { front = "https://…" } or images = { front = { url = "https://…", crop = { x = 200 } } }. An image shows its face head-on and upright

Fixture

Field Type Required Notes
id string layout A stable identifier, e.g. a plan reference or a retailer’s fixture code. Unique. A layout engine assigns one to any fixture that arrives without
name string Display name
dimensions table layout { width, height, depth }, mm
sort_order number See Ordering
equipment array See Equipment

Equipment

Field Type Required Notes
type string always e.g. "bay", "shelf", "peg". Open set; do not reject unknown values
sort_order number See Ordering
position table layout { x, y, z } in the fixture’s frame, mm
dimensions table layout { width, height, depth }, mm
equipment array Child equipment
sites array Product placements

Site

Field Type Required Notes
product table always A selector naming the product placed. See Product selectors
orientation string layout How the product faces the shopper. "F" is front-facing and upright. Defaults to "F" in a spec. See Orientation
facings table layout { wide, high, deep }: how many facings across, up and back. Each defaults to 1 in a spec. Must be greater than zero; fractional values are legal
sort_order number See Ordering
position table layout { x, y, z } in the fixture’s frame, mm
dimensions table layout { width, height, depth } of the space allocated to the site, mm. Never smaller than the facings block; see below

A site’s dimensions is the space allocated to the placement. The facings occupy the facings block defined under Orientation; the allocation is at least that block on every axis, and is often exactly it. It may be larger: planning tools commonly spread a product’s facings evenly across a wider space, or hold space open beside them, and in files converted from those tools the allocation records that decision while the facings record the count. How facings are distributed within a larger allocation is not yet specified; until it is, consumers should treat the block as contiguous and aligned to the allocation’s front-bottom-left corner unless a namespaced attribute says otherwise. Treat dimensions as authoritative for space and the facings as authoritative for count; a consumer that needs only one should prefer dimensions.

Product selectors

A site names its product with a selector: a small table of fields that the product must match. To resolve a selector, find the product in the file’s product list whose fields match every key given. A selector must resolve to exactly one product; zero or several matches is an error, not a fallback.

Any product field can appear in a selector. Any key on the product may be used, standard or descriptive, provided its value is a scalar. Values match by equality; a table-valued field such as dimensions cannot be selected on. This is how specs are naturally written, naming products by identifiers the author already holds, and adding a field wherever one is not enough on its own:

product = { gtin = "5000000000012" }
product = { gtin = "5000000000012", form = "tray" }

ref (short for reference) is the special case. It is an opaque string a producer attaches to a product, unique within the file, so that a selector on it always resolves without further thought:

product = { ref = "p_9f3k2v" }

A selector on ref is resolved by the same matching rule as any other; nothing about it is different to a consumer except that it is guaranteed to be unique. Files produced by software commonly use it, since the producer has already resolved the product and need not rebuild a meaningful selector. A ref is valid only within the file that carries it and may differ between two files describing the same plan. For identity across files, match on durable fields. Where a file contains products sharing a GTIN, a producer ensures they differ in another field, such as name or form.

A ref carries no meaning: it is a string the producer made up, from a hash, a counter or anything else, and nothing about the product can be read from it. Nor does it last: a file written again may carry different refs. So a ref serves selectors in its own file and nothing else; a tool that needs to find a product later keeps the product’s durable fields, and matches on those.

Attributes

Every object in a file (project, product, fixture, equipment, site) carries its keys at the top level, and those keys are of three kinds.

Standard keys are the ones the field tables above define. They carry the semantics a validator or layout engine acts on, and the specification is their only source.

Descriptive keys are any other plain key. They belong to the producer of the file and mean whatever the producer says: brand = "Example Brand", supplier_code = "SUP-4471", store_cluster = "urban-small". They carry no semantics in this specification, and there is deliberately no list of them: product and fixture master data is unbounded and different in every organisation. Write them in lowercase snake_case, as the standard keys are. Values are TOML scalars or arrays of scalars.

Namespaced keys follow the pattern _namespace__key: a single leading underscore, the namespace, a double underscore, the key. They are for data whose meaning a tool owns and depends on, and that must not be confused with anyone else’s: values derived by a layout engine, fields preserved for a round trip to another format, conventions shared between tools. The test is simple: if another tool has to understand this key exactly, namespace it; if it is simply your data, do not.

A namespace is the full domain you control, including the top-level domain, in lowercase, with dots replaced by underscores: vazen.com gives _vazen_com, example.org gives _example_org, so a supplier code owned by example.org is _example_org__supplier_code. A department of a large organisation may add itself as another segment, for example _example_org_retail_lab. This keeps DNS uniqueness without a central register. Never a generic word: _data, _custom, _ext and _x collide immediately and say nothing.

Two short namespaces are reserved and do not follow the domain rule: _vazen for this specification’s own tooling, and _psa for the .psa importer published here. A source format whose importer this specification publishes takes the format’s name. _vazen__unit_count, _psa__custom_attribute_9.

Consumers honour standard keys, may ignore descriptive keys, ignore namespaced keys they do not recognise, and, if they write a file back out, preserve every key they do not understand, plain or namespaced. That last rule is what makes round trips work.

There is no registry. A list of namespaces that published tools emit, with links to what their keys mean, will be kept on this site; publishing yours there is how others learn what your keys mean.

Descriptive keys in wide and consistent use may be promoted to standard keys in a later version; see Versioning.

A product is often placed as something other than a loose unit: a shelf-ready tray, a case, a display unit. Each is a different physical object with its own dimensions and appearance, so each is its own entry in products. A tray and the unit inside it are two products, and a site references whichever actually sits on the shelf. Every product is self-describing: its name, identifiers, dimensions and images are on the entry itself, so a consumer never resolves one product in order to draw or list another.

form says what kind of object a product is: "tray", "case", "display", or any other name. The vocabulary is open and the field is descriptive only; no rules attach to it. It also makes a clean selector where a tray shares its unit’s GTIN: { gtin = "5000000000012", form = "tray" }.

Relationships between products are not first-class fields in this version. Grouping works as it does for any attribute: products that belong together carry a common value, a brand or a family key, and consumers group by equality. Quantitative relationships, such as how many units a tray holds, are expressed by producers in their own namespace until a convention proves broadly useful and is promoted to a field. Vazen’s producers write _vazen__group on every product in a family and _vazen__unit_count on each packaging level, giving the number of units inside.

How the units within a site are arranged, loosely piled or stacked like logs, is likewise not yet specified. A later version will add fill rules at the site level.

Units and coordinates

All dimensions and coordinates are millimetres, written as numbers. There is no unit setting: a number is always a millimetre. Geometry appears as two inline tables on each physical element:

position = { x = 0, y = 0, z = 0 }
dimensions = { width = 0, height = 0, depth = 0 }

Every position, equipment and sites alike, is absolute in its fixture’s frame: a single origin at the fixture’s front-bottom-left corner.

  • x increases left to right
  • y increases bottom to top
  • z increases front to back

width spans x, height spans y, depth spans z. Every dimension is strictly positive: a zero or negative width, height or depth is invalid.

Directions are given for a shopper standing in front of the fixture: x runs to their right, y up, z away from them into the fixture. These axes are left-handed. A right-handed engine (three.js, Blender, Unity) given the values verbatim will show the scene mirrored, as though viewed from behind. To view from the front, negate z on import (z_engine = -z_file) and leave x and y unchanged.

An element’s dimensions is its bounding box, including the physical element itself. A shelf’s box spans from beneath its steel up to the space it governs, so sibling shelves tile their bay exactly, with no gap between them. A site’s position.y is the height of its product-bearing surface.

In a vazen/spec file, any position or dimensions may be omitted. In a vazen/layout file, the bounding boxes of sibling elements never overlap.

Ordering

Sibling fixtures, equipment and sites each carry an optional sort_order number. Siblings read in ascending sort_order: left to right along a shelf, bottom to top within a bay. Values need not be consecutive; inserting between 1 and 2 at 1.5 is legal and expected. Producers write arrays already in sort_order.

In a spec, sort_order is authoritative for reading order. In a layout, geometry is authoritative, and any sort_order present must agree with it. Disagreement is a validation error, not a tie to be broken.

Orientation

orientation names the face of the product cuboid turned towards the shopper, and a rotation. It is one of 24 codes: F, Fl, Fr, Fu, B, Bl, Br, Bu, L, Ll, Lr, Lu, R, Rl, Rr, Ru, U, Ul, Ur, Uu, D, Dl, Dr, Du. The capital letter is the face shown: Front, Back, Left, Right, Up (the top) and Down (the base). The lowercase letter is a twist of that face as the shopper sees it: l turns the top of the shown face to the shopper’s left, r to their right, u turns it upside down. A product’s faces are named as it stands front-facing and upright, so its left side is the side on the shopper’s left. For "F" the twist is the side the product comes to rest on; the Name column gives what it rests on for every code. "F" is a front-facing upright product. In a spec it is the default and may be omitted; in a layout it is written like any other value.

The codes read like the moves of a Rubik’s Cube: a cuboid has exactly 24 distinct orientations, each named by the face it shows and the twist applied to it.

A product’s dimensions gives its width, height and depth as it stands front-facing and upright ("F"): width along x, height along y, depth along z. Every other orientation is a rotation of that cuboid, so each product dimension ends up along a different axis. The table gives each code’s name and which product dimension lies along x, y and z. Names follow one rule: the face towards the shopper, then how the product rests. A product rests on its base (upright), its top (upside down), its front (face down), its back (on its back), or its left or right side. Faces and sides are the product’s own, named from the front-facing upright position, and travel with it as it turns. The facings block is the mapped x dimension × facings.wide, the mapped y dimension × facings.high, the mapped z dimension × facings.deep.

Rotating a product onto its left or right side (l, r) swaps the dimensions along x and y; the two codes differ only in which way the artwork turns, not in the space occupied. Turning it upside-down (u) leaves the dimensions unchanged. For the U and D faces the product lies flat, so its depth runs along y: with U shown, upright means the product’s front is toward the bottom; with D shown, its back is.

Code Name Along x Along y Along z
F Front out, upright width height depth
Fl Front out, on its left side height width depth
Fr Front out, on its right side height width depth
Fu Front out, upside down width height depth
B Back out, upright width height depth
Bl Back out, on its right side height width depth
Br Back out, on its left side height width depth
Bu Back out, upside down width height depth
L Left side out, upright depth height width
Ll Left side out, on its back height depth width
Lr Left side out, face down height depth width
Lu Left side out, upside down depth height width
R Right side out, upright depth height width
Rl Right side out, face down height depth width
Rr Right side out, on its back height depth width
Ru Right side out, upside down depth height width
U Top out, face down width depth height
Ul Top out, on its left side depth width height
Ur Top out, on its right side depth width height
Uu Top out, on its back width depth height
D Base out, on its back width depth height
Dl Base out, on its left side depth width height
Dr Base out, on its right side depth width height
Du Base out, face down width depth height

Example

A layout-stage file describing an end of aisle with two shelves. Deleting the sites’ position and dimensions lines and setting schema = "vazen/spec" yields the corresponding spec.

schema = "vazen/layout"
schema_version = "0.3.0"
name = "Dishwash End of Aisle"

[[products]]
ref = "p_9f3k2v"
name = "Dishwasher Tablets 50 Pack"
gtin = "5000000000012"
dimensions = { width = 190, height = 240, depth = 120 }
_vazen__group = "dishwasher-tablets-50"

[[products]]
ref = "p_4t8rbn"
name = "Dishwasher Tablets 50 Pack, tray of 4"
gtin = "15000000000019"
form = "tray"
dimensions = { width = 385, height = 250, depth = 250 }
_vazen__group = "dishwasher-tablets-50"
_vazen__unit_count = 4

[[products]]
ref = "p_2m8qxz"
name = "Dishwasher Tablets 70 Pack"
gtin = "5000000000029"
dimensions = { width = 210, height = 260, depth = 130 }

[[fixtures]]
id = "AISLE-12-END"
name = "Aisle 12 End"
dimensions = { width = 1250, height = 2000, depth = 570 }

[[fixtures.equipment]]
type = "bay"
sort_order = 1
position = { x = 0, y = 0, z = 0 }
dimensions = { width = 1250, height = 2000, depth = 570 }

[[fixtures.equipment.equipment]]
type = "shelf"
sort_order = 1
position = { x = 0, y = 260, z = 0 }
dimensions = { width = 1250, height = 400, depth = 570 }

[[fixtures.equipment.equipment.sites]]
product = { ref = "p_4t8rbn" }
facings = { wide = 2, high = 1, deep = 2 }
orientation = "F"
sort_order = 1
position = { x = 25, y = 300, z = 0 }
dimensions = { width = 770, height = 250, depth = 500 }

[[fixtures.equipment.equipment]]
type = "shelf"
sort_order = 2
position = { x = 0, y = 660, z = 0 }
dimensions = { width = 1250, height = 400, depth = 570 }

[[fixtures.equipment.equipment.sites]]
product = { ref = "p_2m8qxz" }
facings = { wide = 3, high = 1, deep = 4 }
orientation = "F"
sort_order = 1
position = { x = 25, y = 700, z = 0 }
dimensions = { width = 630, height = 260, depth = 520 }

Reading it: one bay fills the fixture. Two adjacent shelves occupy y = 260 to y = 1060; each shelf’s box starts beneath its steel, and the product surface sits 40mm above that. The first shelf holds two trays of the 50 pack, two deep: four trays, sixteen units. The second holds three facings of the 70 pack, four deep.

JSON

A file’s tables may be written as JSON, with the extension .vazen.json. The JSON has the same keys and the same values. Each table is an object, and each array is an array. JSON suits a program that uses the data, or an API that passes it on. TOML suits a file that people keep and edit, because JSON has no comments and no dates. A date is written as RFC 3339 text with milliseconds: "2026-09-27T09:30:00.000Z" or "2026-09-27T09:30:00.000+01:00" for an offset date-time, "2026-09-27T09:30:00.000" for a local date-time, "2026-09-27" for a local date, and "09:30:00.000" for a local time. Text in exactly one of these forms reads as a date where a date can be: in an attribute or a product selector. Any other text is text, and so is the text of a standard key. JSON’s null has no TOML equivalent, so a file that holds one is invalid. TOML’s nan and inf have no JSON equivalent, so a file that holds either cannot be written as JSON.

Versioning

Every file declares the schema_version it follows. Versions follow Semantic Versioning. While the major version is 0, a minor bump may change field names and structure. Changes are recorded against each version, and the version field lets a consumer handle files from different versions explicitly.

Standard keys are added only at a version bump. A key promoted from descriptive use takes the meaning already prevailing in real files; where usage conflicts, the specification chooses a different name rather than redefining one in use. A file’s schema_version therefore fixes which of its keys are standard: a descriptive key in a 0.3.0 file stays descriptive whatever a later version defines.

Version history:

  • 0.3.0: presentations removed, packaging levels are products with a form field; three kinds of key (standard, descriptive, namespaced) and a Required column in the field tables; facings and dimensions as tables; images on products; facing counts and orientation written in layouts; site dimensions may exceed the facings block; single-fixture shorthand removed; orientation dimension table; ordering rule between sort_order and geometry; ref validity clarified.
  • 0.2.0: spec and layout stages; presentations; site orientation; explicit ordering; fixture identifiers; schema_version replaces vazen_version.
  • 0.1.0: initial specification.

Feedback

This specification is shaped by the people using it. If you are working with planogram data and something here is unclear, missing or awkward for your pipeline, write to toml@vazen.com. We are particularly keen to hear from engineers integrating shelf data, researchers working with space and range data, and anyone building tools that read or write the format.

Why these design choices?