Statements
Studio writes this format for you. The statement editor ( Statements ) builds these bodies as visual step cards — this page documents the JSON those cards write to disk.
A
Function entry
's
body
may be a
JSON array of statements
instead of a JavaScript source string. Each statement is one step; steps run top to bottom, exactly like the statement list inside a JavaScript function — but as structure, so tooling can validate, inspect, and edit every step.
The smallest complete structured body — mutate, then notify:
{
"addItem": {
"$prototype": "Function",
"body": [
{ "operator": "push", "target": { "$ref": "#/state/items" }, "value": "New item" },
{ "dispatchEvent": "items-changed" }
]
}
}
There are four statement kinds, each reusing a web-platform name:
| Kind | Shape | Source of the name |
|---|---|---|
| Expression | a bare expression node (mutation or
call
) |
ECMAScript ExpressionStatement |
| Branch | { "if", "then", "else"? } |
JSON Schema conditional keywords |
| Multiway | { "$switch", "cases", "default"? } |
Element-level
$switch
, ECMA switch |
| Dispatch | { "dispatchEvent", "detail"?, "bubbles"?, "composed"? } |
WHATWG DOM
dispatchEvent |
Expression statements
Any mutating
expression node
— or a
call
— stands alone as a step. No wrapper key; the node appears bare in the array:
{ "operator": "+=", "target": { "$ref": "#/state/count" }, "value": 1 }
To capture a result, make the step an assignment whose
value
is a
call
node — there is no dedicated capture field:
{
"operator": "=",
"target": { "$ref": "#/state/total" },
"value": { "operator": "call", "target": { "$ref": "#/state/computeTotal" } }
}
Branches
if
holds a
pure
expression operand;
then
and
else
hold statement lists.
else
is optional:
{
"if": { "operator": ">", "target": { "$ref": "#/state/cart/length" }, "value": 10 },
"then": [{ "dispatchEvent": "cart-full" }],
"else": [{ "operator": "call", "target": { "$ref": "#/state/refreshTotals" } }]
}
Multiway branches
$switch
mirrors the element-level
$switch
in statement position: the discriminant is a pure operand,
cases
maps its
string form
to statement lists, and the optional
default
runs when no key matches:
{
"$switch": { "$ref": "#/state/status" },
"cases": {
"error": [{ "dispatchEvent": "load-failed" }],
"ready": [{ "operator": "=", "target": { "$ref": "#/state/visible" }, "value": true }]
},
"default": []
}
Dispatching events
dispatchEvent
fires a DOM
CustomEvent
— the statement's other keys mirror
CustomEventInit
.
detail
may be a literal, a
$ref
, or an expression node:
{
"dispatchEvent": "cart-changed",
"detail": { "$ref": "#/state/cart" },
"bubbles": true,
"composed": true
}
The event dispatches from the handler's
event.currentTarget
(or the component instance in compiled custom elements). Declare the events a function fires in its
emits
array so Studio can autocomplete them.
Parameters
A structured body follows the named-formula pattern. Without
parameters
, the entry is an event handler — statements see
state
and the
event#/
scheme. With
parameters
, it is a callable invoked positionally, its arguments bound to
$args/
names:
{
"addToCart": {
"$prototype": "Function",
"parameters": ["item"],
"body": [
{
"operator": "push",
"target": { "$ref": "#/state/cart" },
"value": { "$ref": "$args/item" }
}
]
}
}
Statements, expressions, or a body string?
The escalation ladder continues inside Shape 4 — prefer the least powerful form:
| Form | Use when |
|---|---|
A single
$expression |
One operation — toggle, increment, push, a computed value |
body
as a statement array |
Multiple steps, branching, or dispatching events — still no code |
body
as a JavaScript string |
Loops,
await
chains, try/catch, browser APIs — anything structural statements can't express |
A statement body stays fully analyzable and visually editable; a source string is opaque to tooling. Reach for the string only when structure genuinely runs out.
How it works
One engine serves both halves: an interpreter (
runStatements
) executes statement arrays directly against the reactive
state
proxy, and a compiler (
compileStatements
) emits the genuine ECMAScript forms —
if
/
else
statements, a
switch
over the discriminant's string form, and
dispatchEvent(new CustomEvent(type, init))
. The emitted function is identical in shape to a hand-written handler, so
reactivity
tracking works unchanged.
Statements execute sequentially. A step whose value is a promise is awaited before the next step runs — async/await semantics without writing
await
.
Rules
A
bodyis either a statement array or a source string — one representation per entry.Branch tests (
if) and$switchdiscriminants are pure operands: no mutations, nocallto a mutating entry.$switchmatchesString(discriminant)againstcaseskeys, because JSON keys are strings.Statements run in order; thenable results are awaited before the next step.
There is no capture field — assign a
call's result with an=statement.dispatchEventneeds a dispatch target; outside a handler (noevent), a compiled custom element dispatches from the component instance.Declare dispatched events in
emits— it is the declaration tooling reads.
Related
Expressions — the nodes statements are built from
Functions and sidecars — the entries that own a
bodySwitching —
$switchat the element levelStatements in Studio — the visual step editor
Code editing in Studio — the JavaScript escape hatch