QORMQORM v0.8.4 api Get started

Actions & State

Auto-generated from the source (TestAPIRef) — do not edit by hand. The step vocabulary below is extracted from the code, so it can never drift.

An action is { "type": "action", "id": …, "steps": [ … ] }. Each step mutates state, calls a backend, or navigates. onPress/onChange run an action by id (or inline steps).

Step types

Extracted from the runtime dispatch (internal/runtime):

typeWhat it does
delaywait ms milliseconds, then run the steps that FOLLOW it in the same list — render / delay / render paces a staged reveal declaratively. It never blocks: the wait goes to the host's background sink, and on a host with no sink (an offline render, an MCP simulation) it degrades to no wait at all, so the action still reaches the same final state
renderpublish an intermediate frame right here, so state written by the steps before it (a loading flag) reaches the screen before a slow step runs. No-op on a host with no frame sink; capped at 64 frames per dispatch
ifrun then steps when condition is truthy, else steps otherwise (nestable)
forEachrun steps once per element of in, with the element bound under as (default item) plus index / first / last
invokecall another action by name, merging evaluated args into its scope
navigatego to another scene (or back)
state.setset a state path to a value
state.setAt
state.appendappend a value to an array
state.appendObjectappend an object (built from item field expressions)
state.toggleflip a boolean, or a field on a matched array element; on a scalar array toggles membership of match
state.incrementadd to a number (value is the delta, default +1)
state.removeremove the array element selected by match
state.updateWhereupdate field on every element matching match
state.mergeshallow-merge an object into a state path
state.sortsort an array by field
state.movemove an array element from index to index
state.clearempty an array, or clear a string/number/boolean to its zero
state.resetrestore the manifest's initial values — one key with path, all state without
http.getGET a URL, store the parsed JSON at result
http.postPOST body, store the response at result
http.putPUT body, store the response at result
http.deleteDELETE a URL
http.requestgeneric request with an explicit method

Step fields

Every step is one JSON object; which fields apply depends on its type:

