Form Field Groups
A twenty-field form rendered as one flat column asks the submitter to read every input before understanding any of them. The same fields under three headings — "About you", "Your experience", "Why us" — become something a person can scan and answer a section at a time.
fieldGroups declares those headings. Each entry is a label plus the field names that belong under it:
forms:
- id: 1
name: apply
title: Apply for Senior Engineer
submitTo: { table: candidates }
fieldGroups:
- label: About you
fields: [first_name, last_name, email]
- label: Your experience
fields: [years_of_experience, github_url]
fields:
- { kind: table-field, column: first_name, required: true }
- { kind: table-field, column: last_name, required: true }
- { kind: table-field, column: email, required: true }
- { kind: standalone, name: years_of_experience, inputType: number }
- { kind: standalone, name: github_url, inputType: url }
Group Properties
| Property | Description | Default |
|---|---|---|
label |
Section heading rendered above the group's fields. Must be at least one character. | Required. |
fields |
Field names belonging to this section, in the order they should render. At least one required. | Required. |
visibleWhen |
Condition gating the whole section. Same shape and operators as a field's visibleWhen. |
Always visible. |
fieldGroups is itself optional. Omit it and the form renders fields[] top to bottom with no headings — which is the right choice for a form short enough not to need signposting. When present, the array must declare at least one group.
Order Comes From the Groups
Once fieldGroups exists, it drives layout: sections render in array order, and each field renders in the position its group gives it. That means grouping can reorder a form without touching fields[] — the fields[] array keeps defining what the fields are, the groups define where they appear.
A field you never list still renders. It falls through to the bottom, after every section, in fields[] source order. Grouping half a form is therefore valid: the named fields get headings, the rest trail behind as an unlabeled block.
Conditional Sections
visibleWhen gates an entire section — label and every field under it — on the value of another field, using the same condition primitive as field-level rules:
fieldGroups:
- label: Preferences
fields: [wants_relocation]
- label: Relocation
fields: [relocation_country, relocation_date]
visibleWhen: { field: wants_relocation, operator: eq, value: true }
The rule is evaluated server-side. On first render there are no answers yet, so a conditional section starts hidden and appears once its trigger is answered. On submit the rule is evaluated again, this time against the submitted values, and a section whose rule is false is treated as if it were never part of the form: its required fields do not block the submission.
A hidden section drops its values, it does not merely skip validation. When a group's visibleWhen is false at submit time, every field listed under it is stripped from the payload before the table write and the ledger write. A value the submitter typed while the section was open — then hid again by changing the trigger — is not persisted. Never put a field you always need inside a conditional group.
Groups Versus Steps
Both carve a form into parts, and both take a visibleWhen. The difference is what the submitter sees at once.
| Use | When |
|---|---|
fieldGroups |
All fields stay on one page and one scroll; headings give structure. Requires layout: single-page (the default). |
steps |
Fields are split across screens with prev/next navigation and per-step validation. Requires layout: multi-step or one-question. |
Related Pages
- Form Fields — the
fields[]entries a group refers to by name. - Conditional Logic — the operator vocabulary
visibleWhendraws on. - Multi-Step Forms —
steps[], the multi-screen alternative to groups. - Forms Overview — where
fieldGroupssits in the full form schema.
Last updated July 27, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta — contributions and corrections are welcome.