Model format
The compact JSON a model is written in. Types, inputs, decisions, tables, functions and the expression language.
This reference describes model.content, the executable definition. The complete model file is
{content, questions}: keep both parts and submit them together with state on every execution.
Models explains the envelope; Questions defines its answer
contract. The shape below describes content, not a complete execution request.
{
"name": "<model name>",
"types": { "<Name>": "<type>" },
"inputs": { "<name>": "<input>" },
"decisions": { "<name>": "<decision>" },
"functions": { "<name>": "<function>" }
}
types and functions are optional. Inputs, decisions and functions share one namespace. Names may contain
spaces and underscores; in an expression, write a name exactly as declared.
Types
Types may be strings, objects or one-element arrays, depending on their form.
| Form | Example | Meaning |
|---|---|---|
| built-in | "number", "string", "boolean", "date", "time", "date and time" |
|
| constrained | "number [0..120]", "number > 0" |
the type, a space, a condition |
| choice | "billing|bug|other" |
values joined by | |
| ordered levels | "low<medium<high" |
values joined by <, lowest first |
| named | "Applicant" |
a key of types |
| struct | { "amount": "number", "delivered_on": "date" } |
an object of fields |
| list | ["number"] |
a one-element array |
When a value itself contains | or <, use { "$enum": ["a", "b"] } or { "$levels": ["low", "high"] }.
string is a free text input, allowed only for a key of a JSON state; a reader cannot answer it from text.
Inputs
The compact parser accepts bare types, but the public API applies strict validation: each input needs
a type and a desc. Use an object:
{
"type": "none|degraded|blocked",
"desc": "How much of the customer's normal operation is affected?",
"options": { "none": "no operational effect", "degraded": "slower or partially failing", "blocked": "a core operation cannot be completed" },
"from": "what the customer says they cannot do",
"threshold": 0.75,
"blank": false
}
| Field | Meaning |
|---|---|
desc |
The question the reader must answer. Required. |
from |
Optional hint about where in the state the fact appears. |
options |
What each value of a choice or level means. |
threshold |
Minimum reader confidence; below it the input is missing. |
blank |
true when the input may be absent. |
Inputs are observations. The policy belongs in the decisions.
Decisions
A decision is one of:
- a string: an expression,
"delay_hours * 60 + delay_minutes_part"; - an array: a decision table;
- an object:
{ "type", "desc", "let", "expr" | "table", "hit", "requires" }with exactly one ofexprandtable.letnames intermediate values, in order, visible to later entries and toexpr.
Requirements between decisions and inputs are inferred from the names used in expressions. Decisions must not form cycles.
Decision tables
The first row is the header; every other row is a rule.
[
["cause", "claim_age_days", "=> boolean", "# why"],
["strike", "-", "false", "Strikes are excluded from this policy."],
["-", "> 30", "false", "Claim submitted after the 30-day window."],
["-", "-", "true", "Weather, technical and other delays are covered within 30 days."]
]
- An input column is an expression over inputs, decisions or
letnames, usually just a name. - An output column starts with
=>:"=> name: type"in general;"=> boolean","=> number","=> low<medium<high"for a single output;"=>"alone when the decision object carries the type. - The annotation column
"# why"holds the rationale of each row, one sentence a reviewer would accept. - Input cells are conditions on the column value:
"< 18","[18..65]",">= 700","true","-"for any value. In a string-typed column, bare words are literals:"gold","gold, silver","not(gold)". A cell starting with=is an expression. - Output cells are expressions; in a string-typed column a bare word is a literal;
"-"means null. - Public API strict validation requires FIRST tables: rows are tried top to bottom and the first match wins. The last row is the else
row,
"-"in every input cell, so no state falls through to null.
Rules can also be objects, which read better for wide tables:
{ "when": { "days_since_delivery": "> 30" }, "then": "store_credit", "why": "Outside the 30-day refund window, within the 60-day store-credit window." }
when lists only the columns the rule tests; then is the output cell, or an object naming every output.
Functions
{ "monthly payment": { "params": { "amount": "number", "rate": "number", "months": "number" }, "returns": "number", "body": "amount * rate / (1 - (1 + rate) ** -months)" } }
Functions use only their parameters and other functions. The body is an expression or a table. Call them by name:
"monthly payment(amount, rate, 36)".
The expression language
Expressions use a readable language for calculations, dates and rule conditions. Common forms include comparisons
= != < <= > >=, and, or, not(…), arithmetic + - * / **, if … then … else …, range tests,
and dotted access into objects (applicant.income).
The recorded invoice model uses these expressions:
| Expression | Purpose |
|---|---|
invoice_date + duration("P10D") |
Add ten calendar days to the invoice date. |
payment_on >= invoice_date and payment_on <= discount_by |
Test the inclusive discount window. |
decimal(amount, 2) |
Round the computed payable amount to cents. |
String literals use double quotes. Dates can be constructed with date("2026-10-01"); subtracting dates
produces a duration whose .days can be read. Functions declared in the model are called by name.
This is not an exhaustive function catalogue. Validate a definition through
POST /v1/systemtwo/models and test its computed values before relying on it.
The question contract
For every question id there is exactly one decision with that name:
| Question | Decision type |
|---|---|
noul |
boolean. The answer is the probability the decision is true. |
choice |
the option names as a choice, spelled exactly: "billing|bug|other" |
score |
the levels as ordered levels, in order: "low<medium<high" |
number |
number, or number <range> when the question gives one |
date |
date |
Other decisions are intermediate values and must not be named like a question.
Addresses
Everything is addressed like the JSON: inputs.<name>, decisions.<name>, decisions.<name>.rule[3]
(1-based, row 0 is the header), decisions.<name>.let.<entry>, functions.<name>. Diagnostics and
text spans use model paths. Receipts group entries under decisions.<name> and identify a table row
with its 1-based rule field; they do not embed the entire model or its annotations.