
# 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.

```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](#public-routes).            |
| `submitTo`     | Where the submission is persisted and/or routed: a table, an automation, the ledger, or a combination. Required. See [Submit Targets](#submit-targets).                    |
| `fields`       | Ordered list of field definitions rendered in the form. At least one required. See [Form Fields](/en/docs/form-fields).                                                    |
| `layout`       | Rendering mode: `single-page` (default), `multi-step`, or `one-question`. See [Multi-Step Forms](/en/docs/form-multi-step).                                                |
| `steps`        | Step definitions for `multi-step` / `one-question` layouts. See [Multi-Step Forms](/en/docs/form-multi-step).                                                              |
| `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](/en/docs/form-multi-step).                                                  |
| `access`       | Access control: `all`, `authenticated`, or a role list, with an optional `redirectTo`. See [Access Control](#access-control).                                              |
| `availability` | Submission window (`opensAt`, `closesAt`) and submission cap (`maxSubmissions`), with an optional custom closed-form page.                                                 |
| `antiSpam`     | Honeypot, sliding-window rate limits, and a CAPTCHA connection stub.                                                                                                       |
| `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}` / `$parent.{path}` reference or literal default. See [Form Fields](/en/docs/form-fields).                             |
| `submitter`    | Save-and-resume, edit-after-submit, and unique-submission rules.                                                                                                           |
| `onSuccess`    | Post-submit behavior (success page, redirect, reset, toast, inline message). See [Submissions](/en/docs/form-submissions).                                                 |
| `onError`      | What to show when a submission fails server-side (toast, inline message, error page). See [Submissions](/en/docs/form-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).                    |

```yaml
submitTo:
  table: support_tickets
  automation: page-on-call
  mapping:
    userEmail: email # form field 'userEmail' -> table column 'email'
```

:::callout
**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](/en/docs/form-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. |
| Embed route       | `/forms/{name}/embed` serves a minimal HTML shell for iframe embedding on third-party sites.                     |

## 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 `/`.     |

```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 }
```

:::callout
**`$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](/en/docs/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.

```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](/en/docs/form-controls) for the page-component side of `formRef`.

## Related Pages

- [Form Fields](/en/docs/form-fields) — standalone vs table-bound fields, prefill, inline relationship create.
- [Conditional Logic](/en/docs/form-conditional-logic) — visible / required / disabled rules.
- [Multi-Step Forms](/en/docs/form-multi-step) — steps, branching, one-question layouts, display overrides.
- [Submissions](/en/docs/form-submissions) — the dual-write ledger, success and error handling.
- [File Uploads](/en/docs/form-file-uploads) — attachment fields backed by buckets.
- [Tables Overview](/en/docs/tables-overview) — the data models forms write to.
- [Form Controls](/en/docs/form-controls) — embedding a form inside a page.
- [Auth](/en/docs/auth-overview) — roles and authentication behind `access`.
