# Writing the model file axmo-format-version: ed0892bbbdf4 **Every key** — required or not, its default, its allowed values — is in [`../generated/axmo-model-yaml-keys.json`](../generated/axmo-model-yaml-keys.json), and the full shape in [`../generated/axmo-model.schema.json`](../generated/axmo-model.schema.json). An unknown or misspelt key is refused with `FORMAT_UNKNOWN_KEY`, naming the keys that are allowed there. This page covers what those files cannot tell you: which forms to choose, and the rules that are easy to break. ## The skeleton ```yaml axmo: "1.0" # quoted — an unquoted 1.0 is a number and is refused name: Cash balance description: One or two sentences on what the model is for. dimensions: { … } series: { … } dataSources: { … } # where the input data is dataLoaders: { … } # how it reaches the series outputRanges: { … } # the reports Axmo writes data: { … } # optional: the input cells, so the model works in an empty workbook ``` Every recipe in [`../recipes/`](../recipes/) is a complete, working file. **Start from the closest recipe** rather than from nothing. **The `.axmo` container** is a zip, with standard deflate compression, holding two files at its root: `model.yaml`, and `format-version` containing `1`. To build one: ``` printf 1 > format-version && zip -X model.axmo model.yaml format-version ``` **Two forms of one model.** A **plain YAML** file, `.axmo.yaml`, is the `.axmo`'s `model.yaml` byte for byte. Export offers both at every depth, and Import and the check tool take both. Choose by the tools you have: | You can… | Deliver | |---|---| | work in a chat, even one with code (a limit may stop you making a file) | **Plain YAML** in your reply, for the user to save as `.axmo.yaml` and import. | | work as an agent with a file system | Either; plain YAML is simpler to read and compare. | | **read** a user's model | Ask the user to export it as *Plain YAML text* and paste or attach it. An `.axmo` they send is a zip: `unzip -p model.axmo model.yaml`. | **YAML pitfalls:** - Quote any value that starts with `[` (a formula such as `'[Stage load, h][All_Products] / Capacity'`), since YAML would otherwise read it as a list. - Quote period labels in data (`"2027-01"`) so they stay text. - Section entries are keyed by name and may appear in any order. Some lists are order-sensitive and keep the order you write: a series' `dimensions`, layers, tree children, output-range blocks and a loader's `targets`. ## Dimensions ```yaml Products: type: categorical layers: [Top_Products, Range, Product] # top to bottom; the root sits on the first layer tree: All_Products: Pans: - Pan 24 cm - Pan 28 cm Pots: - Pot 5 L ``` - **Names:** a root `All_` and a top layer `Top_`. A layer may not carry its dimension's name, so use dimension `Products` and layer `Product`. A name may hold any character except `; / [ ] { }`. - **Series and elements share one namespace.** A series may not be named like an element: with a Versions element `Actual`, call the series `Cost`, not `Actual`. - **A tree written as a mapping:** a node with children is a key; a parent whose children are all leaves takes a list. A root with no children yet is `All_Customers: null`. - **The tree can be the mapping.** If entities belong to currencies, make `Currency` a layer above `Entity`. A rate loaded per currency then reaches its entities by split-down ([recipe](04-recipes.md#a-rate-held-at-a-higher-level)). **Time:** ```yaml Time: type: sequential sequentialType: axmo_time parameters: selected_levels: [month, quarter, year] # finest first start: "2027-01" end_mode: end_point end: "2027-12" ``` - **Element names** are `2027`, `2027-H1`, `2027-Q1`, `2027-01`, `2027-01-30`; the root is `All_Time`. - **Layer names** are `Year`, `Half-Year` (with the hyphen), `Quarter`, `Month`, `Day`, one for each level in `selected_levels`, under the root's layer `Top_Time`. An H1 total needs `half_year` selected. - **There is no other naming and no fiscal calendar.** Data must use these exact names. - **`hasBefore: true` / `hasAfter: true`** add opening and closing boundary elements (`BEFORE`, `AFTER`). - **A horizon need not start on a quarter or a year.** From `2026-11`, the elements `2026-Q4` and `2026` exist and hold only the months inside the horizon. ## Series ```yaml Closing Balance: type: calculated description: Cash at the end of the month. formula: Nz(Closing Balance[Month[-1]], Opening Balance) + Receipts - Payments dimensions: - Accounts: rollUp: SUM - Time: rollUp: LAST ``` - **`dimensions` as a plain list** (`[Products, Time]`) means `SUM` on every dimension. - **Otherwise use a list of single-key maps**, in the order totals are calculated ([03](03-formulas-and-totals.md)). - **Give every series a `description`** naming its unit. ## Loading data A **data source** names a range: an address, a named range or an Excel Table. A **series loader** maps the source's columns to the target series' dimensions. **Every dimension of the target must be either mapped to a column or fixed** (`fixedCoordinates`). | The table looks like… | Use | |---|---| | One row per value: `Account, Month, Amount` | A singular loader: `target`, `valueColumn`, `dimensionMappings`. | | One row per coordinate, several measures as columns: `Product, Month, Volume, Revenue` | **One bulk loader** with `targets: [{target, valueColumn, duplicateHandling}, …]`. All targets need the same dimensions. | | One dimension's elements across the columns: `Department, 2027-01, 2027-02, …` | The source declares `columnLayout: dimension`, `columnsDimension: Time` and the band (`bandStartMode: after`, `bandStartColumn: Department`, `bandEndMode: end`). The loader takes `valueColumn: _Value` and maps the dimension to the column `_HeaderDimElement`. **One loader reads the whole band** ([recipe](04-recipes.md#actual-versus-budget-from-two-table-shapes)). With several label columns before the band (`Shop, Product, 2027-01, …`), `bandStartColumn` names the **last** of them. A trailing column such as `Total` is left out with `bandEndMode: before` and `bandEndColumn: Total` (`at` would include it). | | A table that belongs to one version (all *Budget*) | Map the other dimensions and fix the version: `fixedCoordinates: { Versions: Budget }`. | | Rows above the grain (a target per quarter, per all products) | Load them at that level: the column may hold `2027-Q1`, or fix the element (`All_Products`). See the rules below and the [recipe](04-recipes.md#a-top-down-split). | **Rules that bite:** - **Several loaders may fill one series when their coordinates do not overlap.** Two loaders writing the same cell is always a conflict, even with equal values. - **Duplicates within one source** are settled by `duplicateHandling`: `FIRST` (the default), `LAST`, `AVERAGE`, `SUM` or `BLOCK`. **Only `BLOCK` reports them**; the others collapse duplicates silently. Prefer `BLOCK` unless duplicates are expected. - **A row whose label matches no element is skipped with a warning.** If every row fails, the series is empty and the import still succeeds. - **Rows at different levels** (some months, some quarters) may share one series. Under the default `splitDown: NONE` a value stays where it is loaded and reaches the totals above it, but a series calculated at the leaves does not see it, and the load warns `SERIES_LOADER_VALUE_ABOVE_LEAVES` at every Update (`--strict` fails). Spread it (`EQUAL`, `PROPORTIONAL`), or set roll-up `NONE` too for a fully manual load, which is not checked. A row for a parent and a row for its own child collide: `SERIES_ROLLUP_DATACELL_COLLISION` under `NONE`, `SERIES_DATACELL_COLLISION` under a split-down. - **An empty value cell** is skipped with a warning (`SERIES_LOADER_VALUE_MISSING`); `ignoreEmptyValues: true` on the loader skips it silently. Text in a number column is reported either way. **The `data` section** carries the input cells: `data: { "Inputs!A1:C13": { values: [[header…], [row…], …] } }`. Missing sheets are created on import. The address must match the rows exactly. ## A dimension built from data A **dimension generator** reads element names from a column: - **`parentElement`** places them all under one element; - **`parentColumn`** places each under the element named in another column. **Generators into one dimension place as one batch**, whatever their names: a parent may come from any of them. An element named twice is added once; two rows giving one element different parents or layers are a located error (`DG_PARENT_REDEFINITION`, `DG_LAYER_REDEFINITION`). **An element's layer** is its `layerColumn` cell where one is given (any layer below its parent's), otherwise its parent's layer + 1. A generator never creates a layer. **The order of one parent's elements** — which `FIRST`, `LAST` and `$n` read: the elements written in the file first, in their order; then each generated element by the first row that names it, rows in source order, generators in name order (case-insensitive). **A time dimension is never generated.** A generator into it is refused (`DG_TARGET_SEQUENTIAL`); extend it by its parameters (`start`, `end`). See the [recipe](04-recipes.md#a-dimension-built-from-data). ## Output ranges ```yaml Budget report: cell: Report!A1 rowDef: blocks: - dimension: Departments/Department - dimension: Departments/Top_Departments - series: Actual := Cost[Actual] # alias := expression format: "#,##0" newLine: true - series: Variance % format: 0.0% colDef: blocks: - dimension: Time/Month - dimension: Time/Quarter ``` - **Coverage.** Every dimension a series varies over must be on the rows, on the columns, or fixed in the series' expression (`Cost[Actual]` fixes Versions). - **One block per series**, each with its own `format` (an Excel number format). Consecutive series blocks form one group, and a group sits at the start or the end of an axis. - **An alias** (`Name := expression`) is optional, and must be an identifier: letters, digits and underscores (`Sales_YTD`, not `Sales YTD`), or the import refuses it (`FORMAT_NOT_A_NAME`). - **Order of totals: the coarsest layer's position decides.** Listed first (`Top_Products`, `Range`, `Product`), every total comes before its members; anywhere else (`Product`, `Range`, `Top_Products`), after them, at every level. - **Report or table:** `outputMode: table` writes an Excel Table of every combination instead of a laid-out report.