Skip to main content
View as Markdown

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.

app.yaml
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).
app.yaml
submitTo:
  table: support_tickets
  automation: page-on-call
  mapping:
    userEmail: email # form field 'userEmail' -> table column 'email'

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.

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 /.
app.yaml
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 }

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.

app.yaml
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 label

See Form Controls for the page-component side of formRef.

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.

Built with Sovrium