# Vazen TOML > Vazen TOML is an open file format for planogram data: the products, fixtures, equipment and product placements of a retail shelf, written as plain TOML 1.0 with the extension `.vazen.toml`. Any standard TOML parser reads a Vazen TOML file. A file declares one of two stages of a plan in its `schema` field: `vazen/layout`, where every physical element has a position and dimensions, or `vazen/spec`, where they may be left for a layout engine to fill in. Vazen maintains the format. The specification is licensed under CC BY 4.0 and the software under Apache 2.0. The JavaScript/TypeScript package, `@vazen/toml`, is on npm. It reads and writes Vazen TOML files as TOML or JSON. It also reads and writes ProSpace PSA planogram files from JDA and Blue Yonder Space Planning, which start with the line `PROSPACE SCHEMATIC FILE`. A Python implementation and a web converter and visualiser are planned. ## Vazen TOML: an open format for planograms Source: /index.md An open format for planograms # Free your shelf data. You shouldn’t have to pay a software vendor to use your planograms. Vazen TOML puts you back in control of your planogram data, with one open format that lets any new tool, partner or AI use your plans correctly from day one. It works with your [legacy systems](/psa) too. [Explore the format](/docs#example-shelf)[Why Vazen TOML?](/why) ## A complete shelf plan in a few lines Two cereal products share one shelf, with three facings each and four packs deep. This is the complete spec file that describes the plan. 2 products · 6 facings · 24 units Example shelfvazen/spec · 0.3.0 ``` schema = "vazen/spec" schema_version = "0.3.0" name = "Example shelf" [[products]] name = "Oat cereal" [[products]] name = "Cocoa cereal" [[fixtures]] [[fixtures.equipment]] type = "shelf" [[fixtures.equipment.sites]] product = { name = "Oat cereal" } facings = { wide = 3, deep = 4 } [[fixtures.equipment.sites]] product = { name = "Cocoa cereal" } facings = { wide = 3, deep = 4 } ``` A spec describes the products and facings, leaving exact positions and dimensions open. The illustration shows one possible layout.[Read the example](/docs#example-shelf) Why Vazen TOML? ## Tools that work together Retailers and brands should be able to choose their software without starting again with their shelf data. A shared format gives planning, analysis, simulation and AI tools a common model to read and write. [The case for an open format](/why) - Choose your tools Read and analyse plans outside the application that created them. Share the data with the partners and software you choose. - Share the integration work Build against one published structure for products, equipment and placements. Each tool can use the same description of the shelf. - Keep the detail retail needs Represent product variants, nested equipment and your own attributes in the same file. Add information without inventing another format. - AI you can check Every plan follows a published specification with defined units and meanings, so you can check what an agent reads and writes before anyone acts on it. ## Build with Vazen TOML Start with a file and a standard TOML parser. The docs include a working Python example, and the specification defines the data. [Read the documentation](/docs)[Read the specification](/spec/0.3.0) Current schema: 0.3.0. [Schema and package versions](/docs#compatibility) ### JavaScript / TypeScript Install from npm: ``` npm install @vazen/toml ``` [npm (opens in a new tab)](https://www.npmjs.com/package/@vazen/toml)[GitHub (opens in a new tab)](https://github.com/vazen/toml) The npm package reads and writes Vazen TOML, JSON and PSA files. A Python package is planned. Building a tool or considering adoption? Share an idea, propose an extension or help shape the specification. [Contribute your ideas](mailto:toml@vazen.com) ## Documentation | Vazen TOML Source: /docs.md Documentation # Using Vazen TOML Use the @vazen/toml package to read and write Vazen TOML, JSON and PSA files. Any TOML parser can also read a Vazen TOML file. On this page - [Example shelf](#example-shelf) - [Installation](#installation) - [Read a file](#read) - [Write a file](#write) - [JSON files](#json) - [PSA files](#psa) - [Effect schemas](#effect) - [API overview](#api) - [Validation](#validation) - [Versions and compatibility](#compatibility) ## Example shelf This [schema 0.3.0](/spec/0.3.0) spec describes two cereal products on one shelf. Each has three facings across and four packs deep, so the shelf holds 24 units. [Download example-shelf.vazen.toml](/examples/example-shelf.vazen.toml) View the complete TOML file ``` # Wholly fictional example. # All names, identifiers and measurements are invented. schema = "vazen/spec" schema_version = "0.3.0" name = "Example shelf" [[products]] name = "Oat cereal" [[products]] name = "Cocoa cereal" [[fixtures]] [[fixtures.equipment]] type = "shelf" [[fixtures.equipment.sites]] product = { name = "Oat cereal" } facings = { wide = 3, deep = 4 } [[fixtures.equipment.sites]] product = { name = "Cocoa cereal" } facings = { wide = 3, deep = 4 } ``` The `products` list defines the products. Each site selects one product by name and gives its facing counts. An omitted facing count is one, so `facings = { wide = 3, deep = 4 }` means three across, one high and four deep. A `vazen/spec` file can omit dimensions and positions. A layout engine adds them to make a `vazen/layout` file, which records the resolved geometry. ## Installation | Package | [@vazen/toml 0.4.0 (opens in a new tab)](https://www.npmjs.com/package/@vazen/toml) | | -------------- | ----------------------------------------------------------------------------------- | | Reads | Vazen TOML 0.3.0, spec and layout, as TOML or JSON. PSA files of any version. | | Writes | Vazen TOML 0.3.0, as TOML or JSON. PSA 2017.1.0 to 2024.4.0. | | Default output | Vazen TOML 0.3.0. PSA 2024.4.0. | | Details | [Versions and compatibility](#compatibility) | Requirements: - Node.js `>=24`. - ES modules. The package has no CommonJS build. - Effect `^4.0.0`, as a peer dependency. Install the package and Effect: ``` npm install @vazen/toml effect ``` The source is on [GitHub (opens in a new tab)](https://github.com/vazen/toml). The examples on this page use @vazen/toml 0.4.0, Node.js 24 and schema 0.3.0 files. ## Read a file `decodeVazenProjectFromTomlFile` reads the text of a `.vazen.toml` file. It returns the project and its source. **Example** (Count the units on the example shelf) Save this as `count-units.mjs` beside the [example shelf](#example-shelf), then run `node count-units.mjs`. ``` import { readFileSync } from 'node:fs'; import { decodeVazenProjectFromTomlFile, VAZEN_SITE_DEFAULTS, } from '@vazen/toml'; const { project } = decodeVazenProjectFromTomlFile( readFileSync('example-shelf.vazen.toml', 'utf8'), ); let units = 0; for (const fixture of project.fixtures ?? []) { for (const equipment of fixture.equipment ?? []) { for (const site of equipment.sites ?? []) { const { wide, high, deep } = { ...VAZEN_SITE_DEFAULTS.facings, ...site.facings, }; units += wide * high * deep; } } } console.log(`${project.name}: ${units} units`); // Output: Example shelf: 24 units ``` The project holds the tables of the file. A key that the file omits is absent, so `site.facings` can be `undefined`. `VAZEN_SITE_DEFAULTS` gives the value of an omitted facing count or orientation. The source gives the file’s format and declared version, such as `{ declaredVersion: '0.3.0', format: 'vazen-toml' }`. A reader also returns `messages`, a list of structured findings, beside `project` and `source`. A writer writes only the project. ### Read a file with any TOML parser Any TOML 1.0 parser reads the syntax of a Vazen TOML file. This example uses the parser in Python 3.11 or later. Save it as `read_plan.py` beside the file, then run `python3 read_plan.py`. ``` import tomllib with open("example-shelf.vazen.toml", "rb") as source: plan = tomllib.load(source) units = 0 for shelf in plan["fixtures"][0]["equipment"]: for site in shelf["sites"]: facings = site["facings"] units += ( facings.get("wide", 1) * facings.get("high", 1) * facings.get("deep", 1) ) print(plan["name"]) print(f"{units} units") ``` The output is: ``` Example shelf 24 units ``` A general parser checks only the syntax. The package also checks the shape of each table. [Validation](#validation) says what it checks. ## Write a file `encodeTomlFileFromVazenProject` writes a project as the text of a `.vazen.toml` file. **Example** (Write a spec with one site) ``` import { encodeTomlFileFromVazenProject } from '@vazen/toml'; const text = encodeTomlFileFromVazenProject({ project: { schema: 'vazen/spec', schema_version: '0.3.0', name: 'Example shelf', products: [{ name: 'Oat cereal' }], fixtures: [ { equipment: [ { type: 'shelf', sites: [ { product: { name: 'Oat cereal' }, facings: { wide: 3, deep: 4 } }, ], }, ], }, ], }, }); ``` The text is: ``` schema = "vazen/spec" schema_version = "0.3.0" name = "Example shelf" [[products]] name = "Oat cereal" [[fixtures]] [[fixtures.equipment]] type = "shelf" [[fixtures.equipment.sites]] [fixtures.equipment.sites.product] name = "Oat cereal" [fixtures.equipment.sites.facings] wide = 3 deep = 4 ``` A project must have `schema` and `schema_version`. The writer writes schema 0.3.0, unless the `version` option names another version that `VazenVersion` lists. It refuses a project that a reader would refuse as a file. ## JSON files `decodeVazenProjectFromJsonFile` and `encodeJsonFileFromVazenProject` read and write the same tables as JSON, in a `.vazen.json` file. JSON suits a program or an API. TOML suits a file that people keep and edit. The [specification](/spec/0.3.0#json) gives the rules for dates, `null`, `nan` and `inf`. **Example** (Write a date as JSON and read it back) ``` import { decodeVazenProjectFromJsonFile, decodeVazenProjectFromTomlFile, encodeJsonFileFromVazenProject, } from '@vazen/toml'; const { project } = decodeVazenProjectFromTomlFile(`schema = "vazen/spec" schema_version = "0.3.0" reviewed = 2026-09-27 `); const json = encodeJsonFileFromVazenProject({ project }); console.log(json); /* Output: { "schema": "vazen/spec", "schema_version": "0.3.0", "reviewed": "2026-09-27" } */ const read = decodeVazenProjectFromJsonFile(json); console.log(read.project.reviewed instanceof Date); // Output: true ``` The JSON reader also refuses an integer outside JavaScript’s safe range, as the TOML reader does. ## PSA files [PSA files](/psa) shows how to convert a ProSpace PSA file to JSON or Vazen TOML and write it back. It also says what a conversion keeps. ## Effect schemas The file functions throw a `SchemaError`. To get a `Result` or an `Effect`, use the Effect schemas behind them. | Schema | Encoded | Type | | -------------------------- | ------------ | -------------- | | `VazenProjectFromTomlText` | `string` | `VazenProject` | | `VazenProjectFromJsonText` | `string` | `VazenProject` | | `VazenProjectFromPsaText` | `string` | `VazenProject` | | `VazenProjectFromPsaBytes` | `Uint8Array` | `VazenProject` | | `VazenProjectFromPsaData` | `PsaData` | `VazenProject` | | `PsaDataFromPsaText` | `string` | `PsaData` | | `PsaDataFromPsaBytes` | `Uint8Array` | `PsaData` | A `VazenProject` differs from the tables of a file: - An optional value is an `Option`. - Keys that the specification does not define are in the `attributes` of each object. - `stage` is `'spec'` or `'layout'`. - `source` is part of the project. So a PSA schema writes the version of the PSA file that the project came from, if that version can hold the project. **Example** (Decode a file to a Result) ``` import { readFileSync } from 'node:fs'; import { Option, Result, Schema } from 'effect'; import { VazenProjectFromTomlText } from '@vazen/toml'; const result = Schema.decodeResult(VazenProjectFromTomlText)( readFileSync('example-shelf.vazen.toml', 'utf8'), ); if (Result.isSuccess(result)) { const project = result.success; console.log(project.stage, Option.getOrElse(project.name, () => 'Untitled')); } else { console.error(result.failure.message); } // Output: spec Example shelf ``` ## API overview No function reads or writes the file system. Your code reads and writes the files. | Function | Input | Output | | -------------------------------- | ----------------------- | --------------------- | | `decodeVazenProjectFromTomlFile` | `.vazen.toml` text | `{ project, source }` | | `encodeTomlFileFromVazenProject` | `{ project, version? }` | `.vazen.toml` text | | `decodeVazenProjectFromJsonFile` | `.vazen.json` text | `{ project, source }` | | `encodeJsonFileFromVazenProject` | `{ project, version? }` | `.vazen.json` text | | `decodeVazenProjectFromPsaFile` | PSA bytes | `{ project, source }` | | `encodePsaFileFromVazenProject` | `{ project, version? }` | PSA bytes | | `decodePsaDataFromPsaFile` | PSA bytes | `PsaData` | | `encodePsaFileFromPsaData` | `{ data, version? }` | PSA bytes | The package also exports: - `VazenVersion` and `PsaVersion`, the versions each writer accepts. - `VAZEN_SITE_DEFAULTS` and the `PSA_*_DEFAULTS`, the values of omitted keys and empty cells. - `VazenTomlProjectV0_3_0`, the tables of a 0.3.0 file, and the types of `VazenProject`, `PsaData` and their parts. This release does not check whole-file or spatial rules, and does not upgrade a file to another schema version. ### Errors Each function throws a `SchemaError` when it cannot read or write. The message is a tree that ends at the problem. PSA reading makes a best effort and returns a project satisfying its schema, with recoverable issues in `messages`. Writers enforce the destination format’s requirements. **Example** (Read a file of an unsupported version) ``` import { decodeVazenProjectFromTomlFile } from '@vazen/toml'; decodeVazenProjectFromTomlFile(`schema = "vazen/spec" schema_version = "0.9.0" `); /* throws: SchemaError(Expected VazenVersion, got "0.9.0" at ["schema_version"]) */ ``` ## Validation Reading checks the shape of each table: its keys, their types and their allowed values. For example, a product must have a `name`, and a `gtin` must have 8, 12, 13 or 14 digits. Reading does not check rules that need the whole file, such as each product selector matching exactly one product. It does not check spatial rules, such as equipment containment or overlapping product placements. It does not repair a file. We are developing optional spatial checks. They will be published with the schema they apply to, and use that schema’s version. Their result will be separate from base conformance. Validation reports problems. A repair changes the plan, so it must be requested explicitly. The [specification](/spec/0.3.0) is the source for the requirements of each release. ## Versions and compatibility The **schema version** identifies the data contract. A **package version** identifies one implementation. They change independently, and npm and Python releases can support the same schema. | Artifact | Status | Version | | ----------------------------------------------------------------------------------------- | --------- | ----------------------------------- | | [Specification](/spec/0.3.0) | Published | 0.3.0 | | [JavaScript / TypeScript (opens in a new tab)](https://www.npmjs.com/package/@vazen/toml) | Published | @vazen/toml 0.4.0 | | Python | Planned | Its own releases and schema support | @vazen/toml 0.4.0 supports: | Capability | Support | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Read `vazen/spec` and `vazen/layout` | 0.3.0, as TOML or JSON | | Write `vazen/spec` and `vazen/layout` | 0.3.0, as TOML or JSON. Default 0.3.0. | | Read PSA | Any version. Column counts are known for the versions the writer accepts. A row with another count is reported as `UnlistedWidth`. | | Write PSA | 2017.1.0, 2017.2.0, 2019.1.0, 2019.1.1, 2020.1.0, 2020.1.1, 2021.1.2, 2023.1.0, 2024.1.0, 2024.4.0. Default 2024.4.0. | | Check whole-file rules | Not supported | | Check spatial rules | Not supported | | Upgrade to another schema version | Not supported | Each package release states the schemas it reads and writes, and its default output. A reader for a version does not mean a writer or an upgrade path for it. The `schema_version` in a file does not identify the npm package, the Python package or the TOML version. Older specifications keep their own versioned URLs. ## Read and Convert ProSpace PSA Planogram Files | Vazen TOML Source: /psa.md PSA files # Read and convert ProSpace PSA planogram files The free @vazen/toml package reads ProSpace .psa planogram files from JDA and Blue Yonder Space Planning. It converts them to JSON or Vazen TOML, and writes them back as PSA. It runs without Space Planning. On this page - [Is this your file?](#identify) - [Installation](#installation) - [Convert PSA to JSON](#psa-to-json) - [Convert PSA to Vazen TOML](#psa-to-toml) - [Convert from the command line](#command-line) - [Write a PSA file](#write) - [What a conversion keeps](#conversion) - [Edit PSA records](#records) ## Is this your file? A ProSpace PSA file is text. It starts with these lines: ``` PROSPACE SCHEMATIC FILE ; Version 2024.4.0 ; Codepage=1252 ``` The version changes from file to file. Unreal Engine animation files also use the .psa extension. This package does not read them. ## Installation Install the package and Effect from npm: ``` npm install @vazen/toml effect ``` The package needs Node.js `>=24` and ES modules. Effect `^4.0.0` is a peer dependency. A Python package is planned. The examples on this page use @vazen/toml 0.4.0 and Node.js 24. ## Convert PSA to JSON Download the made-up file [v2024.4.0.psa (opens in a new tab)](https://github.com/vazen/toml/blob/main/typescript/src/psa/.test-data/v2024.4.0.psa) and save it as `example.psa`, or run: ``` curl -o example.psa https://raw.githubusercontent.com/vazen/toml/main/typescript/src/psa/.test-data/v2024.4.0.psa ``` Save this script as `convert-to-json.mjs` beside the file. Then run `node convert-to-json.mjs`. ``` import { readFileSync, writeFileSync } from 'node:fs'; import { decodeVazenProjectFromPsaFile, encodeJsonFileFromVazenProject, } from '@vazen/toml'; const { project } = decodeVazenProjectFromPsaFile(readFileSync('example.psa')); writeFileSync( 'example.vazen.json', encodeJsonFileFromVazenProject({ project }), ); ``` The start of `example.vazen.json` is: ``` { "schema": "vazen/layout", "schema_version": "0.3.0", "name": "Fictional Project 5", "products": [ { "ref": "p_18jm0l02ylwz1", "name": "Product 01", "gtin": "5000000000001", "dimensions": { "width": 65, "height": 120, "depth": 60 }, "_psa__id": "7000001", ``` ## Convert PSA to Vazen TOML Save this script as `convert-to-toml.mjs` beside the file. Then run `node convert-to-toml.mjs`. ``` import { readFileSync, writeFileSync } from 'node:fs'; import { decodeVazenProjectFromPsaFile, encodeTomlFileFromVazenProject, } from '@vazen/toml'; const { project, source, messages } = decodeVazenProjectFromPsaFile( readFileSync('example.psa'), ); writeFileSync( 'example.vazen.toml', encodeTomlFileFromVazenProject({ project }), ); console.log(source); // Output: { declaredVersion: '2024.4.0', format: 'psa' } console.log(messages); // Output: [] ``` The start of `example.vazen.toml` is: ``` schema = "vazen/layout" schema_version = "0.3.0" name = "Fictional Project 5" _psa__key = "PRJ-5" _psa__primary_key = "id" _psa__case_multiple = 1 _psa__days_supply = 7 _psa__number_of_stores = 12 ``` The JSON and the TOML file hold the same tables: - Each planogram becomes a fixture. - Each fixture row becomes equipment in that fixture. - Each position becomes a site. - Every length is in millimetres. - A value with a standard key uses that key. Every other value becomes a `_psa__` key on its table, unless the value is the column’s default. The [specification](/spec/0.3.0) defines each table. ## Convert from the command line The package's `vazen-toml` command converts a file without a script, and never changes the file. This writes the same `example.vazen.toml`: ``` npx @vazen/toml@0.4.0 convert example.psa --to toml > example.vazen.toml ``` `inspect` lists the plans in a file, and `--plan` converts one of them. The [package README (opens in a new tab)](https://github.com/vazen/toml/blob/main/typescript/README.md#command-line) lists the command's diagnostics and exit codes. ## Write a PSA file `encodePsaFileFromVazenProject` writes a layout as a PSA file. This script reads `example.vazen.toml` and writes it as PSA: ``` import { readFileSync, writeFileSync } from 'node:fs'; import { decodeVazenProjectFromTomlFile, encodePsaFileFromVazenProject, } from '@vazen/toml'; const { project } = decodeVazenProjectFromTomlFile( readFileSync('example.vazen.toml', 'utf8'), ); writeFileSync('example-copy.psa', encodePsaFileFromVazenProject({ project })); ``` The writer writes PSA 2024.4.0. The `version` option names another version that `PsaVersion` lists: 2017.1.0, 2017.2.0, 2019.1.0, 2019.1.1, 2020.1.0, 2020.1.1, 2021.1.2, 2023.1.0, 2024.1.0, 2024.4.0. ``` const bytes = encodePsaFileFromVazenProject({ project, version: '2017.2.0' }); ``` The writer refuses a value that a column of that version cannot hold. It writes every file in Windows-1252. ## What a conversion keeps The reader reads a file as UTF-8 if it is valid UTF-8, and as Windows-1252 if not. Reading makes a best effort and reports any lost values, uncertain interpretations or generated ids in `messages`, beside the project. An empty list means no messages were reported. Each entry identifies the affected record type, rows and column where known: | Message | Meaning | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `UnlistedWidth` | Rows with a cell count that no listed version has for their type. They are read by position, so a value can be in the wrong column. | | `DroppedCells` | Values in cells after the last column of a row. They are dropped. | | `UnreadCells` | Cells that their column cannot read. They are read as empty. | | `IgnoredRows` | Rows with no place in `PsaData`, such as a position before the first fixture of its planogram. They are ignored. | | `GeneratedIds` | Planograms without a key. Each gets an id from its place in the file, such as `planogram-1`, which writing puts in its key. | Read a PSA file, then write the project as PSA again. The project can pass through TOML or JSON between the two steps. For a file read with no messages, the new file holds the same values, with these differences: - A cell that holds its column’s default can become empty, and an empty cell can get the default. The value does not change. - Text is written in Unicode’s composed form, and U+FFFD is written as `?`. - A length is rounded to nine decimal places of a millimetre. - The number format, line endings and row order can change. So a round trip does not give the same bytes. The [PSA README (opens in a new tab)](https://github.com/vazen/toml/blob/main/typescript/src/psa/README.md) gives every rule for reading and writing. ## Edit PSA records To edit PSA columns directly, read the records, not a project. `decodePsaDataFromPsaFile` returns `PsaData`, the rows of the file as records, and its `messages`. `encodePsaFileFromPsaData` writes the records as a file. The [API overview](/docs#api) lists every function. Blue Yonder and .psa are trademarks of their respective owners. Vazen is not affiliated with or endorsed by Blue Yonder. PSA support was built from PSA files that customers supplied or that were published online; column and value names follow ProSpace’s field definitions. No Blue Yonder software was used. ## Specification 0.3.0 | Vazen TOML Source: /spec/0.3.0.md 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](/docs#compatibility). On this page - [Introduction](#introduction) - [Structure](#structure) - [Fields](#fields) - [Product selectors](#product-selectors) - [Attributes](#attributes) - [Packaging levels and related products](#packaging-levels-and-related-products) - [Units and coordinates](#units-and-coordinates) - [Ordering](#ordering) - [Orientation](#orientation) - [Example](#example) - [JSON](#json) - [Versioning](#versioning) - [Feedback](#feedback) ## 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)](https://toml.io), 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: ```text 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: ```toml product = { gtin = "5000000000012" } ``` ```toml 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: ```toml 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. ## Packaging levels and related products 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: ```toml 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. ```toml 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 . 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?](/why#design) ## Why Vazen TOML? | Vazen TOML Source: /why.md The case for an open planogram format # Why Vazen TOML? Your shelf data is yours; the ability to open it is not. Vazen TOML is an open format for planogram data, published for anyone to use with no fee, ever. On this page - [Reading your own plans takes a licence](#access) - [Every new tool rebuilds the shelf](#integration) - [A product is not its barcode](#retail) - [Why the format works this way](#design) - [Get involved](#get-involved) ## Reading your own plans takes a licence Product and transaction data have long had open standards. Shelf data has none. How products are arranged on fixtures lives in planning software with proprietary file formats, and reading your own planograms requires a licence for the software that wrote them. Every analytics pipeline, model or agent that needs shelf data pays that cost again: another licensed seat, a specialist integration or someone exporting by hand. The format becomes a recurring cost, and the software vendor decides who else can use your data. A published, open format gives the data a life beyond the application that created it. Retailers and brands choose how to analyse it, which partners see it and which tools to build around it. ## Every new tool rebuilds the shelf Reading the file is only the first step. Every analytics pipeline, simulation and agent also needs a model of products, equipment and placements. Each team invents its own, with its own quirks and limits. AI makes this even more acute. Every time an agent is asked to read or write a plan, another representation is born. Agents can generate parsers quickly, but those parsers still have to agree on what the data means if you want systems that work together. A shared format makes these tools interoperable and additive: a standard language for your agent to talk to my agent about the shelf. Each application remains free to decide what it does with the plan. ## A product is not its barcode ### Pack designs and products without a GTIN Space-planning software has keyed products on a unique code since the 1980s, when master data was one row per code and a planogram existed to be executed in a store that would scan it. In the context of a single retailer, that made total sense. The same tools are now asked questions they were never designed to answer. A brand tests four pack designs on one shelf. A retailer builds plans with a product before it’s given an ID. The general perils of copy paste. By GS1’s own rules a new pack design needs no new GTIN, so four designs legitimately carry one code. When the code is the key, every one of these forces the same workaround: ``` 500000000001 500000000001_new1 500000000001_new2 ``` Vazen TOML does not rely on unique ids and is much better for it. ### Any fixture, any field A chiller holds shelves above baskets. A pegboard sits inside a bay and carries hooks. A display unit stands inside another fixture. The file is written as the fixture is built: a fixture holds equipment, equipment holds equipment or sites, and the tree goes as deep as the physical object does. In a layout every element carries its position and dimensions in the fixture’s frame, so one recursive walk renders a two-shelf end or a forty-bay aisle. Equipment types are open. The specification fixes the data and deliberately leaves behaviour to the software that reads it. ### Your existing product data already fits Your master data drops straight in: the same GTINs, millimetres and six product faces. No re-keying, no translation, no competing version of the facts. Your own fields travel under their own names, and a tool’s data travels under its namespace. Product images are URLs, so a plan stays one self-contained text file that renders anywhere its images can be reached. ## Why the format works this way ### Readable text and a published contract TOML is readable as text and has parsers in many languages. The Vazen specification supplies what TOML cannot: the meanings, units, references and rules of a planogram. A TOML parser checks the syntax; a Vazen implementation also checks that contract. ### Planning intent and resolved geometry A `vazen/spec` document can leave placement decisions open. A `vazen/layout` document records resolved geometry. Tools can exchange either planning requirements or a finished arrangement. ### Round trips that keep what you added Extensions carry information beyond the shared core. A consumer that writes a file back must preserve every key it does not understand, and that rule is what makes round trips to and from other formats work. A converter must still declare the source formats it supports and what it preserves. Importing a plan, checking its geometry and repairing it are separate operations. Moving a product to fix an overlap changes the plan, so that decision must be explicit. [Read the specification](/spec/0.3.0) or explore [PSA conversion and round-trip limits](/psa#conversion). ## Get involved Read the [example shelf](/docs#example-shelf) and tell us what your team needs to do with shelf data. We want to hear from retailers, brands, engineers and researchers working on real planning problems. We can help you get started, share sample files and talk through your use case. The specification is free to use, with no fee. Vazen maintains the project, and the current specification is [0.3.0](/spec/0.3.0). The stated aim is neutral, independent governance as the format matures. The JavaScript/TypeScript package is on npm. It reads and writes Vazen TOML, JSON and PSA files. An independent Python implementation and a web converter and visualiser are planned. The software is licensed under Apache 2.0 and the specification under CC BY 4.0; see the [licensing terms (opens in a new tab)](https://github.com/vazen/toml/blob/main/LICENSING.md). It’s time to rescue the shelf data. [Contribute your ideas](mailto:toml@vazen.com) or [follow on GitHub (opens in a new tab)](https://github.com/vazen/toml).