The Form Component
form — one type, two modes: table-bound writes through the records API, static submits to a declared form or to a URL of your own.
dataSource is what decides the mode. Declare one and the form is table-bound: its fields resolve against that table's columns, and submitting writes a record through the records API. Omit it and the form is static: it collects the fields declared on it and submits them to a form you declared in forms[] (formRef) or to a URL you name (endpoint). Nothing else about the component changes.
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
formRef |
string | Reference a top-level form by name (app.forms[].name). Renders that form inline. | ||
layout |
enum | single-column, two-column, custom |
Form layout mode: single-column | two-column | custom |
dataSource
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
dataSource |
object | Binds a component to table data. Supports list, single-record, and search modes with filtering, sorting, and pagination. | ||
dataSource.table |
string | Table to bind to: a declared name (validated against app.tables), or a $param.<name> route reference declared by the page path | ||
dataSource.fields |
array | Specific fields to fetch from the table | ||
dataSource.fields[] |
string | One field name, spelled as the bound table declares it | ||
dataSource.mode |
enum | list, single, search |
Data fetching mode: 'list' (multiple), 'single' (one record), 'search' (interactive) | |
dataSource.filter |
array | Filter conditions applied with AND logic | ||
dataSource.filter[] |
object (truncated) | Single filter condition for data source queries | ||
dataSource.sort |
array | Sort rules applied in order | ||
dataSource.sort[] |
object (truncated) | Single sort rule for data source queries | ||
dataSource.pagination |
object | Pagination configuration for data source | ||
dataSource.pagination.pageSize |
number | Number of records per page | ||
dataSource.pagination.style |
enum | numbered, loadMore, infinite |
How pagination controls are displayed (default: numbered). infinite is accepted but not implemented and pages as numbered. |
|
dataSource.param |
string | Route parameter name for single mode (e.g., slug, id) | ||
dataSource.searchEngine |
enum | client, fts, trigram, hybrid |
Search backend for this data source (default: 'client'). Only 'client' is dispatched today; the other three validate and search as 'client' does. | |
dataSource.searchFields |
array | Fields to search across in search mode | ||
dataSource.searchFields[] |
string | One field name the search term is matched against | ||
dataSource.debounceMs |
number | Debounce delay for search input (ms) | ||
dataSource.limit |
number | Maximum number of results to return | ||
dataSource.targetId |
string | Publisher-side identifier for cross-component references — addressable by a FilterAction (targetDataSource) and by a sibling subscriber's bindTo (shared filter/period state) | ||
dataSource.bindTo |
string | ID of a publisher component whose value drives this data source (cross-component binding). By default a search-input whose query string drives the search; when sharedFilter is also set, a shared filter/period selector whose published params are merged into every request |
||
dataSource.sharedFilter |
object | Companion to bindTo: the bound publisher is a shared filter/period selector whose published params are merged into every request this data source issues. One selector can drive many sibling subscribers. Inert without bindTo. | ||
dataSource.sharedFilter.params |
array (truncated) | Request-param keys this subscriber consumes from the shared publisher's value bag (omit to merge the full bag verbatim) | ||
dataSource.refreshMode |
enum | none, poll, realtime |
Data refresh strategy for this binding (default: 'none'). 'poll' uses pollIntervalMs; 'realtime' subscribes to live change events. | |
dataSource.pollIntervalMs |
number | 30000 | Polling interval in milliseconds for refreshMode: poll (min 1000, max 300000). Defaults to 30000 when omitted, and is ignored unless refreshMode is 'poll'. |
autoSave
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
autoSave |
object | Configuration for automatic persistence of edits. Applies to table, form, kanban, and calendar components. | ||
autoSave.saveMode |
enum | auto, onBlur, manual |
Save trigger strategy: 'auto' (debounced), 'onBlur' (field blur), 'manual' (button). Default: 'manual'. | |
autoSave.autoSaveDebounceMs |
number | Debounce delay for auto-save in milliseconds (default: 500, min: 100) | ||
autoSave.showSaveIndicator |
boolean | Display a save status indicator (Saving... / Saved / Error). Default: true when saveMode is auto or onBlur. | ||
autoSave.saveIndicatorPosition |
enum | inline, toast, toolbar |
Where the save status indicator appears |
search
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
search |
object | Shared search bar configuration for data-bound components (table, kanban, calendar) | ||
search.enabled |
boolean | Enable search bar (default: true) | ||
search.placeholder |
string | Search input placeholder text | ||
search.debounceMs |
number | Debounce delay for search input in ms (default: 300) | ||
search.highlight |
boolean | Highlight matched search terms in results (default: false) |
wizard
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
wizard |
object | Multi-step wizard configuration. Splits form fields into sequential steps with Next/Back navigation. | ||
wizard.steps |
array | Ordered list of wizard steps | ||
wizard.steps[] |
object (truncated) | One step of the wizard: its label, and the fields it collects |
inlinePrefill
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
inlinePrefill |
object | Auto-prefill relationship/scalar fields on an embedded form using values from the host page record. | ||
inlinePrefill.prefill |
object | Map of form-field column name to prefill value. Supports $parent.<field> tokens that resolve against the host page record. |
||
inlinePrefill.lockPrefill |
boolean | When true, prefilled fields render as hidden inputs and the server revalidates the parent on submit (returns 422 if the parent is gone). |
endpoint
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
endpoint |
object | Custom-endpoint submit target for a form: POST collected field values as JSON to an arbitrary url, with response-envelope tolerance and the shipped onSuccess effects (status + sibling refetch). | ||
endpoint.url |
string | Custom submit URL (any path; not the records API). e.g. /api/auth/admin/create-user | ||
endpoint.method |
enum | POST, PUT, PATCH |
HTTP method for the custom-endpoint submit (defaults to POST) | |
endpoint.responseEnvelope |
enum | sovrium, better-auth, raw |
sovrium | Response-envelope interpretation: sovrium (default), better-auth (always-200 enumeration-safe envelope at /api/auth/admin/*), raw (status-only, no body assumptions) |
endpoint.submitLabel |
string | Submit button label (defaults to the form submit label) | ||
endpoint.submitVariant |
enum | default, destructive, outline, secondary, ghost, link, fab |
Visual weight of the submit button, from the platform button vocabulary (the same members a button component accepts). Omit for the primary 'default' fill, unchanged. Set 'secondary' when a page stacks several small forms and a column of primary buttons would make every row look like the page's main action. |
|
endpoint.onSuccess |
object | Success handler for a fetch action: the toast slot plus optional client-state effects — a persistent inline status region (status), a sibling data-bound refetch (refetch), and a full-page reload (reload) that recomposes the page server-side. reload is mutually exclusive with status and refetch. | ||
endpoint.onSuccess.type |
enum | toast |
What the component does once the action returns — navigate away, reset the form, show a message or a success page, send the reader to their role landing, or raise a toast. | |
endpoint.onSuccess.message |
string | Toast notification message. Supports $variable references. | ||
endpoint.onSuccess.variant |
enum | default, success, destructive, error, warning, info |
Visual style of the toast notification | |
endpoint.onSuccess.duration |
number | Auto-dismiss duration in milliseconds (default: 5000) | ||
endpoint.onSuccess.actionLabel |
string | Label of an optional action button rendered inside the toast | ||
endpoint.onSuccess.actionUrl |
string | URL invoked (POST) when the toast action button is clicked. Required with actionLabel. | ||
endpoint.onSuccess.status |
object (truncated) | A persistent inline role="status" region populated on success (the persistent counterpart to a transient toast). | ||
endpoint.onSuccess.refetch |
string | array (truncated) | props.id (or array of ids) of sibling data-bound component(s) to re-query on success. Works for both a DB-table dataSource and a dataSource.system read endpoint. | ||
endpoint.onSuccess.reload |
boolean | When true, the browser reloads the page after a successful request so the SERVER recomposes it — the effect refetch cannot express, because refetch re-queries one region and skips any region holding a mounted island. Use it when the request changes something the server read at render time (the active language, the chrome). Refused at decode alongside status or refetch (both are same-page effects the reload subsumes); the required toast message is NOT displayed, because the reload replaces the document that would have shown it. | ||
endpoint.onError |
object | Toast notification rendered after a fetch action completes | ||
endpoint.onError.type |
enum | toast |
What the component does once the action returns — navigate away, reset the form, show a message or a success page, send the reader to their role landing, or raise a toast. | |
endpoint.onError.message |
string | Toast notification message. Supports $variable references. | ||
endpoint.onError.variant |
enum | default, success, destructive, error, warning, info |
Visual style of the toast notification | |
endpoint.onError.duration |
number | Auto-dismiss duration in milliseconds (default: 5000) | ||
endpoint.onError.actionLabel |
string | Label of an optional action button rendered inside the toast | ||
endpoint.onError.actionUrl |
string | URL invoked (POST) when the toast action button is clicked. Required with actionLabel. |
fields
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
fields |
array | Per-field configuration for form component (labels, placeholders, visibility) | ||
fields[] |
object | Per-field configuration for a form component | ||
fields[].field |
string | Field identifier: a table column name (table-bound form) OR the JSON body key (endpoint-bound form) | ||
fields[].control |
enum | text, email, password, number, tel, url, textarea, select |
Explicit input control for an endpoint-bound form field (text/email/password/number/tel/url/textarea/select). Omitted for table-bound forms (control derived from the column type). | |
fields[].options |
array (truncated) | Dropdown options for a control: select field ({ value, label? }) | ||
fields[].optionsSource |
object (truncated) | Dynamic option source for a choice control: table rows, or the rows of a system read endpoint | ||
fields[].label |
string | Custom label text (overrides default field name) | ||
fields[].description |
string | Guidance text rendered beside the control and linked via aria-describedby (overrides the bound field's description). Required to describe a control on an endpoint-bound form, which has no table field schema to resolve from. Unlike a placeholder it persists once the user starts typing. | ||
fields[].placeholder |
string | Placeholder text shown when field is empty | ||
fields[].readOnly |
boolean | If true, field is displayed but not editable | ||
fields[].disabled |
boolean | If true, field input is disabled | ||
fields[].defaultValue |
string | number | boolean | Default value for create mode. Supports static values or $variable references. | ||
fields[].hidden |
boolean | If true, field value is submitted but input is not rendered | ||
fields[].visibleWhen |
object (truncated) | Condition supporting simple comparisons and compound OR/AND logic for field visibility, required, and disabled states | ||
fields[].requiredWhen |
object (truncated) | Condition supporting simple comparisons and compound OR/AND logic for field visibility, required, and disabled states | ||
fields[].disabledWhen |
object (truncated) | Condition supporting simple comparisons and compound OR/AND logic for field visibility, required, and disabled states | ||
fields[].accept |
string | Comma-separated MIME types or extensions (e.g. "image/*,.pdf") | ||
fields[].dropZone |
boolean | If true, renders a drag-and-drop area for file uploads | ||
fields[].maxFiles |
number | Maximum number of files allowed (for multiple-attachments fields) |
fieldGroups
| Path | Kind | Values | Default | Description |
|---|---|---|---|---|
fieldGroups |
array | Groups form fields under labeled section dividers | ||
fieldGroups[] |
object | Groups form fields under a labeled section divider | ||
fieldGroups[].label |
string | Group label displayed as a section divider above the fields | ||
fieldGroups[].fields |
array (truncated) | Array of field names belonging to this group |
dataSource is table-only by design, since writes go to a table and never to a read endpoint; mode: single supplies current values for an edit form. layout is single-column by default, with two-column and custom beside it. fieldGroups divides the form into { label, fields } sections.
tables:
- name: contacts
fields:
- { name: email, type: email }
- { name: notes, type: long-text }
pages:
- name: Contact
path: /contacts/:id
components:
- type: form
dataSource: { table: contacts, mode: single, param: id }
layout: two-column
action: { type: crud, operation: update, table: contacts }
fields:
- { field: email, label: 'Email address' }
- { field: notes, control: textarea }Submitting to your own endpoint
endpoint is the third submit target, beside a table and a forms[] entry: the form collects its declared fields and POSTs them as a JSON body — { [field]: value } — to any URL you name. Nothing goes through the records API, so the destination can be a platform route, an admin endpoint, or something of your own. Each field must then name its own control.
url is required and takes any path or fully-qualified URL. method is POST by default, or PUT or PATCH. responseEnvelope decides how the response body is read when judging success or failure, and is sovrium by default. submitLabel overrides the button text, onSuccess runs on a 2xx — a toast, plus the client-state effects status, refetch and reload — and onError shows a toast when the submit fails.
submitVariant is for a page that stacks several forms. One form whose submit is the page's main action wants the primary fill, and gets it by declaring nothing. A settings page drawing six one-row forms down a column gets six primary buttons instead, none of which is the main action — so each declares a quieter weight and the page regains a single focal point. The vocabulary is the button component's own: default, destructive, outline, secondary, ghost, link, fab.
- type: form
endpoint:
url: /api/account/display-name
method: POST
submitLabel: Save
submitVariant: secondary
onSuccess: { type: toast, variant: success, message: Name saved }
onError: { type: toast, variant: destructive, message: Could not save the name }
fields:
- { field: name, control: text, label: Display name }The member list is resolved through the same recipe the button component uses, so a submit and a standalone button asking for secondary cannot drift apart.
Prefilling an endpoint form
An endpoint-bound field carries its own defaultValue. Nothing derives it from a column — there is no table binding — so it is the only way such a form opens on anything but empty controls. A static value fills a text control, and on a select it is the option that arrives already chosen.
A defaultValue naming $session.<field> is the caller's own value, and it is filled in the browser rather than during rendering:
- type: form
endpoint: { url: /api/invitations, method: POST, submitLabel: Send the invitation }
fields:
- { field: role, control: text, label: Role, defaultValue: member }
- { field: invitedBy, control: text, label: Invited by, defaultValue: $session.name }
- field: locale
control: select
label: Language
defaultValue: fr
options:
- { value: en, label: English }
- { value: fr, label: 'Français' }Why the identity is not resolved on the server. A page is composed once and may be cached, so resolving $session.name while rendering would write whoever requested it first into every copy handed out afterwards — one reader's name arriving in the next reader's form. The served bytes therefore name nobody: the server emits the template and the browser fills it against the caller's own session. An anonymous visitor gets an empty control, never the literal $session.name, and the static defaults beside it are unaffected, since they are the same for every reader.
The resolvable fields are email, name, role and id — the same set session-bound text resolves, through the same mechanism.
It is the one type a specimen may not draw
form is excluded from the design-system catalogue. It emits a submit control unconditionally, in both its create and its update branch, and a preview frame may carry no write path — so the catalogue reports the type and its reason rather than drawing it. That is a safety rule rather than a gap in the kit.
Behaviour
Authentication Page Components
action type auth method login strategy emailrenders login form- Login form validates email and password before submission
- Invalid credentials display error message
- Successful login navigates to
onSuccessnavigate path and creates session in database - Login form requires auth strategies to include email
action type auth method signup strategy emailrenders signup form- Signup form validates email and password before submission
- Successful signup creates a new user account (verified in
auth.usertable) and redirects - Password requirements are validated on signup
- Signup form requires auth strategies to include email
- OAuth control starts Better Auth social sign-in with a POST
onSuccessnavigate rides the social sign-in as itscallbackURL- OAuth login requires matching provider in auth strategies
visibility.when: authenticatedshows component only to logged-in usersvisibility.when: unauthenticatedshows component only to guestsroles: [admin]restricts visibility to admin role- Multiple roles use OR logic for visibility
whenandrolescombine with AND logicmethod: resetPasswordrenders password reset request form- Submitting the form sends a password reset email and creates verification record in database
- Password reset validates token, allows setting new password, and persists change in database
action type auth method logoutsigns the session out (and gates protected pages afterward)- OAuth sign-in renders one painted button, never a nested control
- User can complete full auth-components workflow (regression)
- Logout button ends the session and gates protected pages (regression)
Create Record Form
action type crudwithoperation createrenders a form- Form fields are generated from table schema definition
- Required fields validated before submission
- Successful creation triggers
onSuccessredirect and persists record in database - Create operation requires create permission
create: 'all'renders the form for anonymous AND signed-in callers- An admin sees a create form whose role allowlist omits
admin - A
statusfield left untouched creates the record with its declareddefault - A
relationshipfield left untouched is stored as NULL, not an empty string - A
multi-selectfield left untouched is stored as NULL, not an empty string - A
progressfield left untouched is stored as NULL, not an empty string - Every constrained field type left empty is omitted from the create payload
- SSR skeleton renders a
statusfield as aselectwith its options and default - Hydrated form renders a
statusfield as a combobox with its options and default - A
barcodefield is omitted when empty only if its own config constrains it - User can complete full create record workflow (regression)
- User can create a record leaving every constrained field empty (regression)
- User creates a record whose barcode fields differ only in config (regression)
Update Record Form
operation updaterenders form pre-filled with existing data- Form fields reflect current record values from data source
- Modified fields validated before submission
- Update operation requires update permission
update: 'authenticated'renders the form for every signed-in role, hidden for anonymous- Successful update persists changes to database
- Successful update triggers
onSuccessredirect - User can complete full update record workflow (regression)
Delete with Confirmation
operation deletetriggers delete actionconfirm: truedisplays confirmation dialog before delete- Delete uses soft-delete by default (verified by
deleted_attimestamp in database) - Successful deletion triggers
onSuccesshandler (with database confirmation of soft-delete) - Delete operation requires delete permission
- User can complete full delete workflow (regression)
Form Validation Feedback
- Inline field errors displayed next to invalid field
- Summary error display shows all errors at top of form
- Server-side validation errors mapped to form fields
- User can complete full validation workflow (regression)
- Server-rejected values are marked on the field they belong to (regression)
CRUD with Attachment Fields
- Create form with
single-attachmentfield uploads file and stores reference in record - Create form with
multiple-attachmentsfield uploads files and stores array in record - Update form pre-fills attachment field with existing file (shows filename/thumbnail)
- Update form allows replacing an existing single attachment with a new file
- Update form allows removing an existing attachment (sets field to null)
- Update form allows adding/removing individual files in a multiple-attachments field
- Delete record with attachments cleans up stored files from storage backend
- User can complete full CRUD with attachments workflow (regression)
Configurable, localizable CRUD form labels
- Action
submitLabeloverrides the built-in "Create" submit-button text - Action
fields[]override the table-derived field labels and placeholders submitLabel+ field labels localize through pagemeta.lang+ applanguages- Full configurable + localized CRUD-form label workflow
Auto-Generated Form from Table
type: formwithdataSource.tableauto-generates fields from table schema- Each table field type maps to appropriate input (text->input, select->dropdown, date->datepicker, richtext->editor, number->number input, boolean->checkbox, single-attachment->file upload, multiple-attachments->multi-file upload)
fieldsarray includes/excludes/reorders specific fields- Required table fields show required indicator and validate before submit
dataSource.mode: singlepre-fills form with existing record (edit mode)action.type: crudwithoperation: createorupdatedetermines form behavior- User can complete full auto-generated form workflow (regression)
Field Configuration
- Per-field
labeloverrides default field name - Per-field
placeholdershows hint text - Per-field
readOnly: truerenders non-editable display - Per-field
defaultValue(static or$variable) pre-fills on create - Per-field
hidden: truesubmits value without rendering input - Per-field
descriptionrenders help text linked byaria-describedby, overriding the bound field'sdescription - User can complete full field configuration workflow (regression)
Conditional Fields
visibleWhencondition shows/hides field based on another field's value- Condition operators:
eq,neq,contains,empty,notEmpty - Hidden fields excluded from validation
- Multiple conditions on same field evaluated with AND logic
- User can complete full conditional fields workflow (regression)
Form Layout
layout: single-columnstacks fields vertically (default)layout: two-columnrenders fields in 2-column responsive gridlayout: customallows wrapping fields inchildrensectionsfieldGroupsgroups fields with label dividers- A form NESTED IN A CARD still lays out as the column it declares, and its submit is separated from the last field by a real distance — measured as computed style and geometry, never asserted by class, because an inline
displayfrombuildEmptyElementStylesbeats every display utility and leaves the classes painting nothing - User can complete full form layout workflow (regression)
Form Actions
action.type: crudcreates/updates recordaction.type: automationtriggers automation with form data as inputData- Combined: CRUD action first, then automation receives created record ID
- Submit button
props.labelandprops.variantcustomizable - User can complete full form actions workflow (regression)
Validation & Feedback
- Inline field-level error messages on blur and submit
- Summary error banner at form top listing all errors
- Success: onSuccess toast + navigate triggered after successful submit
- User can complete full validation workflow (regression)
File Upload Fields
single-attachmentfield type renders a file upload input (file picker button)multiple-attachmentsfield type renders a multi-file upload input (supports selecting multiple files)acceptprop restricts selectable file types in the browser file dialog (e.g.,image/*,.pdf)- File size validation rejects files exceeding
maxFileSizefrom table field definition - File type validation rejects files not matching
allowedFileTypesfrom table field definition dropZone: truerenders a drag-and-drop area for file selection- Upload progress indicator shown while file is uploading
- Image files display a thumbnail preview after selection; non-image files display filename and size
- Selected files can be removed before form submission
- Multi-file input enforces
maxFileslimit from table field definition - Edit mode pre-fills file inputs with existing attachment data (filename, thumbnail) and allows replace/remove
- File is uploaded (to storage backend) and reference stored in record on form submit
- Validation error shown when required attachment field has no file selected
- The upload is POSTed to the bucket declared on the bound column, and the preview URL names that same bucket
- Edit mode renders an existing stored attachment against the column's declared bucket
- A file chosen before the upload island mounts is kept: the filename previews and the upload POSTs exactly once
- Text typed before a tab-panel form mounts survives and is submitted — the visible input and the saved row both carry what was entered, not what the page loaded with
- User can complete full file upload form workflow (regression)
- Uploads and previews honour the column's declared bucket end-to-end (regression)
File Upload with Automation
$form.{field}references for attachment fields resolve to file metadata object (url, name, size, mimeType)$form.{field}formultiple-attachmentsresolves to array of file metadata objects- Automation
inputDatacan reference uploaded file via$form.fileand receive file metadata - Combined CRUD + automation: record created with attachment, then automation receives record with file URL
- User can complete full file upload with automation workflow (regression)
Form Success Page
onSuccess.type: successPagereplaces the form with success page content after successful submission- Success page with
redirectnavigates to the specified URL after a 2-second delay (supports$record.idvariables) showSummary: truedisplays a read-only summary of the submitted field values on the success page- Success page
actionswithaction: resetresets the form to initial state for a new submission - Success page renders a checkmark icon, the configured title, message, and any action buttons — and the actions are painted rather than transparent, the navigate link reading as the quieter of the pair
- User can complete full form success page workflow (regression)
Form Reset After Success
onSuccess.type: resetclears all fields to their default values and shows a success toast after submissionpreserveFieldsarray retains specified field values while clearing all others on reset- Multi-step wizard form resets to step 1 when
onSuccess.type: resetis triggered - The
onSuccess.type: resetschema on the in-page form component is parity-compatible with the top-level form schema — samepreserveFieldssemantics, same wizard reset behavior - User can complete full form reset after success workflow (regression)
Advanced Conditional Fields
- visibleWhen with or array shows field when ANY condition matches
- nested and/or conditions evaluate correctly
- requiredWhen condition makes field required only when condition is met
- requiredWhen unmet allows form to submit without that field
- disabledWhen condition renders field as disabled when met
- disabled field becomes editable again when disabledWhen no longer met
- multiple fields depending on same source field all update when source changes
- hidden conditional field has its value excluded from submission payload
- visibleWhen references field in previous wizard step
- layout reflows when conditional field becomes hidden in grid
Custom-Endpoint Submit
- An endpoint-bound form renders its explicit-
controlfields (typed inputs + aselect) and the custom submit button - Submitting the form POSTs the custom endpoint with the collected field values as a JSON body (not the records API)
onSuccess.refetchre-reads the siblingdataSource.systemgrid so the new row appears without a reload- An endpoint-bound field honours
defaultValue, so the control arrives carrying what it is about to change rather than an empty box - A
defaultValueof$session.<field>ships as a TEMPLATE and is filled CLIENT-side, so the served bytes name nobody and a cached copy cannot carry one reader's identity to the next - An anonymous visitor gets an empty control, never the literal token
endpoint.submitVariantpaints the submit in the platform button vocabulary's own words, and omitting it leaves the submit exactly as it is today- A
selectwhose$session.<field>default resolves to nothing keeps its authored option instead of going blank, while one that resolves still moves to its value endpoint.onSuccess.reloadmakes the SERVER recompose the page, and a form without it re-reads nothing- User can render explicit controls, POST the custom endpoint, and refetch the sibling system grid (regression, dual-dialect)
Reference Top-Level Form
- The
type: 'form'component accepts aformRef: <name>field that referencesapp.forms[].name; an unknown name fails app schema validation with an error naming the page, the component, and the missing form - When
formRefis set, the component inherits ALL behavior from the referenced top-level form:fields[], layout,steps[](multi-step), conditional logic, attachment fields,onSuccess, andonError formRefis mutually exclusive with each ofdataSource,fields, andfieldGroups; setting any of those alongsideformReffails validation with a single error citing the conflict- When
formRefis set, the only honored component-level overrides are display-layer props:props.label(submit button),props.variant(button variant),responsive, andvisibility; component-levellayoutandactionprops are ignored with a single warning at validation time - The host page's access rules (e.g.
pages[].access: authenticated) apply when the form is rendered throughformRef; the same top-level form remains independently reachable at/forms/{name}per its own (default-public) access rules - Submitting the embedded form follows the top-level form's
submitToandonSuccess/onErrorexactly as if the user had submitted at/forms/{name}(one shared submission ledger row, one bound table write, one bound automation invocation)
Inline Multi-Step Wizard Layout
wizard.steps[]requiresminItems: 1; an empty or missingsteps[]array fails validation with a clear error naming the form component- Each step's
fields[]references field names that exist on the inline form'sfields[](or, when fields are auto-generated, on the bound table's columns); unknown names fail validation - The renderer displays one step at a time with a progress indicator showing current step label and total step count; Next and Back buttons navigate between steps
- Next-button click validates the current step's required+visible fields; failing validation blocks advancement and surfaces inline errors on the current step
- Submit fires only on the final step; the request body includes values from all steps (skipped fields are omitted)
- The
wizard:schema accepted on the form component is identical whether the component appears underpages[].components[], top-levelapp.components[], or nested inside another component'schildren[] - Setting
wizard:andformRef:on the same component fails validation with an error explaining that wizard layout flows from the referenced form'slayout: multi-stepdefinition instead - The wizard Back, Next and Submit controls are painted rather than transparent, and Back reads as the quieter of the pair
- User can complete full inline-form wizard workflow (regression)
Last updated September 23, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.