Layouts
A layout is the JSON document the app renders. As an integrator you don't
usually write full layouts by hand (the MCP and
carterkit do that), but you must understand their shape — your server's event names and
msg_types have to match what the layout's controls declare. Full field-by-field
detail is in Layout Schema; this page is the model.
The shape
A layout is tabs → a grid → children. Children are either controls (a gauge, button, …) or groups (a container with its own grid). Groups nest, so you can build cards, sections, and dashboards.
Grid
Every tab — and every group — places its children on a grid of columns × rows.
By default the grid is 2-D: a child's position + span give it a real
row × col rectangle, so a tall control can sit beside two stacked shorter ones
(masonry / L-shapes). Columns divide the width; rows are a fixed rowHeight unit
(default 56pt), so inputs sit in ~1 row and a square gauge/ring wants ~3.
Set a grid's mode to "flow" for the simpler row-banded layout — children stack
by row with natural heights — which reads best for a tab that is a single full-page
control (map, chat, cardList) or a plain form.
"grid": { "columns": 4, "rows": 6 } // 2-D (default)
"grid": { "columns": 4, "rows": 8, "mode": "flow" } // legacy row-banded
Controls
Every control has at minimum:
{ "type": "gauge", "id": "cpu", "position": [0, 0], "span": [2, 2] }
type— what it is (see the Control Catalog).id— unique within the layout; the key your data and actions reference.position—[row, col], zero-indexed, within the parent grid.span—[rowSpan, colSpan], default[1, 1]. In a 2-D gridcolSpansets width androwSpansets height — span more cells to make a control bigger.
Plus type-specific fields (min/max for a gauge, options for a picker, …),
and the two behavior blocks that matter to you: sync (receive) and
action (send), both detailed in Data Flow — sync & actions.
Groups
A group is a control-shaped container:
{ "type": "group", "id": "card", "position": [0,0], "span": [3,4],
"grid": { "columns": 2, "rows": 3 },
"children": [ /* controls positioned within the group's own grid */ ] }
Groups can be dynamic — populated at runtime from a broadcast event — which is how you render a variable number of items (see Recipe: Server-Driven (Dynamic) UI).
Dynamic structure
Two layout-level features let the server shape the UI, not just fill it:
pollGroups— named timers that periodically send an event with a payload. Use them to ask your server for fresh data on an interval without any user action. Each group declares{ event, interval, payload }.dynamicTabs— tabs whose content is generated from a broadcast event, so your server can add/replace tabs on the fly.
Both are covered in Recipe: Server-Driven (Dynamic) UI.
The connection block
"connection": {
"url": "ws://192.168.1.50:8765",
"token": null,
"identity": { "name": "Dashboard", "channel": "home", "role": "viewer",
"canBroadcast": false, "canRoute": false }
}
This is how the device finds your relay and channel. A layout with no
connection runs fully offline (useful for demos). See Connection & Pairing
for transports, tokens, and pairing.
What you actually need to agree on
For your server and a layout to work together, three things must line up:
- Channel — server and layout join the same channel.
- Receive contract — for each display control, the
event(usually"broadcast"), thefilter(msg_type), and thevaluePathit reads. - Send contract — for each input control, the
event(verb) and themsg_typeit tags itsactionpayload with.
Keep a short table of your msg_types and valuePaths; it is your API.
Next: Connection & Pairing.