Documentation

UI builder

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 — inline options array.
  • 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.