Forms Overview
Forms capture input from submitters and route it somewhere useful — into a table, through an automation, into the built-in submission ledger, or any combination of the three. They are declared in the top-level forms array, each one reachable at its own URL, embeddable inside a page, and addressable from an automation trigger.
A form is the public-facing counterpart to a table: where a table defines what data looks like, a form defines how people contribute it. The same form can write directly to a tables[] entry, fire a notify-sales automation, or simply land in the platform's form_submissions ledger for later review.
forms:
- id: 1
name: contact
title: Contact Sales
path: /contact
submitTo:
table: leads
fields:
- { kind: table-field, column: email, required: true }
- { kind: table-field, column: message }Form Properties
Each entry in the forms array accepts the following properties.
| Property | Description |
|---|---|
id |
Unique positive integer identifying the form. Must be unique across forms[]. |
name |
Kebab-case unique identifier (^[a-z][a-z0-9-]*, 1–64 chars). Referenced by page components (formRef) and the form automation trigger. Must be unique across forms[]. |
title |
Human-friendly title shown to submitters. Supports $t: i18n keys. |
description |
Optional intro paragraph rendered above the first field. |
path |
Optional friendly public URL (e.g. /contact). When set, the form is served there and at the canonical /forms/{name}. See Public Routes. |
submitTo |
Where the submission is persisted and/or routed: a table, an automation, the ledger, or a combination. Required. See Submit Targets. |
fields |
Ordered list of field definitions rendered in the form. At least one required. See Form Fields. |
layout |
Rendering mode: single-page (default), multi-step, or one-question. See Multi-Step Forms. |
steps |
Step definitions for multi-step / one-question layouts. See Multi-Step Forms. |
fieldGroups |
Labeled section dividers grouping fields within a single-page layout. |
display |
Cosmetic options (columns, progress bar, submit label, per-form theme). See Multi-Step Forms. |
access |
Access control: all, authenticated, or a role list, with an optional redirectTo. See Access Control. |
availability |
Submission window (opensAt, closesAt) and submission cap (maxSubmissions), with an optional custom closed-form page. |
antiSpam |
Honeypot and sliding-window rate limits. |
analytics |
Per-form opt-out (enabled: false) excluding the form from aggregate analytics. Defaults to enabled. |
prefill |
Map of form field → $query.{name} / $user.{prop} reference or literal default. See Prefill. |
submitter |
Save-and-resume, edit-after-submit, and unique-submission rules. |
onSuccess |
Post-submit behavior (success page, redirect, reset, toast, inline message). See Submissions. |
onError |
What to show when a submission fails server-side (toast, inline message, error page). See Submissions. |
Submit Targets
The submitTo object decouples a form from any single persistence model. At least one of table, automation, or storeSubmission: true (the default) must be set — otherwise the submission would be discarded silently and the schema rejects the config.
| Property | Description |
|---|---|
table |
Persist the submission as a record in this table. References a tables[].name; validated against the app's tables[] array. |
automation |
Invoke this automation after the writes commit. References an automations[].name; validated against the app's automations[] array. |
mapping |
Map form field names to destination column names. Defaults to identity (form field name = table column name). A mapping target that does not exist on the table fails validation. |
storeSubmission |
Persist the submission in the built-in form_submissions ledger. Defaults to true — set false to opt out (and then table or automation is mandatory). |
submitTo:
table: support_tickets
automation: page-on-call
mapping:
userEmail: email # form field 'userEmail' -> table column 'email'Tables, automations, the ledger — or all three. A form can dual-write to a table and the ledger, fire an automation, or do everything at once. The table write and the ledger write happen inside a single transaction; the automation runs only after both commit. See Submissions for the full dual-write contract.
Public Routes
Every form is reachable at the canonical route /forms/{name} regardless of configuration — a form without a path is not private, it is simply only reachable there. When path is set, the form is served at that custom path and at /forms/{name} simultaneously (no redirect; both URLs render the same form).
| Rule | Description |
|---|---|
| Canonical route | /forms/{name} always serves the form (HTTP 200). An unknown name returns 404. |
| Custom path | Optional. Must start with /, 2–256 URL-safe characters; static (no :id dynamic segments). |
| Reserved prefixes | /api/, /admin/, /forms/, and /auth/ are rejected so a form cannot shadow a built-in route. |
| Collision rule | A forms[].path MUST NOT collide with any pages[].path. Collisions fail validation with an error naming both. |
Use the canonical route for anything gated. access and availability are enforced on /forms/{name}. Serve a restricted or time-limited form from there rather than from a custom path.
Access Control
The access object reuses the shared permission model used by tables, pages, buckets, automations, and agents.
| Property | Description |
|---|---|
require |
'all' (everyone, including anonymous), 'authenticated' (any logged-in user), or a role list (['admin', 'editor']). |
redirectTo |
Optional path (e.g. /login) to redirect denied submitters to, instead of returning a 401/403. Must start with /. |
forms:
- id: 2
name: member-survey
title: Member Survey
access:
require: authenticated
redirectTo: /login
submitTo: { table: survey_responses }
fields:
- { kind: standalone, name: rating, inputType: rating, required: true }$user.* references require authentication. A form whose access.require is 'all' (the default) may not carry a per-field defaultValue referencing $user.* — the value would always resolve empty for anonymous visitors and could leak session state. Set access.require to authenticated or a role list, or use top-level prefill (which silently drops unresolvable $user references on public forms). See Form Fields.
Embedding a Form in a Page
A top-level form can be rendered inline inside a page via the form control's formRef. The form is defined once and reused anywhere — fields, validation, conditional logic, multi-step layout, file uploads, and onSuccess/onError all flow from app.forms[]. The host page's access control is intersected with the form's own rules at render time.
pages:
- id: 1
name: landing
path: /
components:
- type: form
formRef: contact # renders the top-level "contact" form inline
props:
label: Get in touch # display-only override of the submit labelSee Form Controls for the page-component side of formRef.
Related Pages
- Form Fields — standalone vs table-bound fields, prefill, inline relationship create.
- Conditional Logic — visible / required / disabled rules.
- Multi-Step Forms — steps, branching, one-question layouts, display overrides.
- Submissions — the dual-write ledger, success and error handling.
- Access Control — the
accessblock in full, and what a denied submitter gets. - Availability — submission windows, caps, and the closed-form page.
- Anti-Spam & Attribution —
antiSpam,submitter, and the analytics opt-out. - File Uploads — attachment fields backed by buckets.
- Tables Overview — the data models forms write to.
- Form Controls — embedding a form inside a page.
- Auth — roles and authentication behind
access.
Last updated August 11, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.