QORMQORM v0.8.4 docs Get started

Expressions

Anything inside {{ … }} is an expression. The same little language is used everywhere: a node's text, a style value, an if condition, a list's data, an action step's value, an invoke's args, and an http URL.

{ "type": "text", "text": "{{ state.user.name }} has {{ count(state.cart) }} items" }

A string that is exactly one binding keeps the value's type — a boolean stays a boolean, a number stays a number, a list stays a list. A string that mixes text and bindings interpolates to a string. That distinction matters when you feed a list into data, a boolean into if, or a number into a component prop.

{ "if": "{{ state.open }}",              "text": "open" }
{ "data": "{{ state.rows }}" }
{ "text": "Rows: {{ len(state.rows) }}" }

Scopes

Which names resolve depends on where the expression sits.

NameAvailable inWhat it is
state.*everywherethe global state store declared in qorm.json
state.computed.*everywherethe manifest's derived values (see below)
computed.*scene bindings, action steps, other derived valuesthe same values, without the state. prefix. Inside an action it is a dispatch-entry snapshot (see below). Not available inside a scene guard
bare argument namesan action's stepsthe args the invoke passed in — {{ id }}, {{ text }}
prop.*a component templatethe properties the instance passed in
item, index, first, lasta list/gridview renderItem template, and a forEach step's bodythe current element and its position (see First Scene). An as alias renames the whole set — "as":"row" gives row / rowIndex / rowFirst / rowLast
row, rowIndex, cella table / datatable cell templatethe current row, its position, and {{ cell.value }} / {{ cell.column }} / {{ cell.index }}
route.*everywhereroute parameters of the current deep link
viewport.*everywherethe current viewport, for responsive bindings
teverywherethe i18n lookup table
responsean http.* step's onSuccess branchthe decoded response body
erroran http.* step's onError branchthe failure message
ita map / filter / count sub-expressionthe element being visited

Two notes that save debugging time:

name** — the surrounding scope is deliberately hidden, so map(state.nums, "state.x") yields a list of nulls, not a list of state.x.

Literals and operators

Literals: numbers, 'single' or "double" quoted strings, true, false, null.

GroupOperators
arithmetic+ - * / % (and unary -)
comparison== != < <= > >=
logical&& `\\ !`
conditionalcond ? a : b
grouping( … )

+ concatenates when either side is a string, and adds otherwise — {{ 1 + 2 }} is 3, {{ "n=" + 2 }} is n=2.

Truthiness (used by if, !, &&, ||, the ternary, and filter): null, false, 0, "", an empty array and an empty object are falsy. Everything else is truthy.

Index access

Postfix [ … ] reads an array element or an object key, and chains freely with . member access.

{ "text": "{{ state.items[0].name }}" }
{ "text": "{{ state.grid[1][0] }}" }
{ "text": "{{ state.users[state.idx].name }}" }
{ "text": "{{ state.user['name'] }}" }
{ "text": "{{ state.user[state.key] }}" }
{ "text": "{{ split(state.csv, ',')[1] }}" }

The index may be any expression. Rules worth knowing:

an error.

element 1).

Builtin functions

Collections

CallResult
len(x)element count of a list/object, rune count of a string
at(list, i)element i; negative counts from the end; out of range is null
first(list) / last(list)first / last element, or null when empty
sum(list)numeric sum; non-numeric elements count as 0; empty is 0
avg(list)mean; empty is 0 (never NaN)
count(list)element count; count(list, "predicate") counts matching elements
keys(obj) / values(obj)keys sorted lexically, values in that same key order
map(list, "expr")each element mapped through the sub-expression
filter(list, "expr")elements whose sub-expression is truthy
slice(list, lo, hi)sub-range, bounds-clamped; an inverted range is empty
push(list, v, …) · unshift(list, v, …)a NEW list with the values appended / prepended (null values drop)
pop(list) · shift(list)a NEW list without the last / first element (read it with last() / first() first)
reverse(list) · sort(list)a NEW list reversed / sorted — numbers ascending, then strings lexically, then booleans, null last; stable within each class
indexOf(list, v) · includes(list, v)first index of v or -1 / whether v is an element
range(start, end, step)list of numbers from start (inclusive) to end (exclusive), by step (default 1); capped at 2^20 elements
fill(n, v)n copies of v in a new list (board / state initialisation)
concat(a, b, …)lists joined in order; a bare value is treated as a one-element list
flatten(list)one level deep — inline [1,[2,[3]]][1,2,[3]]

map, filter and the two-argument count take a sub-expression written as a string, with the element bound to it:

{ "text": "Total {{ sum(map(state.cart, \"it.price * it.qty\")) }}" }
{ "data": "{{ filter(state.todos, \"!it.done\") }}" }
{ "text": "{{ count(state.todos, \"it.done\") }} of {{ len(state.todos) }} done" }

A non-list subject yields the empty result rather than an error, so a binding never blows up on a state key that has not loaded yet.

The list-shaping calls (push, unshift, pop, shift, reverse, sort) are functional: each returns a NEW list and never mutates its subject — assign the result back when you want to keep it:

{ "type": "state.set", "path": "items", "value": "{{ push(state.items, state.draft) }}" }

Strings

