#Widget Catalogue
Widgets are the building blocks of an app dashboard page. Each is one entry in a
page's widgets record — widgetComponent selects the widget, config is its
options (config values may carry {{ … }} smart values, see
Smart Values):
"widgets": {
"tasks": { "widgetComponent": "data_grid", "config": { "header": "Open tasks", "columns": [ … ] } }
}
For the structure that holds widgets (dashboards → pages → widgets) see App spec structure.
#A real app, rendered
Everything below renders with the actual app widgets — this is a live Tasks dashboard (a heading, a task table, and a create-task form), not a screenshot:
{
"title": "Tasks · main dashboard",
"widgets": [
{
"widgetName": "headline",
"config": {
"text": "Tasks",
"headerLevel": 1
}
},
{
"widgetName": "table",
"config": {
"header": "Open tasks",
"columns": [
{
"field": "title",
"header": "Task",
"type": "string"
},
{
"field": "state",
"header": "State",
"type": "string"
},
{
"field": "priority",
"header": "Priority",
"type": "number"
}
],
"rows": [
{
"id": "t1",
"title": "Draft kickoff brief",
"state": "doing",
"priority": 1
},
{
"id": "t2",
"title": "Set up CI",
"state": "todo",
"priority": 2
},
{
"id": "t3",
"title": "Write API smoke test",
"state": "done",
"priority": 3
}
]
}
},
{
"widgetName": "form",
"config": {
"header": "New task",
"fields": [
{
"id": "title",
"label": "Task title",
"type": "string",
"required": true,
"placeholder": "What needs doing?"
},
{
"id": "state",
"label": "State",
"type": "string",
"options": [
{
"label": "To do",
"value": "todo"
},
{
"label": "Doing",
"value": "doing"
},
{
"label": "Done",
"value": "done"
}
]
},
{
"id": "priority",
"label": "Priority",
"type": "number",
"min": 1,
"max": 5
}
],
"submit": {
"label": "Add task"
}
}
}
]
}
This page uses the table widget (its rows are inline, so the preview is
self-contained). In a production app the server-resolved data_grid does the
same job at scale — fetching rows from a live dataSource, paginating, and
carrying row actions. The form submits a command and buttons navigate or run
actions; previews here are visual, so interactions are inert.
#Content & data widgets
#headline
A static (or smart-valued) heading.
| Field | Type | Req | Notes |
|---|---|---|---|
text |
string | ✓ | Heading text (may be {{ }}). |
headerLevel |
int 1–3 | Visual level; defaults to 1. |
{
"widgetName": "headline",
"config": {
"text": "Vessels",
"headerLevel": 2
}
}
#data_grid
The workhorse: a paginated, server-resolved grid with row + bulk actions.
Columns read the entity's attributes by bare name. Filtering composes from
OUTSIDE the grid: a standalone filter_bar (free inputs) or chips (preset
filters) widget publishes state that the grid's SOURCE reads via optional
filter particles — the grid never references the filter widget.
| Field | Type | Req | Notes |
|---|---|---|---|
dataSource |
string | ✓ | The NAME of an entry in the page's dataSources map ($query / $http / $rows producer — $typeSchema cannot feed a grid). |
columns |
array | ✓ | { type, field, header?, display?, lookup?, sortable?, filterable?, minWidth?, maxWidth? } — type is string / number / date; field is the bare attribute name (or a relationship type name), never a storage path; display is a JSONata expression over value and row producing the cell text; lookup maps raw values to labels; sortable / filterable: false opt a synthesized column out. |
actions / leftActions |
rowAction[] | Per-row buttons; each carries a command step-sequence. |
|
selectable |
boolean | Checkbox column; checked ids round-trip as thisWidget.selectedRows. |
|
expand |
boolean | Toggles the open row's id into grid state; a separate author-placed widget reads it to render the detail. | |
pageSize / paginate |
number / boolean | Page size + keyset paging toggle. | |
header, noResultsMessage |
string | Copy. |
A data_grid resolves its rows from its named source at render time, so it
populates in a running app (the live preview above uses the inline-row table
widget to stand in for it).
#process_data_grid
A data_grid for entities that carry a process — a set of steps with
per-entity status. Each row gains a status column per step plus an
invoke-from-row menu, on top of everything data_grid offers.
| Field | Type | Req | Notes |
|---|---|---|---|
dataSource, columns, actions, selectable, pageSize, … |
✓ | As data_grid. The source's include must carry each row's process instances (default field instances). |
|
process |
object | ✓ | { entityType, nodes, instancesField?, model } — nodes is the source NAME of the step catalog; model is inline ({ catalog, statusMap, instances }) or { fromConfig: { slot, scope } }. |
statusColumns |
array | object | Which steps get a column: a static list of step keys paired with author-declared ps_<key> columns, or { fromConfig: { slot, scope, type? } } reading a per-tenant { key, header }[] list that also generates the columns. |
|
invoke |
object | { page } — adds a per-row invoke menu that navigates to that page with the step in scope. |
|
instances |
object | { keyAttr, stateAttr, stateAtAttr?, activeAttr? } — override the model's instance projection when the include shape differs. |
Every sourceable value here follows one rule: inline, or
{ "fromConfig": { "slot", "scope" } } reading config.<scope>.<slot>. Status
columns are sortable and filterable on their underlying state. The process
bundle assembles from live sources and config, so there is no static preview:
{
"widgetComponent": "process_data_grid",
"config": {
"dataSource": "voyages",
"columns": [{ "type": "string", "field": "name", "header": "Voyage" }],
"process": {
"entityType": "Voyage",
"nodes": "processNodes",
"model": {
"fromConfig": { "slot": "voyage_process", "scope": "company" }
}
},
"statusColumns": {
"fromConfig": { "slot": "voyage_status_columns", "scope": "company" }
},
"invoke": { "page": "voyage_step" }
}
}
#table
A simpler, client-side table over inline rows (no server fetch / pagination).
Columns read each row flat (row[field]).
| Field | Type | Req | Notes |
|---|---|---|---|
columns |
array | ✓ | { field, header, type } — read flat from each row. |
rows |
array | ✓ | Inline rows (often a {{ }}/$jsonata projection). |
actions / leftActions |
rowAction[] | Per-row buttons (command). |
|
groupBy |
object | Group rows under headings. | |
header, noResultsMessage |
string | Copy. |
{
"widgetName": "table",
"config": {
"header": "Recent",
"columns": [
{
"field": "name",
"header": "Name",
"type": "string"
},
{
"field": "status",
"header": "Status",
"type": "string"
}
],
"rows": [
{
"id": "1",
"name": "Aurora",
"status": "active"
},
{
"id": "2",
"name": "Borealis",
"status": "docked"
}
]
}
}
#form
An input form. In its plain shape it collects values and submits a command; with bindings it edits entities or a principal config directly and generates its own submit.
| Field | Type | Req | Notes |
|---|---|---|---|
fields |
array | ✓ | The inputs — see the field vocabulary below. |
submit |
object | { label, command? } — the primary button. With command, the form's values reach the steps as outputs.form.<id>; without it, a bound form generates its own writes. |
|
buttons |
array | Additional buttons, each with a command. |
|
initialValues |
record | Seed values per field id (wins over a field's default). |
|
inline |
boolean | Inline (vs stacked) layout. | |
emptyFallback |
object | { message, variant? } shown when every field is gated away. |
|
entities |
record | Bound form — entity slots (below). | |
config |
object | Bound form — { principalType }: fields derive from that principal-config schema and submit writes the config. |
|
links |
array | Bound form — relationships the submit maintains (below). | |
onSuccess / onError |
steps | Run after a bound submit succeeds / fails. |
Fields. Every field has id, type, and optionally label, placeholder,
required, default; a field entry may be $if-gated. The type selects the
input:
type |
Extra keys | Renders |
|---|---|---|
string |
options?, showAsButtons?, showAsDropdown?, numLines?, maxLength?, format? (date) |
A text input; with options, a select (showAsButtons → a button row, showAsDropdown → an auto-applying dropdown). |
number |
min?, max? |
A numeric input. |
boolean |
A toggle. | |
array |
options (required) |
A multi-select. |
dateRange |
Two date pickers, published as <id>.from / <id>.to. |
|
secretInput |
target (required) — { scope, scopeId?, name, env? } |
A write-only secret input; the value may only flow into a setSecret step. See Secrets. |
file |
previewPage?, accept?, maxFileSize?, buttonLabel?, uploadType? |
An upload. Empty it renders an Upload button; once uploaded, the host's upload element plus Preview. See below. |
options is either a static [{ value, label }] array or
{ "dataSource": "<name>", "limit"?: n } binding a page source whose rows carry
value and label (shape them with a $pipe map step).
The file field is a composite, not a single input. Clicking Upload mints a
signed capability for that field's slot and re-renders with the host's upload
element in its place; the value it ends up holding is the uploaded
temp file's id, which is then ordinary
form state — outputs.form.<id> on submit, page.widgets.<form>.state.<id>
elsewhere. previewPage names the modal a Preview button opens (no
previewPage, no Preview button); accept takes dotted extensions or MIME
types; maxFileSize is a per-file byte ceiling capped at 25 MiB. A file field
is never bound — its value is a file id that no attribute schema derives —
so in is refused where it is authored. The full lifecycle is in
Apps → File uploads.
{
"widgetName": "form",
"config": {
"header": "New task",
"fields": [
{
"id": "title",
"label": "Title",
"type": "string",
"required": true
},
{
"id": "priority",
"label": "Priority",
"type": "number"
},
{
"id": "urgent",
"label": "Urgent",
"type": "boolean"
}
],
"submit": {
"label": "Create"
}
}
}
Bound forms. Declaring entities binds the form to one or more entity
slots; each slot is { type, id?, matchOn?, seed? } and its mode follows
from what it carries — id present ⇒ edit that entity; matchOn present ⇒
upsert by those identifier attributes (prefilled when a match exists, created
otherwise); neither ⇒ create. A field binds to a slot with
"in": "<slot>", "attribute": "<name>" (or "in": "config", "key": "<name>"
for a config binding), and its input type derives from the slot type's schema —
the field restates neither. links are the relationships the submit maintains:
{ rule, from, to, direction, mode? }, where an endpoint is a slot name, a
concrete entity id, or { field } (a picker whose value is the target id),
direction names the endpoint the carrying slot occupies, and mode: "replace"
makes the listed targets the whole set for that rule. submit is required with
bindings; the writes (upsertEntity per slot, links attached) are generated for
you.
{
"widgetComponent": "form",
"config": {
"entities": {
"cert": { "type": "VesselCertificate", "seed": { "status": "valid" } }
},
"fields": [
{
"id": "cert_type",
"in": "cert",
"attribute": "type",
"label": "Certificate"
},
{
"id": "expires",
"in": "cert",
"attribute": "expiry_date",
"label": "Expires"
}
],
"links": [
{
"rule": "VesselToVesselCertificate",
"from": "{{ page.params.vessel_id }}",
"to": "cert",
"direction": "to"
}
],
"submit": { "label": "Add certificate" },
"onSuccess": [{ "kind": "closeModal" }, { "kind": "refresh" }]
}
}
#filter_bar
The free-input filter affordance: a pure STATE PUBLISHER. It renders inputs,
round-trips their values as widget state, and does nothing else — the page's
SOURCES read page.widgets.<bar>.state.<field> via optional filter particles,
and every interaction re-renders in full. No widget references the bar.
| Field | Type | Req | Notes |
|---|---|---|---|
fields |
array | ✓ | The same field vocabulary as form (string / multiselect / dateRange…); showAsDropdown / showAsButtons apply on CHANGE. |
apply |
object | { label } — rendered by default (label "Apply") when any field applies on submit; a bar of only change-to-apply fields renders none. |
|
clear |
boolean | object | A Clear button — a type-aware STATE reset of this bar's fields (strings → "", arrays → []). |
|
inline |
boolean | Row layout (default) vs stacked. |
{
"widgetName": "filter_bar",
"config": {
"fields": [
{
"id": "q",
"label": "Search",
"type": "string"
},
{
"id": "status",
"label": "Status",
"type": "string",
"options": [
{
"value": "open",
"label": "Open"
},
{
"value": "done",
"label": "Done"
}
],
"showAsDropdown": true
}
],
"clear": true
}
}
#chips
The preset-filter affordance — one row of exclusive chips publishing the
selected value as widget state (the preset face of what filter_bar does with
free inputs).
| Field | Type | Req | Notes |
|---|---|---|---|
id |
string | ✓ | The state key the selected value publishes to. |
options |
array | ✓ | { value, label }[] — one chip each. |
default |
string | Initially-selected value. |
{
"widgetName": "chips",
"config": {
"id": "status",
"options": [
{
"value": "all",
"label": "All"
},
{
"value": "open",
"label": "Open"
},
{
"value": "done",
"label": "Done"
}
],
"default": "open"
}
}
#page_tabs
A row of tab buttons that navigate between pages of the dashboard.
| Field | Type | Req | Notes |
|---|---|---|---|
tabs |
array | ✓ | { label, page }[] — each tab navigates to a page key. |
{
"widgetName": "page_tabs",
"config": {
"tabs": [
{
"label": "Overview",
"page": "root"
},
{
"label": "Details",
"page": "detail"
},
{
"label": "Activity",
"page": "activity"
}
]
}
}
#section_switcher
A button group that writes a page.data key; sections gate visibility on it via
$if (no navigation).
| Field | Type | Req | Notes |
|---|---|---|---|
stateKey |
string | ✓ | The page.data key this switcher writes. |
options |
array | ✓ | { label, value }[] — the selectable sections. |
{
"widgetName": "section_switcher",
"config": {
"stateKey": "view",
"options": [
{
"label": "List",
"value": "list"
},
{
"label": "Board",
"value": "board"
}
]
}
}
#breadcrumbs
A breadcrumb trail — explicit crumbs or an entity ancestry chain, with an optional refresh button.
| Field | Type | Req | Notes |
|---|---|---|---|
crumbs |
array | Explicit { label, page } crumbs. |
|
ancestry |
object | { dataSource, root?, detailPage, self } — walk the named detail read's parent chain into crumbs (dataSource is a dataSources entry name). |
|
refresh |
boolean | Append a ↻ button that re-renders the page. |
{
"widgetName": "breadcrumbs",
"config": {
"crumbs": [
{
"label": "Vessels",
"page": {
"id": "vessels"
}
},
{
"label": "MV Northern Star",
"page": {
"id": "vessel",
"params": {
"id": "v-100"
}
}
},
{
"label": "Voyage 2025-0142"
}
],
"refresh": true
}
}
#entity_info
A labelled read-only field list for one entity (a definition-list card). The
entity arrives through a named source — typically a single: true point-read in
the page's dataSources map.
| Field | Type | Req | Notes |
|---|---|---|---|
dataSource |
string | ✓ | Name of the dataSources entry carrying the entity. |
fields |
array | ✓ | { label, attr }[] — label + attribute, in order. |
header |
string | Optional card heading. |
{
"widgetName": "entity_info",
"config": {
"header": "Vessel",
"dataSource": "vessel",
"fields": [
{
"label": "Name",
"attr": "name"
},
{
"label": "IMO",
"attr": "imo"
},
{
"label": "Flag",
"attr": "flag"
}
]
},
"data": {
"vessel": {
"id": "v1",
"attributes": {
"name": "Aurora",
"imo": "9876543",
"flag": "Panama"
}
}
}
}
#related_collections
Lists an entity's related collections as grouped sections — a pure PROJECTION of
one detail read: the named source is a point-read whose include tree
materializes each collection under an as: alias, and every collections entry
names that alias via key.
| Field | Type | Req | Notes |
|---|---|---|---|
dataSource |
string | ✓ | Name of the detail point-read carrying the collections. |
collections |
array | ✓ | { label, key, columns, noResultsMessage? }[] — key is the include's as alias on the detail row. |
collapsible |
boolean | Render each section collapsible. | |
header |
string | Optional heading. |
{
"widgetName": "related_collections",
"config": {
"dataSource": "voyageDetail",
"header": "Related",
"collections": [
{
"label": "Tasks",
"key": "tasks",
"columns": [
{
"field": "title",
"header": "Task"
},
{
"field": "status",
"header": "Status",
"type": "tag"
},
{
"field": "due",
"header": "Due",
"type": "date"
}
]
},
{
"label": "Alerts",
"key": "alerts",
"columns": [
{
"field": "summary",
"header": "Alert"
},
{
"field": "severity",
"header": "Severity",
"type": "tag"
}
],
"noResultsMessage": "No alerts"
}
]
},
"data": {
"voyageDetail": {
"id": "voyage-1",
"tasks": [
{
"attributes": {
"title": "Submit port docs",
"status": "open",
"due": "2025-09-10"
}
},
{
"attributes": {
"title": "Confirm berth",
"status": "done",
"due": "2025-09-08"
}
}
],
"alerts": [
{
"attributes": {
"summary": "Late ETA",
"severity": "high"
}
}
]
}
}
}
#actions
A free-standing button group (not bound to a row), rendered as a plain row of
buttons. For the pinned footer-bar treatment, wrap it in a footer
layout widget.
| Field | Type | Req | Notes |
|---|---|---|---|
actions |
array | ✓ | { label, command, icon?, variant? }[] — each runs a step-sequence. |
asFooter |
boolean | Accepted; placement is decided by the layout — pin the bar with a footer node. |
{
"widgetName": "actions",
"config": {
"actions": [
{
"label": "Refresh",
"command": []
},
{
"label": "Export CSV",
"command": []
}
]
}
}
#action_bar
An "invoke the actionable steps" button bar for one entity's process: a primary
button for the first non-secondary invokable step plus an overflow dropdown for
the rest. Shares the process block with status_checklist /
process_data_grid; the widget is pure — its nodes / states sources are
names in the page's dataSources map.
| Field | Type | Req | Notes |
|---|---|---|---|
process |
object | ✓ | { entityType, entityId, parentKeyFrom?, nodes, states, model } — nodes/states are source NAMES; model is inline or { fromConfig: { slot, scope } }. |
invoke |
object | ✓ | { command, label? } — the step-sequence an invoked step runs; label may carry a {label} placeholder. |
The invoked step's node/parent seed outputs for the command
({{ outputs.node.* }} / {{ outputs.parent.* }}). Renders nothing when no
step is invokable. Because the process bundle assembles from live sources +
config, there is no static preview here — see the process guides for a running
example:
{
"widgetComponent": "action_bar",
"config": {
"process": {
"entityType": "Voyage",
"entityId": "{{ page.params.id }}",
"nodes": "processNodes",
"states": "processStates",
"model": {
"fromConfig": { "slot": "voyage_process", "scope": "company" }
}
},
"invoke": { "label": "Start {label}", "command": [{ "kind": "refresh" }] }
}
}
#status_checklist
A checklist of process steps with per-step status + row actions. Same process
block as action_bar.
| Field | Type | Req | Notes |
|---|---|---|---|
process |
object | ✓ | { entityType, entityId, parentKeyFrom?, nodes, states, model } — as action_bar; the model's statusMap drives status display. |
actions |
array | Per-step row actions — { id, label, command, enabledWhen?, reinvokeLabel? }. |
|
header |
string | Optional heading. | |
noResultsMessage |
string | Copy when the process has no steps. | |
fullWidth |
boolean | Stretch the checklist to the full column width. |
{
"widgetComponent": "status_checklist",
"config": {
"header": "Voyage checklist",
"process": {
"entityType": "Voyage",
"entityId": "{{ page.params.id }}",
"nodes": "processNodes",
"states": "processStates",
"model": {
"fromConfig": { "slot": "voyage_process", "scope": "company" }
}
},
"actions": [
{ "id": "start", "label": "Start", "command": [{ "kind": "refresh" }] }
]
}
}
#custom_markup
An escape hatch: an authored markup tree with named action handlers.
| Field | Type | Req | Notes |
|---|---|---|---|
markup |
blockElement | ✓ | The markup tree to render. |
actionHandlers |
record | Named handlers → step-sequences invoked from the markup. |
{
"widgetName": "custom_markup",
"config": {
"markup": {
"type": "dataList",
"items": [
{
"label": "Vessel",
"content": "MV Northern Star"
},
{
"label": "Voyage",
"content": "2025-0142"
},
{
"label": "Status",
"content": "In transit"
},
{
"label": "ETA",
"content": "2025-09-14"
}
]
}
}
}
#Layout widgets
Layout widgets structure a page rather than showing data. They are placed as
inline nodes in a page's layout tree —
{ "component": "...", "config": {...}, "children": [...] } — instead of
entries in the widgets record; the grammar and its publish-time rules are
documented in
Apps (Bridge) → Page layout. The
containers receive their children from the tree; the leaves stand alone.
#section (container)
A titled, optionally collapsible group with a column grid.
| Field | Type | Req | Notes |
|---|---|---|---|
title |
string | Section heading. | |
columns / columnSpan |
int | Column grid inside / span when nested. | |
collapsible / defaultCollapsed |
boolean | Collapse behaviour. | |
justify |
enum | left / center / right. |
#row (container)
A horizontal run of leaf elements.
| Field | Type | Req | Notes |
|---|---|---|---|
justify |
enum | left / center / right / between. |
|
columnSpan |
int | Span when nested in a section grid. |
#tabs (container, top-level only, requires id)
Tabbed groups — each config.items[i] carries its own children slot.
| Field | Type | Req | Notes |
|---|---|---|---|
items |
array | ✓ | { id, label, children }[] — one slot per tab. |
variant |
enum | inline / segment. |
|
scrollable |
boolean | Scroll overflowing tab strips. |
#header (container, top-level only)
The sticky page header: a title plus filter controls. Not the text heading —
that's headline. Children must be filter elements
(filterButtons / filterDropdown).
| Field | Type | Req | Notes |
|---|---|---|---|
title |
string | Header title. |
#footer (container, top-level only)
The pinned bottom bar (the modal action-bar treatment). Children are text and
buttons — e.g. wrap an actions widget reference to pin it.
#divider (leaf)
A horizontal rule. No config.
#iframe (leaf)
An embedded external page. src must be https.
| Field | Type | Req | Notes |
|---|---|---|---|
src |
string | ✓ | https://… URL. |
height / width |
number | Pixel dimensions. |
#file_viewer (leaf)
An inline document viewer (PDF and other browser-renderable types) served through the platform's signed component URLs — the engine signs the source at render time, so the file URL itself is never exposed to the client unsigned.
| Field | Type | Req | Notes |
|---|---|---|---|
fileUrl |
string | ✓ | The file to render (often a {{ }} value). |
mimeType |
string | ✓ | The file's MIME type. |
fileName |
string | Display name. | |
startPage |
number | Initial page (paged formats). | |
height |
number | Viewer height (default 500). |
Every content widget above renders live on this page. The entity-bound widgets
(entity_info, related_collections, status_checklist, action_bar) are
shown with sample data the engine resolves at render time in a real app. The
data_grid is the one widget that needs a live data source ($query/$http) —
it renders its columns + actions but populates its rows only in a running app.
Layout widgets render as part of a page's layout tree rather than standalone, so
they are documented without live previews; see
Apps (Bridge) for an end-to-end app.