Copy-paste spec examples
Three minimal, self-contained ModelSpec documents, each with the exact command to create and drive
it. They are deliberately small — no viewDefinition, no effects — so you can paste one, watch it
compute, and grow it from there. For the full field reference see
Model spec format; for larger, view-rendered specs see
the Examples gallery.
- The shape of every spec
- 1 — One derivation (the smallest useful model)
- 2 — Add a constraint
- 3 — An array with per-row and rollup derivations
- Create the same model from an AI agent (MCP)
- 4 — A spec that proves itself: embedded tests
- 5 — Evolve a live model without losing its state
- Where to go next
The shape of every spec
A ModelSpec is a JSON document. The three fields that matter for a first model:
schema— a JSON Schemaobjectnaming your fields. Mark derived fields"readOnly": true; you never write them.derivations— formulas that compute a field from others, addressed by JSON Path and expressed in JSONata. Valem recomputes them in dependency order whenever an input changes.constraints— boolean invariants checked after derivations settle; arollbackpolicy reverts the whole mutation, aflagpolicy commits but reports the violation.
You write only base fields; Valem maintains the rest.
1 — One derivation (the smallest useful model)
An invoice whose total is always subtotal × (1 + taxRate). Write subtotal and taxRate; read a
total that is never stale.
{
"id": "invoice",
"version": "1.0.0",
"schema": {
"type": "object",
"properties": {
"subtotal": { "type": "number", "minimum": 0 },
"taxRate": { "type": "number", "minimum": 0 },
"total": { "type": "number", "readOnly": true }
}
},
"derivations": [
{ "path": "$.total", "expr": "subtotal * (1 + taxRate)" }
]
}
Create it and drive it over the REST API:
# 1. create the model
curl -X POST localhost:8080/models -H 'Content-Type: application/json' -d @invoice.json
# 2. write base fields
curl -X POST localhost:8080/models/invoice/mutations \
-H 'Content-Type: application/json' \
-d '{ "$.subtotal": 100, "$.taxRate": 0.2 }'
# 3. read the merged, consistent state
curl localhost:8080/models/invoice/state
# → { "subtotal": 100, "taxRate": 0.2, "total": 120 }
2 — Add a constraint
The same pattern with an invariant that cannot be violated. A budget where remaining = income −
spending and spending is never allowed to exceed income: the rollback policy makes the offending
mutation fail with 409 Conflict and leave the model untouched.
{
"id": "budget",
"version": "1.0.0",
"schema": {
"type": "object",
"properties": {
"income": { "type": "number", "minimum": 0 },
"spending": { "type": "number", "minimum": 0 },
"remaining": { "type": "number", "readOnly": true }
}
},
"derivations": [
{ "path": "$.remaining", "expr": "income - spending" }
],
"constraints": [
{
"id": "no-overspend",
"expr": "remaining >= 0",
"message": "Spending cannot exceed income",
"policy": "rollback"
}
]
}
curl -X POST localhost:8080/models -H 'Content-Type: application/json' -d @budget.json
curl -X POST localhost:8080/models/budget/mutations \
-H 'Content-Type: application/json' \
-d '{ "$.income": 3000, "$.spending": 3200 }'
# → 409 Conflict — "Spending cannot exceed income"; the model stays as it was.
Switch "policy" to "flag" and the mutation commits instead, with the violation reported under
flaggedConstraints in the response — commit-but-warn rather than block.
3 — An array with per-row and rollup derivations
Line items with a per-row lineTotal and a grand total. $parent in a row derivation refers to the
enclosing array element; $sum(...) rolls the rows up.
{
"id": "order",
"version": "1.0.0",
"schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"price": { "type": "number", "minimum": 0 },
"qty": { "type": "integer", "minimum": 1 },
"lineTotal": { "type": "number", "readOnly": true }
},
"required": ["name", "price", "qty"]
}
},
"total": { "type": "number", "readOnly": true }
}
},
"derivations": [
{ "path": "$.items[*].lineTotal", "expr": "$parent.price * $parent.qty" },
{ "path": "$.total", "expr": "$sum(items.(price * qty))" }
]
}
curl -X POST localhost:8080/models -H 'Content-Type: application/json' -d @order.json
curl -X POST localhost:8080/models/order/mutations \
-H 'Content-Type: application/json' \
-d '{
"$.items[0].name": "Apple", "$.items[0].price": 1.5, "$.items[0].qty": 4,
"$.items[1].name": "Bread", "$.items[1].price": 2.75, "$.items[1].qty": 2
}'
curl localhost:8080/models/order/state
# → items[0].lineTotal = 6, items[1].lineTotal = 5.5, total = 11.5
Create the same model from an AI agent (MCP)
Every spec above is the spec argument to the create_model MCP tool, so an agent
paired via valem-mcp creates and drives it with the same JSON:
// tools/call → create_model
{ "name": "create_model", "arguments": { "spec": { /* any ModelSpec above */ } } }
// then mutate and read back
{ "name": "mutate", "arguments": { "id": "invoice", "mutations": { "$.subtotal": 100, "$.taxRate": 0.2 } } }
{ "name": "get_state", "arguments": { "id": "invoice" } }
Before pushing a spec, an agent can vet it offline with the pure authoring tools — validate_spec
(structural check), test_spec (run embedded tests), and dry_run (apply mutations without
committing). These always run against local core, even in remote mode. See the
MCP tools reference.
4 — A spec that proves itself: embedded tests
Everything above is checked by a human reading the output. Move that check into the spec and the model verifies itself on every create, every evolution, and every CI run — which is what makes a generated spec safe to trust without reading it line by line.
{
"id": "payroll",
"version": "1.0.0",
"schema": {
"type": "object",
"properties": {
"gross": { "type": "number", "minimum": 0 },
"taxRate": { "type": "number", "minimum": 0, "maximum": 1 },
"tax": { "type": "number", "readOnly": true },
"net": { "type": "number", "readOnly": true }
}
},
"defaultValues": [
{ "path": "$", "expr": "{ 'gross': 50000, 'taxRate': 0.3 }" }
],
"derivations": [
{ "path": "$.tax", "expr": "$round(gross * taxRate, 2)" },
{ "path": "$.net", "expr": "$round(gross - tax, 2)" }
],
"tests": [
{
"description": "30% on 50,000 leaves 35,000",
"given": { "$.gross": 50000, "$.taxRate": 0.3 },
"expect": { "$.tax": 15000, "$.net": 35000 }
},
{
"description": "a zero rate is a no-op",
"given": { "$.gross": 50000, "$.taxRate": 0 },
"expect": { "$.net": 50000 }
}
]
}
// Run the embedded cases before creating anything — the agent-side pre-flight check. Pure, local,
// and available even when the MCP server is pointed at a remote Valem.
{ "name": "test_spec", "arguments": { "spec": { /* the spec above */ } } }
# For a model that already exists, the same cases back the trust report:
curl -s localhost:8080/models/payroll/verification
# → { "state": "green", "checkedCount": 2, "passedCount": 2, "unverifiableCount": 0, "cases": [...] }
Expected values must match the engine exactly, so the honest way to author them is to write the scenario, run it, and paste back what the engine produced — then read those numbers against the rule you are modelling. A test you derived from the model proves consistency, not correctness, and the verification report says so in as many words. That distinction is the whole subject of how we verify the published calculators.
5 — Evolve a live model without losing its state
Specs are not immutable. evolve_spec applies a targeted diff and carries existing state forward,
so an agent can add a field mid-conversation rather than rebuilding.
// tools/call → evolve_spec (or POST /models/payroll/spec/evolve)
{
"name": "evolve_spec",
"arguments": {
"id": "payroll",
"evolution": {
"newVersion": "1.1.0",
"expectedVersion": "1.0.0",
"upsertSchemaNodes": [
{ "path": "$.pensionPct", "schema": { "type": "number", "minimum": 0, "maximum": 1 } },
{ "path": "$.pension", "schema": { "type": "number", "readOnly": true } }
],
"upsertDerivations": [
{ "path": "$.pension", "expr": "$round(gross * pensionPct, 2)" },
{ "path": "$.net", "expr": "$round(gross - tax - pension, 2)" }
]
}
}
}
expectedVersion makes the evolution a compare-and-swap: a concurrent edit gets a 409 rather than
silently winning. Re-running test_spec afterwards is what tells you the change did not break a
rule the model already guaranteed.
Where to go next
- Add a UI: give the spec a
viewDefinitionand it renders as a reactive form. - Add reactions: effects fire HTTP/LLM/timer calls post-commit and fold the result back into state.
- Let an LLM write the whole spec from a sentence: Generating specs with an LLM, or just describe it in the sandbox.