CallResult
str(x)stringify any value
trim(s) · upper(s) · lower(s)whitespace-trimmed / upper / lower
contains(s, sub) · startsWith(s, p) · endsWith(s, p)boolean tests
replace(s, old, new)replace every occurrence
matches(s, regexp)regular-expression test; an invalid pattern is false
split(s, sep)split into a list; an empty sep splits into runes; an empty subject is []
join(list, sep)join elements; a null element joins as empty
format(pattern, …)%s, %d, %f, %.Nf, %%; an unknown verb passes through literally
charAt(s, i)the i-th rune as a string; out of range is ""
substring(s, start, end)rune-clamped slice; negative/inverted ranges collapse like slice (end optional: to the end)
repeat(s, n)s repeated n times; n <= 0 is ""
padStart(s, n, ch) · padEnd(s, n, ch)pad to n runes with ch (default " "), left / right
trimStart(s) · trimEnd(s)trim leading / trailing whitespace only
includes(s, sub)substring containment when the subject is a string
{ "text": "{{ format('%s scored %.1f%%', state.name, state.pct) }}" }
{ "text": "{{ join(map(state.tags, \"upper(it)\"), ' · ') }}" }

Numbers and logic

CallResult
number(x) (alias num) · int(x)numeric coercion; int truncates
abs(x) · round(x) · floor(x) · ceil(x)the usual
min(a, b, …) · max(a, b, …)over the arguments
not(x) · empty(x)logical negation of truthiness
default(x, fallback) (alias coalesce)x when truthy, otherwise fallback
sin(x) · cos(x) · tan(x)trig (radians) — curved motion / aimed shots in games
atan2(y, x)angle in (-π, π] from positive x-axis to (x, y) — aim towards a target
sqrt(x)square root
now()Unix time in milliseconds (the one non-deterministic builtin)

Type checks

CallResult
typeof(x)"string" / "number" / "boolean" / "object" / "array" / "null"
isString(x) · isNumber(x) · isBool(x) · isObject(x) · isArray(x) · isNull(x)boolean tests

JSON

CallResult
jsonEncode(v) (alias JSON.stringify)JSON string; throws away functions, keeps numbers / strings / booleans / lists / objects / null
jsonDecode(s) (alias JSON.parse)parsed value; malformed input and an empty string both give null

Audio (canvas engine only)

CallResult
playSound(src)one-shot WAV playback — source resolves under the app directory
playMusic(src)loop WAV playback — call stopMusic() to stop
stopMusic()halt the current looping track

Dispatch

CallResult
call(name)dispatch another script action on the same runtime; subject to the same recursion guard as human / agent dispatches

An unknown function name evaluates to null at runtime — but the loader statically checks arity and argument types for the collection and format calls, so qorm run reports the mistake before you see a blank screen.

Derived values (computed)

When the same expression appears in a dozen bindings, declare it once in the manifest instead. computed is a map of name → expression, written beside globalState (or nested inside it, which normalises to the top level on a round trip):

{
  "type": "app", "id": "cart", "entry": "main",
  "globalState": { "schema": { "items": "array" }, "initial": { "items": [] } },
  "computed": {
    "itemCount": "{{ sum(map(state.items, \"it.qty\")) }}",
    "subtotal":  "{{ sum(map(state.items, \"it.price * it.qty\")) }}",
    "shipping":  "{{ computed.subtotal >= 50 ? 0 : 5 }}",
    "total":     "{{ computed.subtotal + computed.shipping }}",
    "isEmpty":   "{{ len(state.items) == 0 }}"
  }
}

Read them as {{ state.computed.total }} in a scene binding and {{ computed.total }} inside an action. Both spellings resolve in both places (examples/derived uses the state.-rooted form in scenes), but they are not quite the same expression inside an action — see Which spelling to use in an action below. A derived value may read other derived values in any declaration order, as shipping and total do above.

What a declaration is worth knowing about:

nodes is computed once, and the view is stable for the whole of a dispatch: the published values refresh at frame boundaries (the end of a top-level dispatch, a render step, a scene entry), not after each step. So a step that writes state.items and a later step in the same action that reads {{ computed.subtotal }} still sees the pre-dispatch value.

namespace is a load-time error and is dropped at dispatch — the whole step, so a gated http.get never even issues its request. Both spellings are refused: "path": "computed.total" and "path": "state.computed.total" alike. (A step path is already relative to the state root, so any state.-prefixed path is a mistake — writing "path": "state.count" would create a top-level state key literally named state. The loader warns.)

downstream of one) are reported at load time and simply evaluate to nothing; the rest of the app still works.

viewport.* — but not route.*, so route parameters cannot feed one.

state key; none of the above applies to it.

Which spelling to use in an action

Inside an action the two spellings read the same value until a frame lands mid-action, and then they diverge:

SpellingWhat it reads
{{ state.computed.total }}the live namespace — refreshed by a render step, a delay, or an async reply
{{ computed.total }}the value the namespace held when the action was dispatched

That is not special to computed. An action's context is built once, at dispatch entry: state is the live store, while every top-level state key also offered under its bare name ({{ count }} for {{ state.count }}) is a copy taken at that moment. {{ computed.total }} is that bare spelling of the computed key, so it behaves exactly like {{ count }} does.

{ "type": "state.set", "path": "items", "value": "{{ push(state.items, 1) }}" },
{ "type": "render" },
{ "type": "state.set", "path": "shown", "value": "{{ state.computed.subtotal }}" }

Reading {{ computed.subtotal }} on that last line would show the subtotal from before the push. Use the state.-rooted spelling in an action that renders mid-flight; the bare one is fine (and shorter) in a straight-line action, and in scene bindings both are always current.

Where to go next