Model spec format
A ModelSpec is the declarative JSON document that describes a Valem model. It is
typically produced by an LLM (via SpecGenerator) and then compiled, validated, and executed
by the runtime. The spec is stored verbatim; it is reloaded and recompiled whenever
valem.persistence-dir is set.
This page is the map: the top-level structure, worked examples, and the path notation that runs through every section. Each section has its own page, because a single 1,400-line reference is harder to search than five focused ones.
| Section pages | Covers |
|---|---|
| Schema, constants & defaults | schema (with $defs / $ref), constants, defaultValues |
| Derivations, meta & constraints | derivations, metaDerivations, constraints |
| Effects | effects — executors, triggers, dedupe, status paths, fold-back |
| Tests & spec evolution | tests, SpecEvolution, targeted section diffs |
| View definition & components | viewDefinition, ViewSpec, ComponentSpec, EventHandler, EvaluatedView |
| Component type catalog | Every built-in component type and the fields it accepts |
Prefer prose first? The Model guide explains the same material by concept rather than by field.
Top-level structure
{
"id": "my-model",
"version": "1.0.0",
"template": { "ref": "<coordinate>" },
"lineage": [ ... ],
"schema": { ... },
"constants": { "name": <any JSON value>, ... },
"defaultValues": [ ... ],
"derivations": [ ... ],
"metaDerivations": [ ... ],
"constraints": [ ... ],
"effects": [ ... ],
"tests": [ ... ],
"viewDefinition": { ... }
}
| Field | Required | Default | Description |
|---|---|---|---|
id |
yes | — | Model identifier, validated as a ModelCoordinate: [namespace/]name, each segment starting with a letter (letters/digits/-/_), no @. |
version |
no | "1.0.0" |
Version string; when non-blank must be valid semver (MAJOR.MINOR.PATCH) — a non-semver value is rejected at validation. |
template |
no | null |
{ "ref": "<coordinate>" } — branches this model from another owner’s template. POST /models flattens (materializes) the referenced template into a self-contained inlined spec before creation; see composition/branching below. |
lineage |
no | [] |
Read-only, materializer-written: the pinned ancestor chain this model was branched from ([] for a non-branch model). Validated acyclic. Exposed at GET /models/{id}/lineage. |
schema |
yes | — | JSON Schema (Draft 2020-12) for the base document |
constants |
no | {} |
Named immutable values (any JSON type), bound as $const in every expression |
defaultValues |
no | [] |
Default rules for newly-created containers; a $ rule seeds at creation |
derivations |
no | [] |
Computed read-only fields |
metaDerivations |
no | [] |
Per-field metadata (min/max/required/…) |
constraints |
no | [] |
Boolean invariants with violation policy |
effects |
no | [] |
Effect requests emitted by the core, run by a shell (caller/server/llm/timer/plugin); replaces the removed actions |
tests |
no | [] |
Embedded spec-level test cases |
viewDefinition |
no | null |
Declarative UI form definition |
Composition/branching. A model created with template set is a branch: the API materializes
(flattens) the referenced template’s spec into the new model’s own inlined spec, pins the ancestor
chain into lineage, and validates the result acyclic. Branching across an ownership boundary (the
new model’s owner differs from lineage’s fromOwner) quarantines any inherited non-caller effect
until approved — see security-model.md and
valem.authz.inherited-effects in configuration.md. Models can also be
promoted between repositories (POST /models/{id}/promote) and queried for cross-model topology
(GET /composition/graph). The end-to-end lifecycle (branch → approve inherited effects → promote,
plus links between models) is walked through in the
composition & branching guide.
No per-field access control. Valem has no
fieldAccess/roles. Any caller with model access reads/mutates/evolves every field; access is a single coarse gate (valem.api.key). See security-model.md.
Complete examples
Customer Satisfaction Survey
Demonstrates:
radioFieldfor a 1–5 ratingcheckboxFieldfor boolean questionsselectFieldwith static optionstextAreaFieldfor free textemailFieldseparatorLinegroupwith horizontal layoutlabelandbadgewith JSONatatextandvariantexpressionsmetaDerivationsthat automatically hide/lock fields (relevant,readOnly)
Key pattern — view components have no visible or readOnly expressions. Visibility
is driven entirely by metaDerivations on $.issueCategory and $.contactEmail:
"metaDerivations": [
{ "path": "$.issueCategory", "property": "relevant",
"expr": "issueEncountered = true" },
{ "path": "$.issueCategory", "property": "readOnly",
"expr": "issueEncountered != true" }
]
The issueCategoryField component has no visible field:
{
"id": "issueCategoryField", "type": "selectField",
"label": "Issue Category", "bind": "$.issueCategory",
"options": [ ... ]
}
When issueEncountered = false, the relevant meta value is false → the evaluator
sets visible: false automatically.
Order Line Items
Demonstrates:
dataTablewithtableColumnsincluding"format": "currency"separatorLinelabelshowing the derived$.total- Wildcard derivation
$.items[*].lineTotal(per-row computation via$parent)
Path notation
There are two notations, and which one applies depends on where the path appears:
Addresses — JSON Path (RFC 9535). Anywhere a path is used as data — spec path fields
(derivations, metaDerivations, defaultValues), mutation/patch keys, the path portion of a meta
key, and view bind — use canonical JSON Path: $.-rooted, with bracket array indices.
| Notation | Meaning | Example |
|---|---|---|
$.field |
Top-level field | $.total |
$.parent.child |
Nested field | $.order.status |
$.items[0].name |
Array element by index | First item’s name |
$.items[*].lineTotal |
Wildcard — all elements | Per-row derivation path |
Expressions — JSONata. Inside a JSONata body (expr, trigger, payload/emit, and the
JSONata view fields), use whatever navigation JSONata accepts; the $. prefix is omitted and
dot-notation is used directly (e.g. items.(price * qty)). Expression bodies are never rewritten
or constrained — only compiled.
Address dialect is enforced. The validator rejects non-canonical addresses as errors: the legacy dot-index form (
$.items.0.name) and unrooted forms (items.0.name) are not accepted — use the bracket form ($.items[0].name). The error message includes the canonical rewrite. Expression bodies are unaffected (any JSONata navigation is allowed).