# Axmo — everything needed to write a model > Generated by `npm run gen:ai-pack` (scripts/ai-pack.ts) — do not edit. Format version: ed0892bbbdf4. > The Agent Skill's instructions, the modelling guide (its README and every part) and the capability table, in that > order, each under its path in the pack (https://axmo.it/ai/). Every link is absolute. The rest of the pack — the JSON > Schema, the key list, the function reference, the recipes, the tools — is listed in https://axmo.it/llms.txt. # skill/SKILL.md --- name: axmo-model-check description: Build, check and change Axmo models — the .axmo / model.yaml files of the Axmo Excel add-in for multi-dimensional financial and operational modelling. Use when asked to create an Axmo model, convert a spreadsheet model or a description into one, fix an Axmo import report, or change an existing Axmo model. Runs the real Axmo import, compile and update on the file and prints the coded report and the computed values. --- # Axmo models Axmo is an Excel add-in. A **model** is one YAML file, `model.yaml`, normally inside an `.axmo` zip; the user imports it from the Axmo ribbon. Its parts: **dimensions** (trees of elements); **series**, loaded from data or calculated by a formula written once on named series; **data sources and loaders**; **output ranges**; optionally **data**, the input cells. ## What is in this folder | Path | Use | |---|---| | `tool/axmo-check.mjs` | The full check: the product's own import, compile and update, report, values. Node 18+. | | `tool/axmo-shape-check.mjs` | Shape only, against the JSON Schema. | | `tool/axmo-workbook.mjs` | `extract` the model from an Axmo workbook; `apply` a changed one to a new copy. | | `generated/axmo-model.schema.json`, `generated/axmo-model-yaml-keys.json` | Every key, its allowed values, and every shape a file may hold. | | `generated/error-codes.md` | Every report code and its meaning. | | `generated/message-forms.md` | How report lines read. | | `generated/functions.md` | Formula functions and naming rules. | | `generated/tool-contract.md` | The tool's flags and exit codes. | | `capabilities.md` | What Axmo does with a model's data, measured. Claim nothing beyond it. | | `guide/` | The modelling guide: method, file, formulas and totals, recipes, checking. Read `guide/README.md` first. | | `recipes/` | Worked models that import cleanly and compute stated values. **Start from the closest one.** Each `.axmo` has its YAML beside it, `.model.yaml`. | ## The method 1. **Purpose, grain, horizon, units**, written down first. 2. **Study the source.** List the target, input and intermediary series and each one's dimensions, as a matrix that marks the combinations that never exist. Note the input tables, or agree sample tables with the user. 3. **Reconcile the sources.** The same dimension may be labelled differently, and one series may come from several tables. Axmo matches labels exactly, ignoring case and surrounding spaces, and **converts nothing**. Reshape mismatched labels before loading, and tell the user. 4. **Dimensions:** layers top to bottom, static or generated from data. 5. **Input series:** each one's roll-up per dimension, any split-down, and its loader. 6. **Calculated series:** from the intermediaries to the targets. Think through time behaviour: balances, seeds, stocks versus flows. 7. **Outputs:** agree with the user what to show, and where. 8. **Verify** with the loop below. **Changing an existing model** (`guide/01-method.md`): change an Axmo workbook through Axmo or through `apply`, **never by saving it with openpyxl, pandas, ExcelJS or SheetJS**, which deletes the model. With the workbook: `tool/axmo-workbook.mjs extract book.xlsx --out work --depth complete` (stop on `WORKBOOK_OBJECT_LEFT_OUT`) → edit `work/model.yaml` → `axmo-check --values` until no errors and right values → `apply book.xlsx work/model.yaml -o book-v2.xlsx` → read its lines (ignore the check's Update complaints of empty sources) → the user opens `book-v2.xlsx` and presses **Update**. Without it: ask for a *Plain YAML text* export at `complete`, and the user imports your result. **A new model is always imported**, never made with `apply`. Preserve what you did not create. `apply` writes no cell; data changes in Excel. ## The loop: never hand over an unchecked model ``` node tool/axmo-check.mjs model.yaml --values ``` 1. **Read the report.** `No issues.` is clean. Otherwise each line is `{severity}[{CODE}]: {path} [{line}:{col}]: …`, in three sections: Import, Compile and Update. Look the code up in `generated/error-codes.md`. 2. **Fix, and check again.** 3. **Read the warnings too.** A source whose rows all failed to match loads nothing, yet the exit code is 0. 4. **Read the values** printed after the report. Compare two or three, including a total, **with numbers you calculated by hand.** A clean report is not a correct model. 5. **`--series ""`** lists one series' values, one line per coordinate; narrow it with `--at "=;…"`. 6. **Finish with `--strict`**, which makes any warning exit `1`. **Plain YAML or `.axmo`:** two forms of one model; Export, Import and the tool take both. In a chat, deliver plain YAML in your reply, for the user to save as `.axmo.yaml`. An agent may zip: `printf 1 > format-version && zip -X model.axmo model.yaml format-version`. **Code that cannot download:** ask the user to download https://axmo.it/ai/axmo-model-check-skill.zip and drop it into the chat. **No code:** ask them to import the file and paste its **Copy report**, the same report. **Only a printed or pasted result counts.** Files at different format versions: prefer `llms-full.txt`. ## Rules that are easy to break - `axmo: "1.0"`, quoted. Quote any YAML value that starts with `[`, and quote period labels in data (`"2027-01"`). - **Names:** root `All_`, top layer `Top_`. A layer may not have its dimension's name, so use dimension `Products` with layer `Product`. A series may not be named like an element (`Cost`, not `Actual`). - **Time** (`sequentialType: axmo_time`): element names are `2027`, `2027-Q1` and `2027-01`, the root is `All_Time`, and the layers are `Year`, `Quarter` and `Month` (`Half-Year` and `2027-H1` with `half_year` selected). There is no fiscal calendar. - **A calculated series may have more dimensions than its formula, never fewer.** Drop a dimension by reading its total (`X[All_Products]`) or filtering it (`X[Actual]`). - **Totals:** `SUM` for amounts; `LAST` across time for balances; `FORMULA` for ratios (re-evaluated on totals); `WEIGHTED_AVG` with a `weightSeries` for per-unit inputs; `AVG` across time for rates; `NONE` for values that must not combine (units, versions, currencies, period-on-period growth across time). - **Entered above the grain:** under the default `splitDown: NONE` the value stays where it is loaded; a series calculated at the leaves does not see it, and the load warns `SERIES_LOADER_VALUE_ABOVE_LEAVES`. Spread it: `COPY` for rates, `EQUAL` or `PROPORTIONAL` (with `driverSeries`) for amounts; or set roll-up `NONE` too for a fully manual load. A weight or driver series must drive every dimension it spans; that moves no value loaded at a dimension's own elements. - **Loaders:** every target dimension must be mapped to a column or fixed (`fixedCoordinates`). Several measures in columns go through one bulk loader (`targets`). Months across columns: the source declares `columnLayout: dimension`, and the loader reads `_Value` with the dimension mapped to `_HeaderDimElement`; a trailing `Total` column is left out with `bandEndMode: before`, `bandEndColumn: Total`. Two loaders may never write the same cell. - **Generators** into one dimension place as one batch: a parent may come from any of them. A time dimension is never generated (`DG_TARGET_SEQUENTIAL`); extend its `start` / `end`. - **Output ranges:** every dimension of a shown series must be on the rows, on the columns, or fixed in its expression (`Actual := Cost[Actual]`; an alias is an identifier: `Sales_YTD`). The coarsest layer's position decides the order: listed first, totals come before their members; anywhere else, after. - **An empty input counts as zero in a formula; a division by zero fails** (`IFERROR` catches it, `Nz` does not). Seed a balance with `Nz(Balance[Month[-1]], Opening)`. # guide/README.md # Building Axmo models — the guide axmo-format-version: ed0892bbbdf4 **Who this is for:** an AI assistant or agent asked to build, check or change an Axmo model. **What it is:** how to do it well. **What it is not:** the reference — the keys, codes, functions and report forms are generated from the product and live in [`../generated/`](https://axmo.it/llms.txt); this guide links to them. ## What an Axmo model is Axmo is an Excel add-in. A model is **one YAML file**, `model.yaml`, usually inside an `.axmo` container (a zip that holds `model.yaml` and a `format-version` file containing `1`). The user imports it from the Axmo ribbon in Excel. A model holds: - **dimensions** — trees of named elements: products by range, months by quarter; - **series** — named quantities over some dimensions: an **input** is loaded from data; a **calculated** series has a formula, written once, that holds for every element; - **data sources and loaders** — which ranges feed which series, columns mapped to dimensions; - **output ranges** — reports Axmo writes to sheets; - optionally **data** — the input cells, so the model works in an empty workbook. The idea that makes Axmo different from a spreadsheet: **a formula is written on named quantities, not cells**. `Stage Load = Volume * Minutes / 60` is one formula for every product, stage, plant and month, and every total exists automatically at every level of every dimension. **What the guide may claim** is bounded by [`../capabilities.md`](https://axmo.it/ai/capabilities.md): what the product is measured to do with a model's data, each row pinned by a test. Where the guide and that table differ, the table is right. **Each file declares its format version.** If two you read differ, one is an old cached copy: prefer `llms-full.txt`, tell the user, and let the report decide. ## The files of this guide | File | Read it when | |---|---| | [01-method.md](https://axmo.it/ai/guide/01-method.md) | Always, first: the order of work, from description to checked model. | | [02-model-file.md](https://axmo.it/ai/guide/02-model-file.md) | Writing the YAML: skeleton, names, time, loaders, generated dimensions, output ranges. | | [03-formulas-and-totals.md](https://axmo.it/ai/guide/03-formulas-and-totals.md) | Writing formulas; how each series totals up and splits down. | | [04-recipes.md](https://axmo.it/ai/guide/04-recipes.md) | Looking for a worked pattern: balances, rates, ratios, actual vs budget, splits, formula forms, a ledger P&L. | | [05-checking-and-fixing.md](https://axmo.it/ai/guide/05-checking-and-fixing.md) | Checking, reading the report and fixing, what Axmo cannot do today, the workbook tool. | ## The loop that makes the result reliable **Never hand a model over unchecked.** Check, read the report and values, fix, check again, by the first route you have: - **Code that can download:** `node tool/axmo-check.mjs model.axmo --values` runs the real import, compile and update, prints the report, then every output range's cells. Exit `0` means no error; finish with `--strict`, which fails on a warning too. See [05-checking-and-fixing.md](https://axmo.it/ai/guide/05-checking-and-fixing.md). - **Code that cannot download:** ask the user, in these words: *"Please download https://axmo.it/ai/axmo-model-check-skill.zip and drop it into our chat."* Unzip it and run its tool. - **No code:** ask the user to import the file and press **Copy report** in the Import dialog, and paste the text back. It is the same report. **Only a result the tool printed or the user pasted counts.** Never write output you did not run. Then compare the values with numbers you calculated by hand for two or three cells. A model that imports cleanly can still compute the wrong thing; only a hand-calculated value catches that. # guide/01-method.md # The method axmo-format-version: ed0892bbbdf4 Build a model in this order. Each step ends with something you can show the user, and the user's answer to it is worth more than any guess. ## 1. Purpose, grain, horizon, units Before anything else, write down in a few lines: - **the purpose** — the questions the model must answer (*"what does each stage cost us, by plant and quarter?"*); - **the grain** — the finest level of each dimension (product, not range; month, not quarter); - **the horizon** — the first and last period; - **the units** — of every input and result (units, minutes, kWh, EUR). ## 2. Study the model source Read the user's description and their data, and list: - the **target series** — what the user wants to see; - the **input series** — what is given; - the **intermediary series** — what is needed between them. For each, its **dimensionality**: the dimensions it varies over. A price varies over resource, country and month; it does not vary over product. Then the **input tables**: the user's, or sample tables you agree with the user. **Write a dimensionality matrix**, series down and dimensions across. Mark the combinations that never exist: a product that skips a stage, a resource a stage does not use. Axmo computes every combination of a calculated series, and an empty input counts as zero in a formula. So a combination that never exists computes to 0, which is right, as long as its input is left empty rather than filled. ## 3. Reconcile the sources The same data often arrives in differently labelled or differently shaped tables: - **A dimension under different labels:** `Month` in one table, `Period` in another, or months as column headers `2027-01, 2027-02, …`. - **One series from several tables:** each department's actuals in its own layout; actuals as a long table and the budget as a wide one. Decide, per table: - which **columns** map to which **dimensions**; - whether the table is **long** (one row per value) or **wide** (one dimension's elements across the columns); - which **Version**, if any, each table supplies. **Axmo matches labels exactly**, apart from case and surrounding spaces (inner spaces count); a number and its text, such as `2027` and `"2027"`, also match. It does **not** convert anything. `Jan-27`, `2027-1`, a date, `FY2027` or `0100` will not match `2027-01` or `100`. If the labels differ, reshape the source before loading, or add an Excel formula column that the input range reads, and say so to the user. The check tool does not calculate formulas written to cells, so to check the model, put the reshaped labels in its data. ## 4. Define the dimensions For each dimension: - **Its layers, top to bottom.** By convention the top layer is `Top_` and the root element is `All_`. **A layer may not have its dimension's name**: use a plural for the dimension and a singular for the layer (dimension `Products`, layer `Product`). - **Static or generated?** Write the tree in the file when it is known and stable. Generate it from data when the elements live in a table that changes, such as customers or cost centres ([recipe](https://axmo.it/ai/guide/04-recipes.md#a-dimension-built-from-data)). - **Time** is a sequential dimension, named for you ([02](https://axmo.it/ai/guide/02-model-file.md#dimensions)). ## 5. Define the input series For each input series: - **its dimensions**, from the matrix; - **how it totals up on each dimension** — its roll-up. Not everything sums ([03-formulas-and-totals.md](https://axmo.it/ai/guide/03-formulas-and-totals.md#how-a-series-totals-up)); - **whether it is entered above the grain**, and if so how it splits down: a price per country copied to plants, a target per quarter split to months; - **its loader:** which source, which columns, which fixed elements. ## 6. Define the calculated series Work from the intermediaries to the targets. For each: - **its dimensions** — the business situation it describes, not what its formula happens to produce; - **its formula**, on named series; - **how it totals up.** A sum of amounts sums. A ratio is recalculated from the totals (`FORMULA`). **Time behaviour needs its own thought:** - **balances** carry forward ([recipe](https://axmo.it/ai/guide/04-recipes.md#a-balance-carried-forward)); - **future-dependent values** look forward ([recipe](https://axmo.it/ai/guide/04-recipes.md#a-balance-from-the-future)); - **opening values** need a seed; - **stocks and flows** total differently over time: `LAST` for a stock, `SUM` for a flow. ## 7. Agree and create the outputs Agree with the user what they want to see, then create **output ranges**: - which dimensions go on the rows and which on the columns; - which totals to show; - the number format of each series. Every dimension of a series shown must be placed ([02-model-file.md](https://axmo.it/ai/guide/02-model-file.md#output-ranges)). ## 8. Verify Check the model by [the loop](https://axmo.it/ai/guide/README.md#the-loop-that-makes-the-result-reliable) until it reports `No issues.`. Then compare **two or three values with numbers you calculated by hand**, including one total. Check that totals reconcile: the regions add up to the whole, and the months to the quarter. Tell the user which values you checked, and what you assumed. ## Changing an existing model **Change an Axmo workbook through Axmo or through `apply`, never by saving it with openpyxl, pandas, ExcelJS or SheetJS**: a single load and save deletes the model and its stored values. Reading the cells without saving is harmless, but the model is not in the cells. **With the user's workbook and the ability to run code:** 1. `node tool/axmo-workbook.mjs extract book.xlsx --out work --depth complete` writes `work/model.yaml` (the model with a `data` section of the input cells as last saved, as Export at *Complete* would give it) and `work/dsdata.yaml` (every `DS.DATA` formula in the workbook). A `WORKBOOK_OBJECT_LEFT_OUT` line means the workbook holds a broken object, missing from `model.yaml`: stop, and have the user repair it in Excel first ([05](https://axmo.it/ai/guide/05-checking-and-fixing.md#the-workbook-tool)). 2. Edit `work/model.yaml`, and check it with `axmo-check --values` until it has no errors and the values are right. **A rename** must reach every formula, report and loader in the file; `apply` names the `DS.DATA` cells it breaks. 3. `node tool/axmo-workbook.mjs apply book.xlsx work/model.yaml -o book-v2.xlsx` writes a **new** workbook with the new model (`-o` must name a file that does not exist yet); the original is never changed. Read its lines ([05](https://axmo.it/ai/guide/05-checking-and-fixing.md#the-workbook-tool)): a `DS.DATA` cell to edit, old report cells to clear, data not applied. 4. Give the user `book-v2.xlsx`: they open it in Excel and press **Update**. Until then it shows the old numbers. **Otherwise** ask for the model as an export, *Plain YAML text* at `complete` (or `bindings`) depth — a `definition` export leaves generated dimensions empty (`All_Customers: null`) — and the user imports your result. **A new model** always goes this way: write it, check it, and the user imports it. **Input data changes only in Excel, or through Import at `complete`.** `apply` writes no cell. Either way: - **List the assumptions you find in it** (roll-ups, split-downs, names) before you change anything. - **Preserve what you did not create**: names, order, descriptions, reports. **Re-importing replaces the whole model**, and a `complete` file also replaces the input cells. Before it does, the Import dialog says how many input cells differ from the file, and the user chooses **Import anyway** or **Cancel**. Tell the user to expect it, and that **Import anyway** overwrites what they changed since the export. # guide/02-model-file.md # 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`](https://axmo.it/ai/generated/axmo-model-yaml-keys.json), and the full shape in [`../generated/axmo-model.schema.json`](https://axmo.it/ai/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/`](https://axmo.it/llms.txt) 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](https://axmo.it/ai/guide/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](https://axmo.it/ai/guide/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](https://axmo.it/ai/guide/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](https://axmo.it/ai/guide/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](https://axmo.it/ai/guide/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. # guide/03-formulas-and-totals.md # Formulas, and how series total up axmo-format-version: ed0892bbbdf4 **The functions** — signatures, argument shapes, reserved names — are in [`../generated/functions.md`](https://axmo.it/ai/generated/functions.md). This page covers the grammar you need most often. ## Writing a formula - **Names are written as they are**, spaces included: `Production Volume * Production Time / 60`. - **Put a name in brackets** when it holds punctuation or would otherwise be misread: `[Stage load, h]`, `[Margin %]`. Brackets right after a series are a **filter**: `Cost[Actual]`. - **Names are case-insensitive**, functions too. - **Series line up on the dimensions they share.** `Volume * Usage * Price` multiplies a series over product × month by one over product × stage × resource and one over resource × month. The result has every dimension of all three, with no lookups. - **The formula may not have a dimension its series lacks.** A calculated series' dimensions describe the business situation, and the formula's result must fit inside them. A series may have **more** dimensions than its formula (it is then the same across the extra ones), never fewer. To drop a dimension, read its total or filter it (below). ## Filters: picking elements | Written | Means | |---|---| | `Cost[Actual]` | Cost for Version *Actual*; the result no longer varies over Versions. | | `Resource cost[Frying pans, Europe, 2027-Q3]` | Several dimensions pinned at once, one element each. | | `Cost[Versions/Actual]` | The same, with the dimension named, when an element name is ambiguous. | | `Revenue[All_Products]` | **The total element**: the value the model already holds for all products. | | `Closing Balance[Month[-1]]` | The previous month (relative offset); `[Month[+1]]` is the next. | | `AVG(Receipts[Month[-2:0]])` | A set (here the last three months) reduced by a function. At the start it uses the months that exist. | | `Month@Quarter` | The current month's position within its quarter (1–3); `Month@` is its position overall. | **Dropping a dimension: read the total element.** `[Stage load, h][All_Products]` reads the total the model has already calculated. It equals a sum of the members only when the series totals up with `SUM` on that dimension. With any other roll-up it is that roll-up's total. ## More forms Each of these is shown working in the [formula-forms](https://axmo.it/ai/guide/04-recipes.md#formula-forms) recipe. | Written | Means | |---|---| | `Sales > 100`, `=`, `<>`, `<`, `<=`, `>=` | A comparison: true or false. Comparisons do not chain (`a < b < c`). | | `IF(Sales > 100 and Units >= 5, 1, 0)` | A choice. `and`, `or` and `not (…)` combine conditions. | | `Units ^ 2` | A power. `2 ^ 3 ^ 2` is 64 (left to right) and `-2 ^ 2` is 4, as in Excel. | | `ROUND(Sales / 7, 2)`, `MAX(Sales, Units * 30)` | Functions of values; the full list is in [`../generated/functions.md`](https://axmo.it/ai/generated/functions.md). | | `ISEMPTY(Units)` | True where an input holds no value (a loaded 0 is not empty). | | `SUM(Sales[2027-01:2027-02])` | An element range: from one element to another. | | `Sales[Month[$1]]` | An absolute position: the **first month of the horizon**. | | `SUM(Sales[Month[$1:0]])` | From the first month of the horizon to this one: a running total that does **not** restart each year. Give it `LAST` across time, so a quarter shows its last month. | | `SUM(Sales[Year[0]/Month[$1:0]])` | **Year to date**: `Year[0]/` scopes `$1` to the current year, so it restarts each January. Give it `LAST` across time too. | | `SUM(Marketing[Region])` | **A layer as a set**: every element of that layer in the whole dimension, each read at its own total. The result is the same at every element of the dimension. It needs an aggregation. | | `Month[$1]`, not `Sales[$1]` | A position must name its layer; `Sales[$1]` is refused (`UNANCHORED_POSITIONAL`). | **There are no text literals**, and `Dimension.Element` is not a form: write `Cost[Versions/Actual]`. The parser accepts more — sets, anchors, `WHERE` (one is in [pl-from-ledger](https://axmo.it/ai/guide/04-recipes.md#a-pl-from-a-general-ledger)) — that this guide does not cover yet; check any other form with the tool before relying on it. ## Empty, zero and failed - **To leave a result empty**, give `IF` no third argument: `IF(Sales > 0, Marketing / Sales)` is empty where the condition fails, not 0. With `FORMULA` totals the guard works at every level: a quarter's figure over empty months gives the quarter its share, and the months stay empty. - **An empty cell counts as zero in arithmetic.** `Nz(x, fallback)` replaces an empty value. A reference before the first month is empty, which is how a balance is seeded: `Nz(Balance[Month[-1]], Opening)`. - **A division by zero fails the cell**, and everything calculated from it fails too. `IFERROR(x, fallback)` catches it; `Nz` does not. `ISERROR(x)` tests for it. - **Inputs stay sparse; calculated series are dense.** An empty input stays empty; `ISEMPTY` tells it from a 0. A calculated series has a value at every combination, with empties counted as zero. ## How a series totals up Every series says, **for each of its dimensions**, how its value at a total is formed. That is the `rollUp`. **A calculated series totals like an input:** with `SUM`, a total adds up its members' values; only `FORMULA` re-evaluates the formula at the total. So `Nights Available = Days`, over apartments and months, sums to days × apartments at the business total. Choose the roll-up from what the quantity is, not by habit: | The quantity is… | rollUp | Example | |---|---|---| | An amount or a count that adds up | `SUM` (the default) | volume, cost, hours | | A 0/1 flag (`IF(Occupancy < 0.5, 1, 0)`) | `SUM`: at a total, a count | apartment-months below a threshold | | A stock or balance, across time | `LAST` across time; `SUM` across everything else | cash, inventory, outstanding loan | | An opening position, across time | `FIRST` across time | opening balance | | A ratio of two series | `FORMULA`: the formula is evaluated at the total from the total's own inputs, whatever its members hold. An **input** series needs `rollUpFormula`; a calculated one uses its own | margin %, utilisation, average price, share | | A per-unit input that totals as a weighted average | `WEIGHTED_AVG` with `weightSeries` | unit cost weighted by volume | | A rate or price, across time | `AVG` | monthly exchange rate, price | | Values that must not be combined | `NONE`: the total stays empty | kWh + m³; Actual + Budget; local currencies | | A period-on-period change, across time | `NONE` across time | month-on-month growth, which has no meaning at a quarter | **Rules that bite:** - **The order of a series' `dimensions` list is the order in which totals are calculated** where methods differ. An average of sums is not a sum of averages. - **A `FORMULA` total uses the totals of the series it reads, each by its own rule.** `Cost[Actual] / Headcount`, with headcount averaged across time, gives a quarter's cost over its average headcount. - **A `FORMULA` total needs an input on all of the series' dimensions.** An input bound on fewer (a rate by Time only) reaches no total by itself, so a formula guarded on such an input alone stays empty at its totals. - **A ratio is never `SUM` or `AVG`.** Adding margins gives nonsense; averaging them ignores size. - **A `FORMULA` total must have an answer at every level.** A formula that reaches for something a total does not have, such as a *previous month* at a quarter, fails there, and the update reports `SERIES_CELLS_FAILED … in the roll-up formula for "Time"`. Use `NONE` on that dimension, or guard with `IFERROR`. - **A weight or driver series must drive every dimension it spans.** If *Volume* varies over product and time and weights *Unit Cost* on product, it must weight it on time too. Naming a driver on a dimension whose values are loaded at its own elements moves none of them: a split-down splits only what is entered above. **And it may not have a dimension its target lacks:** a price by product and month cannot be weighted by units sold by *shop*, product and month. Then give the input a plain rule (`NONE` across products, `AVG` across time), and show the true average as a calculated ratio with `FORMULA` totals: `Average Price = Revenue / Units Sold`. ## Entering above the grain: split-down An input loaded at a total — a target per quarter, a price per country — is split to the members below it by its `splitDown` rule on that dimension: | Rule | Use for | |---|---| | `COPY` | **Rates and prices**: every member gets the same value (a country's price for each of its plants). | | `EQUAL` | **Amounts** shared evenly (a quarter's target to its three months). | | `PROPORTIONAL` + `driverSeries` | **Amounts** shared by a key (a target to products by last year's sales). | | `FIRST` / `LAST` | Everything to the first or last member. | | `NONE` (the default) | No split: the value stays where it was entered. With an active roll-up the load warns. | A split's total stays the value entered. Choose split and roll-up as a pair: `EQUAL` or `PROPORTIONAL` with `SUM`, and `COPY` with `AVG` or `NONE`. # guide/04-recipes.md # Recipes axmo-format-version: ed0892bbbdf4 Each recipe is a complete model in [`../recipes/`](https://axmo.it/llms.txt), with its data, that **imports with no error and computes the values stated beside it** (`.expected.json`). Its YAML is beside it as text (`.model.yaml`). The build checks every recipe on every change. **Start from the closest recipe** and adapt it; the patterns below are the ones you will need most. | Pattern | Recipe | |---|---| | Combining inputs of different shapes; dropping a dimension through its total; a ratio over totals; a price per country | [kitchenware-production](https://axmo.it/ai/guide/04-recipes.md#a-complete-operational-model) | | A balance carried forward from an opening value | [cash-balance](https://axmo.it/ai/guide/04-recipes.md#a-balance-carried-forward) | | A value that depends on the future | [loan-outstanding](https://axmo.it/ai/guide/04-recipes.md#a-balance-from-the-future) | | A rate that belongs to a higher level than the data | [fx-conversion](https://axmo.it/ai/guide/04-recipes.md#a-rate-held-at-a-higher-level) | | Ratios, weighted averages and shares | [pricing-and-margins](https://axmo.it/ai/guide/04-recipes.md#totals-that-are-not-sums) | | One series from two tables of different shapes; versions; variance | [actual-vs-budget](https://axmo.it/ai/guide/04-recipes.md#actual-versus-budget-from-two-table-shapes) | | A figure entered at a total and split down | [budget-split](https://axmo.it/ai/guide/04-recipes.md#a-top-down-split) | | A dimension built from a table | [generated-dimension](https://axmo.it/ai/guide/04-recipes.md#a-dimension-built-from-data) | | A generated tree with versions, a wide budget with a Total column, a ratio over an averaged headcount | [cost-centre-report](https://axmo.it/ai/guide/04-recipes.md#cost-centre-reporting-from-a-master-list) | | Comparisons, `IF`, powers, `ROUND`, `ISEMPTY`, element ranges, running and year-to-date totals | [formula-forms](https://axmo.it/ai/guide/04-recipes.md#formula-forms) | | A P&L from ledger postings; a budget by line; the last closed month found from the data; a forecast | [pl-from-ledger](https://axmo.it/ai/guide/04-recipes.md#a-pl-from-a-general-ledger) | | A headcount and salary roll-forward; a year-on-year change | `walkthrough-demo` (the axmo.it guide's model) | | A small P&L by country and year; shares; a rank | `intro` | ## A complete operational model **`kitchenware-production.axmo`** — a manufacturer with 20 products, 10 production stages, 4 resources, 4 plants in 3 countries, and 12 months. The Check sheet's bottom-right cell, **25,533.53**, was confirmed in Excel. ```yaml Stage load, h: Production Volume * Production Time / 60 Resource cost: Production Volume * Resource Usage * Resource Price Utilisation: '[Stage load, h][All_Product_Catalog] / Stage Capacity' # FORMULA on every dimension ``` - **Three inputs of different shapes meet in one formula**: volume by product, plant and month; usage by product, stage, resource and plant; price by resource, country and month. - **Utilisation has no product dimension**, so it reads stage load's all-products total. - **Prices are loaded per country**, copied to plants (`COPY`). ## A balance carried forward **`cash-balance.axmo`** ```yaml formula: Nz(Closing Balance[Month[-1]], Opening Balance) + Receipts - Payments dimensions: [{Accounts: {rollUp: SUM}}, {Time: {rollUp: LAST}}] ``` - **In the first month there is no previous month**, so `Nz` falls back to the opening balance, an input over accounts only. - **Across time a balance takes its `LAST` value**: a quarter's balance is its last month's. - **Receipts and payments come from one table by one bulk loader** (`targets`). ## A balance from the future **`loan-outstanding.axmo`** ```yaml formula: Nz(Outstanding[Month[+1]], 0) + Nz(Repayment[Month[+1]], 0) ``` The amount still owed at a month's end is everything repaid after it, so the formula refers to its own **next** value. The last month has no next one, so it is 0. ## A rate held at a higher level **`fx-conversion.axmo`** — the entity tree has a `Currency` layer above `Entity`, so the tree itself maps each entity to its currency. - **Rates are loaded once per currency** and copied down (`splitDown: COPY`). - **`Sales EUR = Sales LC * FX Rate`** is converted month by month, then summed. - **Local-currency sales are not added across entities** (`NONE`). - **A quarter's rate is the average of its months**, but a quarter's euro sales are the **sum of monthly conversions**, not the quarter's sales times the average rate. ## Totals that are not sums **`pricing-and-margins.axmo`** - **Margin %, average price and revenue share use `FORMULA`**, so at every total they are recalculated from that total's margin, revenue and volume. - **`Revenue Share = Revenue / Revenue[All_Products]`** divides by the total element. - **Unit cost is an input with `WEIGHTED_AVG` by volume** on both its dimensions. Its value at a total equals total cost over total volume. ## Actual versus budget from two table shapes **`actual-vs-budget.axmo`** — one series, **Cost**, over Departments × Versions × Time: - **Actuals** arrive as a long table, with one loader and `fixedCoordinates: { Versions: Actual }`; - **the budget** arrives as a wide table, months across the columns, read by **one** loader through the source's `columnLayout: dimension`, with `fixedCoordinates: { Versions: Budget }`; - **Versions are never added** (`NONE`); `Variance = Cost[Actual] - Cost[Budget]`, and `Variance %` uses `FORMULA`. ## A top-down split **`budget-split.axmo`** — each region's quarter target is loaded at `All_Products` and `2027-Q1`: - **To products** in proportion to last year's sales (`PROPORTIONAL`, `driverSeries: Prior Year Sales`). The driver spans products and regions, so both dimensions name it. - **To months equally** (`EQUAL`). ## A dimension built from data **`generated-dimension.axmo`** — the Customers dimension declares only its layers and `All_Customers: null`. Two generators fill it: - **`Segments`** places the segments under the root (`parentElement`); - **`Customers`** places each customer under the segment named in its row (`parentColumn`). They place as one batch, so their names do not matter ([02](https://axmo.it/ai/guide/02-model-file.md#a-dimension-built-from-data)). ## Cost-centre reporting from a master list **`cost-centre-report.axmo`** — actual cost, budget, variance, variance % and cost per head, by cost centre, department and company, by month and quarter: - **The cost-centre tree is generated** from the master list: departments under the root, and cost centres under their department (`parentColumn`). - **The budget table ends in a `Total` column**, which the band leaves out: `bandEndMode: before`, `bandEndColumn: Total`. - **Headcount sums across cost centres and averages across time** (`AVG`), so `Cost per head = Cost[Actual] / Headcount` with `FORMULA` totals gives a quarter's cost over its average headcount. ## Formula forms **`formula-forms.axmo`** — one calculated series per form in [03](https://axmo.it/ai/guide/03-formulas-and-totals.md#more-forms). Each totals with `NONE`, so read it month by month. ## A P&L from a general ledger **`pl-from-ledger.axmo`** — the *P&L Budget vs Actuals from the general ledger* demo: ledger postings by account and cost centre, against a budget by P&L line. - **The P&L tree is generated** from an Element/Parent table and the chart of accounts (each account under its line). - **The ledger loads as exported** (`duplicateHandling: SUM`) from an Excel Table (`namedStructures`), so new rows load on the next Update. **Signs belong to lines**, copied to their accounts (`COPY`). - **The budget is entered by line, above the accounts**, so it is split down (`EQUAL`). Without the split, `Budget = Budget Input * Sign`, computed at the accounts, reads nothing there, and the load warns. - **The last closed month comes from the data**, as the last month with postings (`WHERE`, in *Month Actual Index*). `IF(Month@ <= Month Actual Index, …)` leaves later months empty, and the forecast takes the budget there. # guide/05-checking-and-fixing.md # Checking a model, and fixing what the report says axmo-format-version: ed0892bbbdf4 ## Running the check ``` node tool/axmo-check.mjs model.axmo # the report; exit 0 = no error, 1 = an error, 2 = the tool failed node tool/axmo-check.mjs model.axmo --values # … then every output range's cells node tool/axmo-check.mjs model.axmo --series "Cost" # … then one series, one line per coordinate node tool/axmo-check.mjs model.axmo --series "Cost" --at "Versions=Actual;Time=2027-Q1" # … only there node tool/axmo-check.mjs model.axmo --strict # a warning fails it too (exit 1): finish with this ``` - **Node 18 or later.** The tool runs the product's own import, compile and update, over a stand-in for Excel. - **`--series` on a large series is very long**, because it prints every coordinate, totals included. Narrow it with `--at`: `;` separates the pairs, and a dimension not named prints all its elements. - **The flags and exit codes** are in [`../generated/tool-contract.md`](https://axmo.it/ai/generated/tool-contract.md). - **If you cannot download it or run code**, use the other routes in [README](https://axmo.it/ai/guide/README.md). ## Reading the report A clean report is one line: `No issues.` Otherwise there are three sections, **Import**, **Compile** and **Update**, and each issue is one line: ``` error[FORMAT_UNKNOWN_KEY]: series.Sales.formla [12:5]: unknown key | expected one of: "type", "formula", "description", "dimensions" ``` - **The code** is in [`../generated/error-codes.md`](https://axmo.it/ai/generated/error-codes.md), with its meaning. - **The path and position** point into the file. - **The forms** are all in [`../generated/message-forms.md`](https://axmo.it/ai/generated/message-forms.md). - **Import judges the file's shape; Compile judges its meaning.** A file that names something missing still imports, and the compile says what is missing. **Warnings matter.** The import succeeds and the exit code is 0 even when a whole source loaded nothing, because every label failed to match. **Read every warning**, and finish with `--strict`, which turns a warning into exit `1` and ends the report with *(checked with --strict …)*. ## Common faults and their fixes | The report says | Usually because | Fix | |---|---|---| | `IMPORT_BUILD_REFUSED` … *has a layer named "X" identical to the dimension's own name* | Dimension `Account` with layer `Account` | Plural dimension, singular layer: `Accounts` / `Account`. | | `FORMAT_UNKNOWN_KEY` | A misspelt or invented key | Use a key the message lists. | | `DEFINITION_VALIDITY_ERROR` … *varies over dimension "Time" that is not in the output scope* | An output range shows a series without one of its dimensions | Put the dimension on the rows or columns, or fix it in the expression (`Sales[2027]`). | | `DG_PARENT_NOT_FOUND` … *parent "Retail" … is not in dimension* | A row names a parent that no generator and no written element supplies | Add the parent to the tree or to a generator's source, or correct the label. | | `DG_TARGET_SEQUENTIAL` | A generator targets a time dimension | Remove it; extend the time dimension's `start` / `end`. | | `SERIES_CELLS_FAILED` … *division by zero, in the roll-up formula for "Time"* | A `FORMULA` total that has no answer at some level (a previous month at a quarter) | `NONE` on that dimension, or `IFERROR` in the formula. | | `DIMENSIONAL_INCOMPATIBILITY` | A formula yields a dimension its series lacks | Read the total (`X[All_Products]`) or filter the dimension; or add the dimension to the series. | | `SL_DIM_MAPPING_MISMATCH` | A loader maps or fixes too few or too many dimensions | Map or fix exactly the target's dimensions. | | `OR_RENDER_RUNTIME_FAILED` | Cells of a report could not be calculated; the line names the first one (its series and elements) | Check that series' formula at those elements. If it says *Axmo hit an internal problem*, it is a defect: give the user the line to report. | | `FORMAT_VALUE_NOT_ALLOWED` | A key holds a value outside its set (`bandEndMode: bfore`) | Use a value the message lists. | | `SERIES_LOADER_CROSS_LOADER_CONFLICT` | Two loaders write the same cell; the line states both values, equal or not | Make the loaders' coordinates disjoint, whatever the values. | | `SERIES_LOADER_UNKNOWN_DIM_ELEMENT` on a wide table's `Total` column | The band runs to the end and takes in `Total` | `bandEndMode: before`, `bandEndColumn: Total`. | | `SERIES_LOADER_VALUE_ABOVE_LEAVES` | Values loaded above a dimension's leaves under split-down `NONE`, roll-up active: a series calculated at the leaves does not see them | A split-down on that dimension, or roll-up `NONE` too (a fully manual load). | | A warning per label, rows skipped | Labels in the data do not match the element names | Reshape the data to the element names (Axmo does not convert labels). | ## What Axmo cannot do today Say so to the user rather than working around it silently: - **No label conversion or mapping** ([01](https://axmo.it/ai/guide/01-method.md#3-reconcile-the-sources)). A dimension cannot be read from two columns. - **No fiscal calendar**, and no time naming other than `2027`, `2027-H1`, `2027-Q1`, `2027-01` and `2027-01-30`. - **A time dimension cannot be extended from data**, only by its parameters. - **Vector and scalar loaders load nothing.** Use tables. - **Calculated series are dense.** A table-mode output prints every combination. - **Re-importing a model replaces it** ([01](https://axmo.it/ai/guide/01-method.md#changing-an-existing-model)). The check tool has no workbook, so it never shows the **Import anyway** choice. - **Rows below a fixed-address input range** are neither loaded nor exported. Use an Excel Table, which grows. ## The workbook tool `tool/axmo-workbook.mjs` takes the model out of an Axmo workbook and puts a changed one back ([01](https://axmo.it/ai/guide/01-method.md#changing-an-existing-model) gives the flow; the contract is [`../generated/tool-contract.md`](https://axmo.it/ai/generated/tool-contract.md)): ``` node tool/axmo-workbook.mjs extract book.xlsx --out work --depth complete # work/model.yaml, work/dsdata.yaml node tool/axmo-workbook.mjs apply book.xlsx work/model.yaml -o book-v2.xlsx # exit 0 written, 1 refused, 2 failed ``` - **`apply` prints the check's report first.** An Import or Compile error refuses. **Its Update section never refuses, and is expected to show lines you must not fix**: the check has none of the workbook's cells or tables, so a source on a table reads `EXCEL_REFERENCE_UNRESOLVED`, and loaders find their sources empty (`SL_COLUMN_NOT_FOUND`, then `SERIES_CELLS_FAILED` and other lines from calculating on empty inputs). Then come `Apply` and its own lines. - **`--strict`** refuses on a warning among `apply`'s own lines, never on the check's Update section. - **`extract --depth complete`** adds the `data` section (cells as last saved; a formula as its value): check values on it, then apply it; its data is reported as not applied. | `apply` says | Meaning | What to do | |---|---|---| | `APPLY_SOURCE_NOT_IN_WORKBOOK` (refused) | A source's `rangeRef` names a sheet, range, table or name the workbook does not hold | Correct the reference to one the workbook holds. | | `APPLY_DSDATA_BROKEN` | A `DS.DATA` formula no longer compiles against the new model; the line gives its cell and the message | No formula is rewritten: tell the user which cell to edit, and to what. | | `APPLY_OUTPUT_RANGE_MOVED` / `APPLY_OUTPUT_RANGE_REMOVED` | A report moved or went; its old cells stay | Tell the user which cells to clear. | | `APPLY_DATA_NOT_APPLIED` | The file's `data` section was not written | Data changes in Excel, or through Import at `complete`. | | `APPLY_OUTPUT_SHEET_NOT_IN_WORKBOOK` | A report's sheet is not in the workbook; Update does not create it | Tell the user to add the sheet before pressing Update, or anchor the report on an existing sheet. | | `APPLY_LOADER_COLUMN_NOT_IN_WORKBOOK` | A loader names a column its source does not have in the workbook | Correct the column name. | | `WORKBOOK_OBJECT_LEFT_OUT` (`extract`) | A stored object is broken; the line gives its definition error, and it is not in `model.yaml` | Do not apply: that would drop it. The user repairs it in Excel, then extract again. | | `WORKBOOK_MODEL_NOT_WRITABLE` (`extract`, refused) | A stored value the file format has no word for | The user repairs the named field in the add-in, then extract again. | | `EXTRACT_FORMULA_NOT_CALCULATED` / `EXTRACT_DATA_NOT_READ` (`extract`) | A formula cell with no saved value, or a source the file does not resolve: written empty | Check those values; ask the user to open and save the workbook in Excel. | | `WORKBOOK_NOT_AXMO` (refused) | No model in the file: not an Axmo workbook, or saved by a library that removed it | Ask for the workbook as Axmo last saved it, or for an export. | # capabilities.md # What Axmo can do with a model's data — the capability table Written by production (AGL9-20, from the AGL9-19 Part 0 census, re-measured). **Each row names one test that pins it by value** (`tests/capabilities/capabilities.test.ts`); `check:ai-pack` fails when a row names no such test or a test names no row. **The rows are part of the format version**: a changed row moves the version, and every guide file declaring the old one then fails the guide gate, naming the row — that is trigger T2. Scenario names in brackets are the test fixtures in `tests/fixtures/capabilities/`. "Row N" in a report line counts data rows from 1, without the header. | Row | Question | Works | Does not | Gap | Test | |---|---|---|---|---|---| | 1a | Several loaders into one series | Loaders with disjoint coordinates fill one series, and its roll-ups combine them (P1 10 + P2 20 = 30). | — | None. | `[row 1a]` | | 1b | Duplicate rows within one loader | `duplicateHandling`, set per target, default `FIRST`: of rows 10 and 30, `FIRST` 10, `LAST` 30, `AVERAGE` 20, `SUM` 40. `BLOCK` warns (`SERIES_LOADER_DUPLICATE_COORD_CONFLICT`), and the cell and its totals become error values. | `FIRST`, `LAST`, `AVERAGE` and `SUM` collapse duplicates with no report line. | A source with duplicate rows by mistake is collapsed silently under every policy but `BLOCK`. | `[row 1b]` | | 1b2 | Two loaders writing one cell | Always reported (`SERIES_LOADER_CROSS_LOADER_CONFLICT`), whatever `duplicateHandling` says; the cell becomes an error value. | No precedence between loaders. The conflict fires when both values are equal too, because one cell has one loader; the line states the values (*Both supplied 10*, or *"L1" supplied 10 and "L2" supplied 99*). | Sources cannot be layered, a corrections table over a base table. | `[row 1b2]` | | 1c | Actuals and budget from two tables | One series with a Version dimension, each loader fixing Version (`fixedCoordinates`): Actual 30, Budget 30, All_Version 60. Two separate series work too. | A loader that leaves a dimension of its target unmapped is a compile error (`SL_DIM_MAPPING_MISMATCH`). A series named as an element (`Actual`) is refused. | Series and element names share one namespace. | `[row 1c]` | | 2a | Several generators into one dimension | They merge as one batch, case-insensitively, without duplicates; `parentColumn` builds a layer under another generator's elements, whatever the generators' names (`Catalogue` placing under `Groups` works). One parent's elements keep the order of their first appearance: spec-time first, then by first row, generators in name order. **An element's layer** is its `layerColumn` cell where one is given, any layer strictly below its parent's (a gap allowed: `X1` at Item under All_Product); otherwise, with no column or an empty cell, its parent's layer + 1, and the top layer for an element with no parent. | A generator into a time (sequential) dimension is a compile error (`DG_TARGET_SEQUENTIAL`), and the generator does not run: a sequential dimension's elements come from its parameters only. A layer cell at or above its parent's layer is refused and its row skipped (`DG_LAYER_PARENT_MISMATCH`); two rows giving one element different layers keep the first (`DG_LAYER_REDEFINITION`). A generator never creates a layer. | A time dimension cannot be extended from data; change its parameters (`start`, `end`). | `[row 2a]` | | 2b | How labels match element names | Trimmed and case-insensitive, inner spaces kept, for cell labels and column headers alike (`' P2 '`, `' 2026-02 '`, `ACTUAL`); a number matches by its digits (2025 and `'2026'` both match). | No format conversion: `2026-1`, `Jan 2026`, `Jan-26`, a date serial, `2026-03-01`, `FY2026` do not match, and the row is skipped with a warning. | A source whose period labels differ from the element names must be reshaped first. A date-typed period column in Excel never matches. | `[row 2b]` | | 2c | Mapping a source column or label to a dimension | Any column maps to any dimension (`dimensionMappings: {Time: {column: PERIOD}}`); a dimension can be fixed for a whole loader. | No label-to-element mapping (no alias, lookup or transform). A dimension cannot be read from two columns (Year + Month). | Recoding happens outside Axmo, or in Excel formula columns the input range reads. | `[row 2c]` | | 2d | A row whose label matches no element | Skipped, one warning per distinct label (`SERIES_LOADER_UNKNOWN_DIM_ELEMENT`); empty dimension cells, non-numeric values and missing columns are reported too. | When every row fails, the source loads nothing and the import still exits `0`, with warnings only. | Read the warnings, not only the exit code. | `[row 2d]` | | 3 | Table shapes | Long (dimension columns and one value column). Measures as columns, into several series, with one loader. **Months across columns with one loader**: the data source's `columnLayout: dimension`, `columnsDimension: Time` and a band (`bandStartMode` / `bandEndMode` with their columns), the loader mapping Time to `_HeaderDimElement` and reading `_Value` (P1 1, 2, 3; quarter 6). Rows at a quarter or a year with a split-down rule (`EQUAL` 90 → 20 a month). Rows at mixed levels under the default `splitDown: NONE` stay where they are loaded and reach every total (P1 at `2026-Q1` 30 and P2 at `2026` 60: `All_Product / 2026` 90, `All_Product / 2026-Q1` 30, months empty), at any depth (over `All_Restaurants › Nord (Milano, Torino) › Sud (Napoli)`, Milano at `2026-Q4` 30, Torino at `2026-11` 5 and `2026-12` 6, Napoli at `2026` 12: `All_Restaurants / 2026-Q4` 41, `All_Restaurants / 2026` 53). | Months across columns in one loader **without** `columnLayout` is a compile error. Vector and scalar loaders load nothing (`SL_LOAD_MODE_NOT_FED`). A parent row and its child row in one load collide (`SERIES_DATACELL_COLLISION`). | Vector and scalar sources are unusable. | `[row 3]` | | 4 | Dimensions, and time element names | Static trees and generated elements. `axmo_time` names: `2026`, `2026-H1`, `2026-Q1`, `2026-01`, `2026-01-30`, under `All_Time`; optional `BEFORE` / `AFTER`. | No other naming (no `Jan 2026`, no fiscal years). A layer may not carry its dimension's name. | Period labels in a source must be exactly those forms. | `[row 4]` | | 5 | Combinations that never get data | An empty input cell stays empty; a loaded 0 is 0; `ISEMPTY` tells them apart. | Calculated series count empty as zero, so they are dense: 120 of 120 cells, the whole Budget version included. | Reports over sparse data show long runs of zeros. | `[row 5]` | | 6 | Export depths and re-import | `definition`: dimensions and series. `bindings`: and the data sources, loaders and output ranges. `complete`: and the cells of each data source's range. | Generated elements are not in a `definition` export (`All_Product: null`). A re-import replaces input the user changed after the export; before it does, the Import dialog names how many input cells differ from the file, and in which input ranges, and the user chooses **Import anyway** or **Cancel** (`axmo:check` has no such workbook, so it never shows this). | A definition export of a model with generated dimensions arrives with empty dimensions; re-importing an older file still replaces newer input when the user chooses **Import anyway**. | `[row 6]` | | 7 | A total rolled up by `FORMULA` | The formula (the series' own, or `rollUpFormula`) is evaluated at the total from the total's own inputs, whatever its members hold: `IF(not (ISEMPTY(Marketing)), Marketing / Sales)`, with Marketing loaded only at `Milano / 2026-Q4` (30) and Sales 220 there, is empty in every month and 0.136 at the quarter and its totals. A total is reached where a member holds a value, or where an input of the formula bound on every dimension of the series does. Over several dimensions with different methods, each total is produced in the series' roll-up order. | An input bound on fewer dimensions (a rate by Time only) reaches no total by itself. Every other method leaves a total whose members hold nothing empty. | A guarded formula whose only input is bound on fewer dimensions stays empty at its totals. | `[row 7]` |