FieldTypeUsed by
typestringthe step kind (table above) — required
pathstringtarget state path, e.g. todos or user.name
valuestringvalue expression; may contain {{ bindings }}
matchstringexpression selecting an array element (with matchKey)
matchKeystringobject key compared against match (default id)
fieldstringfield to toggle/update within the matched object
itemobjectfield → value expressions for state.appendObject
tostringnavigate: target scene id · state.move: target index
backboolnavigate: pop the back stack instead of pushing
fromstringstate.move: source index
urlstringhttp.*: request URL (may contain {{ bindings }})
methodstringhttp.request: HTTP method override
bodystringhttp.*: request body — a string is sent verbatim (an inline JSON template is not double-encoded); a bound non-string value (map/list/number/bool) is JSON-encoded
headersobjecthttp.*: request headers
resultstringhttp.*: state path to store the parsed response
errorstringhttp.*: state path to store an error message
asyncboolhttp.*: run the request in the background — the dispatch returns immediately (so the frame at its boundary already shows the loading state and the session stays responsive) and the result branch runs when the reply arrives. Defaults to false, which blocks the dispatch; it also falls back to false on a host with no background sink, so the same JSON stays portable
keystringhttp.*: name a request slot — starting a new request on a key cancels whichever request was still open on it AND discards that one's outcome entirely (no result/error write, no branch). This is what makes search-as-you-type land the reply to the LAST keystroke instead of whichever round trip finished last. Only an async request can be superseded; unkeyed requests never cancel each other
timeoutnumberhttp.*: this request's ceiling in milliseconds, overriding the shared client's 20s. Expiry is an ordinary failure — the error path is written and onError runs, with the message request timed out after <n>ms. Applies to synchronous requests too. Omit (or 0) to keep the 20s ceiling
pendingstringhttp.*: a state path held true for exactly as long as the request is open — set on launch, cleared when it settles, INCLUDING on failure, timeout and refusal. Replaces the hand-written pair of state.set steps (the one that reliably forgets the error path); bind a spinner to {{ state.<path> }} as usual. Reference-counted, so overlapping requests on one path hold it until the last settles and a superseded request cannot switch off its successor's spinner
msnumberdelay: how long to wait, in milliseconds. The steps FOLLOWING the delay in the same list run when it expires
onSuccessarrayhttp.*: steps run after a 2xx response; the decoded response is bound as {{ response }} (the result path is written first). With async they run in the completion callback, after the dispatch has ended: {{ state.x }} reads live, the action's args stay frozen at dispatch time
onErrorarrayhttp.*: steps run after a failure; the message is bound as {{ error }} (the error path is written first). With async they run in the completion callback, on the same terms as onSuccess
conditionstringif: a {{ … }} expression selecting then (truthy) or else
thenarrayif: steps run when condition is truthy (branches nest, depth-capped at 32)
elsearrayif: steps run when condition is falsy
namestringinvoke: the target action id (call depth capped at 16)
argsobjectinvoke: arg → value expressions, evaluated in the caller's context and merged into the callee's scope (same semantics as an event invoke's args)
instringforEach: a {{ … }} expression yielding the array to iterate; anything that is not an array iterates zero times
asstringforEach: name the current element (default item), plus the derived <as>Index / <as>First / <as>Last keys — the same alias rule a list's renderItem uses
stepsarrayforEach: the loop body, run once per element (iterations capped at 10000; the body nests under the same depth cap as if)
// actions/addTodo.json — append a new object, then clear the input
{ "type": "action", "id": "addTodo", "steps": [
  { "type": "state.appendObject", "path": "todos",
    "item": { "id": "{{ now }}", "title": "{{ state.draft }}", "done": "false" } },
  { "type": "state.set", "path": "draft", "value": "" }
] }

Script actions (script)

An action may carry a qscript program instead of steps: { "type": "action", "id": "tick", "script": "…" }. JSON keeps declaring the scenes and the data; the script carries the logic — let, assignments (state.a =, state.arr[i] =), if/else, for x in … and while (with break/continue), fn definitions and calls, the expression language's operators and builtins, state as the read/write handle and args for the dispatch arguments (see internal/qscript). The loader compiles the script at load time — a parse error is a diagnostic naming the line — and warns when script and steps are both declared; the script always wins. A runtime failure (a governance limit or a type error) is recorded on the runtime with the script line number. Scripts are bounded (200k operations per run, 100k iterations per loop, 64 nested calls) and deterministic by construction — no I/O, and the only clock is the explicit now() builtin (Unix ms; scripts that call it are reproducible only up to that value). A script may fire a SIBLING action with call("id" [, args]) — the dispatch re-enters the runtime, so the same invoke-depth cap (16) that governs invoke steps governs call() chains, and a missing action or a refused depth surfaces as a script error with the caller's line — but never reaches anything outside the app. examples/tetris is a full game written this way.

Script file actions (actions/*.qs)

A script action can live in a file of its own instead of a JSON string field: actions/tick.qs is the action tick, and the file's full text is its qscript source. The layout is the DOM+CSS+JS separation applied to an app — structure in scenes/*.json, logic in actions/*.qs, the two bound by action id (a scene's onPress/keys/timer names the action; the script reaches the structure through state). The loader collects each *.qs file directly under an actions/ directory as the same type:"action" document the JSON spelling declares, so everything downstream is uniform: the scene references, the load-time compile (a parse error is a diagnostic naming the file AND the line), and the bundle hash (a .qs file is signed exactly like a JSON action — qorm build and qorm run always agree). The two spellings coexist; a .json and a .qs defining the same id is a duplicate definition — an error diagnostic on directory load (first definition wins, the .json sorts first) and a hard refusal from qorm build. One filename is reserved: actions/lib.qs is NOT an action but the SHARED FUNCTION LIBRARY — its fn definitions are spliced ahead of EVERY script action at dispatch, so an app keeps its helpers in one file instead of copy-pasting them into each action. Keep it to fn definitions and comments (it is compiled ahead of each action's body, so any top-level statement would run before every action); a second library definition is diagnosed like a duplicated stylesheet, and the library is packaged and hashed like any other document. examples/tetris keeps all ten of its actions this way, with its board core in lib.qs.

Derived values (computed)

Declare a value ONCE in the manifest instead of repeating the expression in every binding. Derived values are evaluated once per frame (not once per binding), may read each other, and are read-only — a step that writes into the namespace is a load-time error and is dropped at dispatch.

// qorm.json — beside "globalState" (or nested inside it)
"computed": {
  "subtotal": "{{ sum(map(state.items, \"it.price * it.qty\")) }}",
  "isEmpty":  "{{ len(state.items) == 0 }}",
  "withTax":  "{{ computed.subtotal * 1.2 }}"
}

Read them as {{ state.computed.subtotal }} in a scene binding, and as {{ computed.subtotal }} inside an action (which also sees every top-level state key bare). A dependency cycle is reported at load time and those values evaluate to nothing rather than recursing.