Anatomy of a model
A ModelSpec is a declarative JSON document — often LLM-generated — that fully describes a reactive model: its shape, its formulas, its invariants, and its side effects. Valem compiles it into a live runtime.
- A complete small spec
- The sections
- Sharing values and computation
- Base vs derived vs meta
- Addresses vs expressions
- What compilation checks
- Next
A complete small spec
{
"id": "order",
"version": "1.0.0",
"schema": { "type": "object" },
"constants": { "taxRate": 0.08 },
"defaultValues": [ { "path": "$", "expr": "{ 'currency': 'USD' }" } ],
"derivations": [ { "path": "$.tax", "expr": "subtotal * $const.taxRate" },
{ "path": "$.total", "expr": "subtotal + tax" } ],
"metaDerivations":[ { "path": "$.subtotal", "property": "minimum", "expr": "0" } ],
"constraints": [ { "id": "max-order", "expr": "total <= 5000",
"message": "Order exceeds the cap", "policy": "rollback" } ],
"effects": [ { "id": "large-order-alert", "executor": "caller", "trigger": "total > 1000",
"emit": "large-order", "payload": { "total": "total" } } ],
"tests": [ ]
}
Nothing here is procedural. Each section states a fact about the domain; the runtime works out when to apply it.
The sections
| Field | What it does |
|---|---|
id, version |
Identity of the model and its spec version. |
schema |
JSON Schema for the base (writable) document. Local $defs / $ref are supported. |
constants |
Named immutable values (any JSON type), bound as $const.<name> in every expression. No dependency edge — a derivation reading only $const never recomputes, so reference an input alongside it. |
library |
Named JSONata functions and values, bound as $name(...) in every expression — the function-level counterpart of constants. A library computes only from its arguments and $const; it cannot read the model document. |
defaultValues |
(path, expr) rules that deep-merge into a newly-created container (array element, object, or root $), filling only caller-absent fields. A $ rule seeds the root at creation. |
derivations |
Read-only computed fields. Evaluated in topological level order, so a later level can read an earlier one. Wildcard paths ($.items[*].lineTotal) evaluate once per element with $parent bound to that element. |
metaDerivations |
Live per-field metadata (min / max / required / …) overlaying the effective schema — so a limit can depend on state. |
constraints |
Boolean invariants with a rollback (reject the mutation) or flag (record a violation) policy. |
effects |
Requests the pure core emits as data, executed post-commit by a shell: caller (returned inline), server (HTTP behind an SSRF guard), llm, timer, or a custom plugin kind. Replay never re-runs I/O. See Effects. |
tests |
Embedded spec-level test cases the runtime can execute — versioned with the rules they check. |
viewDefinition |
An optional renderer-agnostic UI component tree. See Views. |
Field-by-field detail for every one of them — including every effect option and the full view component catalog — is in the model spec format reference.
Sharing values and computation
Two sections exist so a spec doesn’t repeat itself, and they are counterparts: constants shares
values, library shares computation. Both bind into every expression the model
evaluates — derivations, meta-derivations, constraints, defaultValues, effects, embedded tests and
view expressions alike.
"constants": { "vatRate": 0.22 },
"library": "( $money := function($n) { $round($n, 2) }; [\"money\"] )",
"derivations": [ { "path": "$.vat", "expr": "$money(net * $const.vatRate)" } ]
A library’s definition is plain JSONata: it binds names and returns the list of names to export. Reach for one when the same shape shows up in three or more expressions — a rounding convention, a bracket walk, a proration. Wrapping a one-liner is worse than the line it replaces.
A library function cannot read the model document. Its only inputs are its arguments and
$const; a bare field name in a function body evaluates to nothing, and validation rejects it.
That is a deliberate design decision rather than a compiler limitation. Dependency edges are
extracted from the text of each expression, and Valem cannot see inside a callee — so a function
that could read the document would create dependencies nothing recorded, and the derived value would
go silently stale. Because every document value has to be passed in at the call site, the field
names stay in the caller’s own expression, where the extractor sees them, and a library call ends up
with exactly the edges the inlined logic would have had.
$netTotal() // ✗ reads order.subtotal inside the function — rejected
$netTotal(order.subtotal, order.discount) // ✓ edges on subtotal and discount, as if inlined
A library is stored as an ordered list of layers: the model’s own, preceded by any it inherited
by branching from a template. Later layers win a name collision, so a
branch can both call and override an inherited function. The merged vocabulary — every export with
its kind, signature, arity and originating layer — is readable at GET /models/{id}/library.
Full rules, the validator’s checklist, and the lambda-body syntax gotcha:
schema, constants, library, defaultValues.
Base vs derived vs meta
Three kinds of node, and the distinction matters everywhere else:
- BASE — writable. The only thing a mutation may target.
- DERIVED — computed by a derivation. Read-only; writing one is an error, not an override.
- META — meta-derivations, plus synthetic nodes for each constraint and effect, which is how those participate in the same dependency graph.
Reads return the merged document: the base document with all derived values spliced in. Callers generally shouldn’t care which is which — that’s the point.
Addresses vs expressions
Two distinct dialects, and Valem keeps them strictly separate. Mixing them up is the single most common authoring mistake.
Addresses — paths used as data: path fields, defaultValues paths, mutation keys, view
bind. These must be canonical JSON Path: $.-rooted, bracket array indices.
$.order.items[0].qty ✅
$.order.items[*].qty ✅ (wildcard — evaluated per element)
$.items.0.x ❌ rejected (legacy dot-index)
order.items[0].qty ❌ rejected (not rooted)
Expressions — the bodies of expr, trigger, payload, and the view’s dynamic properties.
These are JSONata, evaluated against the merged document, and are never
rewritten:
order.subtotal + order.tax
items[qty > 0].(price * qty) ~> $sum()
riskBand = 'high' ? 250000 : 1000000
Useful bindings inside expressions: $const.<name> for constants, $name(...) for a library export,
$parent for the containing element of a wildcard derivation, and $self for a container’s
caller-provided fields in defaultValues.
What compilation checks
Creating (or evolving) a model is a validation gate, not just a parse:
- Every address is canonical and resolvable; expressions compile.
- Every
$nameresolves — against the JSONata built-ins, thelibrary’s exports, and Valem’s own bindings. An unresolvable call is an error (it would fail at runtime and leave a blank field); an unresolvable value reference is a warning, since an unbound variable is legal JSONata. - The
librarydefinition compiles, exports at least one name, reads no document field, and collides with no built-in or Valem binding. - The dependency graph is acyclic — a cycle is rejected, not detected at runtime.
- The schema’s local
$ref/$defsresolve; non-local or dangling refs are rejected. - The
viewDefinitionparses, ids are unique, anddefaultView/itemViewname existing views. - A spec still carrying the removed
initialStateoractionssections is rejected with a pointer to its replacement (defaultValuesandeffectsrespectively).
Failures come back as a 422 with locations — which is exactly what makes
generate-then-repair work, and what the MCP
validate_spec tool exposes to an agent before anything is created.
Next
- The reactive pipeline — what happens when you mutate.
- Model spec format — the authoritative reference.
- Examples gallery — working specs to start from.