Apply model patch
layerz_patchMutate a model with batched ops (create/replace/update/delete/move/list_create/list_update/list_delete/meta). ops must be JSON objects with a string op field, never JSON-encoded strings. The batch is all-or-nothing: if one op fails validation or a replace migration is incompatible, nothing is persisted. Give a create op a temp id and reference it as parent/source/target/series on later ops in the same batch (wire references before the real UID exists). Temp ids also resolve inside FORMULAS ($_my_id * 1.2), matching whole tokens only, so a full dependency chain ships in one batch — including a SELF-reference: a create may reference its own temp id (or its own name) in its formula, e.g. {op:"create",id:"_y",name:"Annee",formula:"IF($_y_Y-1 = 0, $start, $_y_Y-1 + 1)"} — no dummy-formula + update round-trip. To swap a referenced item atomically, prefer replace over create+rewire+delete. Hierarchy rules: only section and dashboard live at root; section accepts every role except chart/kpi/mini_table AND can be nested inside another section to model true sub-sections (use this for structural grouping with no values); dashboard accepts ONLY chart/kpi/mini_table; balance and formula-with-no-expression act as aggregators that sum their children (use a no-expression formula when you need an aggregate VALUE — e.g. "Total Revenue" — and use a nested section when you just need structural grouping with no computed value); a formula is an aggregator XOR a transformation — giving it BOTH an expression and children is rejected (FORMULA_EXPR_AND_CHILDREN); assumption and callup are leaves. Callup role: a callup is a typed mirror of ONE source whose value flows INTO its parent container — use it for balance flows (a flow attached as a callup child of a balance) and for surfacing a value inside a section/subtotal. Set source to the source item's UID (or a temp id from the same batch). The callup inherits its source's display_name and timeline_ref — do not set timeline_ref on a callup. Do NOT use a callup for arithmetic: inside a real formula, reference the source directly (e.g. Revenue * 1.2) rather than mirroring it through a callup first. On create/replace, a formula whose expression is a single bare reference with no timeline_ref override is auto-posed as a callup mirror, so a plain pass-through like formula:"Revenue" becomes callup{source:Revenue}. Stock-flow modeling (rolling balances — cash, ARR, retained earnings, debt, fixed assets): (1) create a balance with opening_balance: X; (2) for each flow, create a formula whose value is the per-period delta — outflows must return negative values; (3) attach each flow as a callup child of the balance with source: <flow_uid>; (4) if a flow depends on the balance itself (interest on debt, churn on ARR), use the lag suffix _M-1 / _Q-1 / _Y-1 on the balance UID to read the previous-period value and break the cycle. A balance without callup children stays flat at opening_balance forever — that is almost always a modeling bug. List mode (per-element series): three coexisting concepts must not be confused — (a) "Dataset entity" (plan/actual/forecast, see layerz_import_branch) is a stacked input version of the model; (b) the "Lists registry" (list_create/list_update/list_delete) is a named registry of elements (Functions, Products, Channels, Debt Tranches, Employees…); (c) "list-mode items" are assumption/formula/balance items that carry liste_ref and expand to one virtual instance per element. The pre-MDK 0.2.0 layer role is gone. Pattern: (1) {op:"list_create",id:"_functions",name:"Functions",items:[{id:"_eng",label:"Engineering"},{id:"_sales",label:"Sales"}]} — list_create is idempotent by name; both the list itself and each item can carry an id (temp_id) usable from later ops in the same batch. (2) Bind on the consumer with liste_ref:"_functions" (temp id), display_name, or the returned UID. (3) Populate per-element data via timeline_values keyed by element UID, label, or item temp_id: value = null | number | (number|null)[]; a scalar is auto-wrapped into a 1-element array. Passing scalar value/values on a list-mode item is rejected — those fields are silently dropped at compute time. Balance-list rule: when a balance carries liste_ref, every list-typed child MUST share the same liste_ref (mismatch → validator-outputs.ts: "Children of a list-mode balance must share the same list or be scalar"); scalar children broadcast across all elements. Cross-list bridging is declared on the list element itself via list_update with items:[{label:"Direct",mapped_to:{Products:["Pro","Enterprise"]}}] (channel Direct rolls up products Pro and Enterprise). Values — two regimes, keyed on liste_ref: (a) LIST-MODE item → timeline_values keyed by element UID/label/temp-id IS the normal write path (see List mode above); value/values are rejected. (b) NON-LIST item → type a plan/forecast with value (scalar, broadcast to every period) or values (array at the item’s grain); the patch surface never writes timeline_values there, nor on a computed line (formula/balance) — imported actuals/overrides are import-owned (use layerz_import_branch or the Sources import). A one-cell edit uses a period-keyed map {"2026-06":[val]} (also "2026" / "2026-Q1"): it merges into the typed values series, leaving the other periods intact. Formula language: operators + - * / ^ ( ) (^ = power, Excel semantics: left-associative, binds tighter than *) and comparisons > < >= <= = !=. Scalar functions: IF, AND, OR, NOT, MAX, MIN, ABS, POW, POWER, EDATE, YEAR, MONTH, IN_PERIOD, NPV, IRR, PMT, IPMT, PPMT. IRR/NPV consume a FULL series and must be the item's ENTIRE formula, their series argument a single item reference — IRR($fcfe), NPV($rate, $fcfe); nesting them in a larger expression or passing an expression as the series is rejected (SCALAR_FUNCTION_MISUSE); the result is a single timeless value (timeline constant). IRR($fcfe, CUMULATIVE) is the temporal variant: one IRR per period over the series from the start through that period (the project-finance run-up line; 0 before convergence). Per-period tokens: PERIOD_YEAR (calendar year of the evaluated period — date-gate lines with IF(PERIOD_YEAR >= commissioning_year, ...), no hand-rolled year counter needed) and PERIOD_INDEX (0-based position on the item's timeline). Aggregate functions over a UID/list: SUM(operand), AVG(operand), COUNTA(operand), SUMIF(operand, condition_ident OP number), COUNTIF(...), AVGIF(...) — OP ∈ < <= > >= = !=, RHS must be a numeric literal. The magic identifier CURRENT_PERIOD returns the current period's end as a date serial — use it for time-gated patterns: store Hire Date as a date serial and write IF(Hire Date <= CURRENT_PERIOD, salary, 0) for hire ramps, contract activation, ramp-up, depreciation windows. The magic identifier LIST_INDEX (list-mode formulas only) is the current element's 0-based position in the list — auto-populate age/seniority offsets: cohort_lag = LIST_INDEX, then retention = retention_curve_M-$cohort_lag reads the curve at each cohort's age with no manual per-element inputs. Lag suffix on any UID or back-ticked name: _M-N, _Q-N, _Y-N (e.g. cash_eop_M-1, `Total Revenue`_Y-1); variable lag _M-$delay reads the offset from another item ($ + name or uid; rounded, clamped ≥ 0, converted at the reading item's grain). In a list-mode formula a list-dimensioned offset resolves PER ELEMENT (each element shifts by its own lag — the ramp-up/cohort primitive); a list offset that cannot project onto exactly one element is rejected (LIST_LAG_REF_AMBIGUOUS). Self-referencing lags are legal and the standard way to break instant cycles. Subscript syntax: Item[`key`] slices a list-dimensioned item by a list-item UID/label of its liste_ref (returns that element's scalar series), or by a key from any list reachable via mapped_to (auto-aggregates the matching source items — equivalent to a SUMIF over mapped_to). Lag suffixes also apply to a subscript: Salary[`Sales`]_Y-1, and Item[i]_M-1 is the current element's previous-period value — the per-element roll-forward (Headcount[i] = Hires[i] + Headcount[i]_M-1); a bare lagged self-ref in a list-mode formula is the list TOTAL re-added into every element (×N compounding, warning BARE_SELF_LAG_IN_LIST_FORMULA) — use [i]_M-N. MDK 0.7.0: a bare reference to a list-dimensioned item is its TOTAL (sum across the dimension) in EVERY context; wrap in SUM(...) only to also collapse across time. For the per-element value inside a list-mode formula (one whose own liste_ref is set), use the current-element subscript Item[i] — same-list → that element, cross-list → the items mapped to it. Example allocation: G&A[i] = Shared G&A * Revenue[i] / Revenue (per-element numerator, bare total denominator). Do NOT introduce a scalar callup to get a total — a bare reference already is the total. Not supported: string literals (use backtick-quoted identifiers or numeric literals), SUMIFS / COUNTIFS with multiple criteria (compose with a list-mode formula instead), user-defined functions, ternary ?: (use IF(...)). KPI items targeting monthly data may use annual shorthand like 2028, which normalizes to 2028-12. The meta op sets model identity: name, subtitle, finance_md, timelines, formats, default_branch, and glyph — the model emblem, a kebab-case Lucide icon name (https://lucide.dev/icons) validated at write time (unknown names rejected with suggestions; aliases like cash→banknote normalized; null → monogram). Formula-list (cross-dimension aggregation): a scalar formula whose operand carries a liste_ref auto-aggregates that operand by sum across its dimension; combined with mapped_to on the operand list, the subscript Operand[`key`] rolls up the source elements mapped to key (a SUMIF over the mapping). This is the most powerful Layerz pattern: prefer it over a parallel "code" column + SUMIF(..., = N). Inspect existing mappings via layerz_read (each list exposes mappings per element). Chart list rendering: list_mode (chart field) picks how list-backed series render — 'split' (default) explodes EVERY list-backed source into one series per list element (a single-source chart becomes the stacked breakdown, titled after the source; a multi-series chart prefixes each element with the item name — two 10-element lists = 20 legend rows), 'total' plots each item's aggregate as ONE series (chart a list total directly, no $item * 1 mirror formula needed). Pass compute with UIDs to get calculated values back. Not available for read-only API keys.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| ops | Yes | Mutation operations. Each op is a discriminated object on a string `op` field (e.g. {op:'create', role:'assumption', name:'Revenue'}; {op:'replace', from_uid:'abc', new_item:{role:'formula', name:'Revenue', formula:'xyz * 12'}}; {op:'update', uid:'abc', value:42}). | |
| compute | No | UIDs to compute after mutations | |
| dry_run | No | Validate and compute the batch without persisting it | |
| summary | Yes | Required: short human-readable note (3–100 chars) summarising what this change does. Shown as the label in the model version history. Be specific (e.g. "Renamed Revenue to Net Revenue", "Imported 2024 OPEX from Excel"). | |
| model_id | No | Target model UUID. Required for user-scoped API keys; ignored (or validated against scope) for model-scoped keys. | |
| compute_branch | No | Return the `compute` values under a single branch's `[base, branch]` checkout instead of the stacked-all view (mirror of `layerz_read` `branch_id`). Pass `default` to audit the base plan alone — e.g. verify a base correction without a live import layer polluting the result. Requires `compute`. Does not change where the write lands (always the base). |