UI builder reference
The technical companion to Pages, Forms and Navigation: configuration formats, field options and the server-function hooks. Everything here is stored as configuration in your instance and editable through the designers — you only need this page when integrating, importing or authoring configs by hand.
Form definition
A form is one JSON document:
| Key | Meaning |
|---|---|
schema, table |
Target table for table-backed forms (omit for action forms) |
layout |
grid — fields place on a 12-column grid via col_span |
present |
page, modal — how the form opens |
present_config |
Presentation extras, e.g. { "icon": "Contact" } |
fields |
Ordered field list (below) |
submit |
Custom submit behavior (below); omit for direct table writes |
buttons |
Labels, e.g. { "submit_label": "Create contact" } |
{
"schema": "crm",
"table": "contact",
"layout": "grid",
"present": "modal",
"fields": [
{ "name": "name", "type": "text", "label": "Name", "required": true, "col_span": 6 },
{ "name": "email", "type": "text", "label": "Email", "col_span": 6 },
{
"name": "account_id", "type": "reference", "label": "Account", "col_span": 12,
"reference": { "schema": "crm", "table": "account", "display": "name" }
}
],
"buttons": { "submit_label": "Create contact" }
}
Field types
type |
Notes |
|---|---|
text, textarea |
Plain text; minLength/maxLength validations |
number |
Numeric with optional range |
boolean |
Switch/checkbox |
date, time, datetime |
Pickers |
select |
Dropdown; options static or computed (below) |
cardselect |
Rich clickable options: { "value", "label", "description", "icon" } |
reference |
FK picker: reference: { schema, table, display } |
file, image |
Uploads; image supports client-side crop/resize |
json, code |
Structured/technical input |
Common field keys: name, label, required, col_span (1–12),
placeholder, default, visible.
Dynamic options
options_mode selects where choices come from:
static— inlineoptionsarray.fn— a server function returns the options at open time. The function is stored under the__fn_options__key and runs with the caller's permissions:
{
"name": "warehouse", "type": "select", "options_mode": "fn",
"__fn_options__": "async (ctx) => { const rows = await ctx.datacore.table('erp','warehouse').list({ limit: 50 }); return rows.map(w => ({ value: w.id, label: w.name })); }"
}
Visibility rules
{
"visible": {
"mode": "manual",
"rules": [{ "field": "tier", "op": "eq", "value": "enterprise" }]
}
}
Operators: eq, neq, truthy, falsy, in.
Submit handlers
With no submit key, a table-backed form writes the record through the data
API under the caller's role. A custom handler is a
server function stored under
__fn_submit__; its return value drives the UI:
{
"submit": {
"__fn_submit__": "async (ctx) => { const row = await ctx.datacore.table('crm','contact').create(ctx.values); return { ok: true, alert: { type: 'success', message: 'Contact created' }, navigate: 'refresh' }; }",
"on_success": "refresh"
}
}
Handler context: ctx.values (form values), ctx.user, ctx.datacore
(data API bound to the caller: .table(schema, table).list/get/create/update/delete,
.action(schema, fn).execute(args) for gated RPC). Return shape:
{ ok, alert?: { type, message }, navigate?: "refresh" | url }.
All
__fn_*__source stays server-side — clients only ever send a reference to run it, never receive the code.
Widget configuration
Widgets share type, optional title, col_span, roles (required roles)
and access_effect (hide | warning).
{ "type": "metric", "title": "Open deals",
"data_source": { "schema": "crm", "table": "deal" },
"aggregate": "count",
"filters": [{ "field": "stage", "op": "neq", "value": "won" }] }
{ "type": "table",
"data_source": { "schema": "crm", "table": "deal" },
"columns": ["title", "stage", "value"],
"page_size": 25,
"filters": [{ "field": "owner_id", "op": "eq", "value": "{{ user.id }}" }],
"row_actions": [
{ "label": "Edit", "form": "crm_deal_edit", "present": "modal" },
{ "label": "Mark won", "confirm": "Mark this deal as won?",
"__fn_action__": "async (ctx) => { await ctx.datacore.table('crm','deal').update(ctx.row.id, { stage: 'won' }); return { ok: true, navigate: 'refresh' }; }" }
] }
{ "type": "tab",
"tabs": [
{ "label": "Overview", "children": [ { "type": "metric", "…": "…" } ] },
{ "label": "Forecasts", "roles": ["sales_manager"], "children": [ "…" ] }
] }
Pages may also declare a preload server function (__fn_preload__) whose
result is shared by all widgets on the page.
{{ user.id }} in filter values resolves to the signed-in user at query
time, server-side.
Navigation items
| Field | Meaning |
|---|---|
label, icon |
Display |
url |
The item's URL segment (composed down the parent chain) |
is_group |
Container vs page link |
parent |
Position in the tree |
roles |
Required roles; app items additionally require the app's membership role |
Segments may declare parameters (:id) that the linked page's widgets
receive as route parameters.