# Dynamic Forms — LLM Guide

A form is a JSON schema stored in `forms.forms` and rendered at `https://form.thesqd.com/f/{id}`. This is the authoring reference: skim the TOC, jump to what you need, copy from the examples.

Built on `@coltorapps/builder` (entity graph) + `@thesqd/squad-ui` (controls). Each field entity maps to one squad-ui component — see the catalog at the bottom.

## Table of contents

**Fundamentals**
- [How forms render](#how-forms-render)
- [Form storage and access](#form-storage-and-access)
- [Status workflow](#status-workflow)

**Schema**
- [Schema shape](#schema-shape)
- [Pages](#pages)
- [Headers and inline text](#headers-and-inline-text)
- [Attribute reference](#attribute-reference)

**Conditional logic & dynamic data**
- [Visibility rule shape](#visibility-rule-shape)
- [URL params: prefill, template variables, prefetch keys](#url-params-prefill-template-variables-prefetch-keys)
- [Theme via URL](#theme-via-url)
- [Prefetching external data into template vars](#prefetching-external-data-into-template-vars)

**Runtime & integration**
- [Embedding](#embedding)
- [Submissions](#submissions)
- [Submission shape](#submission-shape)
- [In-progress, resume, abandonment](#in-progress-resume-abandonment)

**Authoring**
- [Common authoring mistakes](#common-authoring-mistakes)
- [Quick INSERT recipe](#quick-insert-recipe)
- [End-to-end example schemas](#end-to-end-example-schemas)

**Reference**
- [Field entity catalog](#field-entity-catalog)

## How forms render

The schema is a flat `entities` map plus a `root` array of page IDs — no "form object". The tree has one container type (`formPage`); `repeatableGroup`, `panelContentField`, and `subformField` look like containers but are field entities with structured values (their inner shape comes from `fields` / `panels` / `formId`).

1. Runtime walks `schema.root` (ordered page IDs).
2. For each page, renders its `children` field IDs in order.
3. Each entity's `attributes` are validated against its definition in `src/lib/forms/entities/*.ts`, then the matching component from `src/components/forms/fields/*.tsx` renders (a squad-ui primitive under the hood).
4. Values live in `entitiesValues` (id → value). Next is blocked while any required visible field on the current page is invalid.
5. `visibilityRule` is the only mechanism for conditional rendering: false → field is excluded from validation AND the final payload. Same applies to pages.
6. On submit, the runtime walks the tree, builds the `response` array, classifies URL params, and writes a row to `forms.form_submissions`.

Layout-only entities (`sectionHeader`, `alertBanner`, `imageDisplay`) never appear in the response.

## Form storage and access

Table: `forms.forms`. Key columns:

- `id text` — primary key, also the URL slug (e.g. `VpFFPutB`).
- `schema jsonb` — the form schema (shape below).
- `form_title text` — visible title above the form.
- `status` — `draft | published | archived`. Only `published` is reachable at `/f/{id}`; the others 404.
- `name`, `description`, `form_tags`, `created_at`, `updated_at` — admin metadata.

Related tables you'll rarely touch: `forms.form_submissions` (one row per attempt — see Submissions), and the legacy join tables `forms_fillout_mapping`, `forms_project_types`, `forms_to_tasks_content`.

Inspect via MCP (`mcp__supabase-squad-data__execute_sql`):

```sql
SELECT id, name, schema FROM forms.forms WHERE id = 'VpFFPutB';
```

Create one by INSERTing a unique `id`, `name`, `schema`, and `status = 'published'`. It's then live at `https://form.thesqd.com/f/{id}`.

## Status workflow

`UPDATE forms.forms SET status = 'published' WHERE id = '…';` — takes it live. Set back to `draft` or `archived` to hide without losing the row.

## Schema shape

Top-level keys on `schema` (full TS in `src/lib/forms/types.ts`):

**Required**
- `root: string[]` — ordered formPage IDs. First ID = first page.
- `entities: Record<string, Entity>` — every entity by ID. Use kebab-case slugs.

**Optional**
- `successTitle?`, `successMessage?` — success-screen copy (defaults: `"Thanks!"` + empty body).
- `hideFormTitle?`, `showTitleOnFirstPageOnly?` — control where `form_title` renders.
- `maxWidth?: "sm"|"md"|"lg"|"xl"|"2xl"|"3xl"|"4xl"|"5xl"|"6xl"|"7xl"|"full"` — desktop content width, applied to the form's centered grid container. Default `3xl`. Bump for layouts that need horizontal room: long file-upload forms tend to want `4xl`; multi-column card pickers (e.g. `multiSelectField` `displayStyle="cards"` with `columns: 3+`) usually want `6xl` so each card has breathing room.
- `defaultValues?: Record<string, unknown>` — `entityId → initial value`. Strings support `{{tokens}}`. Lives at schema root, NOT inside per-entity attributes (the builder rejects unknown attribute keys).
- `prefetch?: PrefetchConfig` — declarative GET that hydrates template vars, prefills fields, and feeds visibility rules. See [Prefetching](#prefetching-external-data-into-template-vars).
- `onSubmitRedirect?: OnSubmitRedirectRule[]` — list of `{ when?: VisibilityRule, url: string, params?: Record<string,string> }`. First matching rule (or one with no `when`) sends the user to `url` with `params` appended. Both support `{{tokens}}`.

**Entity shape**

```json
{ "type": "<entityName>", "attributes": { ... }, "children": ["id-1"], "parentId": "page-1" }
```

`children` is set on `formPage` only. `parentId` is set on every non-page entity. `children` is the source of truth — keep `parentId` consistent anyway.

### Minimal example

```json
{
  "root": ["page-1"],
  "entities": {
    "page-1": {
      "type": "formPage",
      "attributes": { "label": "Contact" },
      "children": ["name"]
    },
    "name": {
      "type": "textField",
      "attributes": { "label": "Your name", "required": true },
      "parentId": "page-1"
    }
  }
}
```

## Pages

Every form has at least one `formPage`. Add more by appending entity IDs to `root`; the stepper renders automatically (hidden when there's only one page).

`formPage` accepts `label`, `description`, `hideHeader`, `visibilityRule`, and `pageNav`. `hideHeader: true` suppresses the page's own title/description block.

**`pageNav` — schema-driven floating action bar.** Replaces the default Back/Next strip for that page with a glassy `FloatingShell` (from `@thesqd/squad-ui`) containing the declared buttons, plus an optional dark selection popover stacked above the bar.

```json
"pageNav": {
  "buttons": [
    { "kind": "save",     "label": "Save Kit",          "icon": "Save" },
    { "kind": "back" },
    { "kind": "clear",    "label": "Clear" },
    { "kind": "continue", "label": "Continue ({count})" },
    { "kind": "submit",   "label": "Submit ({count})" }
  ],
  "selection": {
    "fromField": "field-project-types",
    "countLabel": "Selected ({count})"
  }
}
```

Each button's `kind` maps to a hard-coded behavior — no callbacks ship through JSON:

| Kind | Behavior |
|------|----------|
| `back` | Previous page. Hidden automatically on the first page. |
| `continue` | Next page (Next-disabled-style validation applied). Hidden on the review page. |
| `submit` | Submit the form. Only shown on the review page. |
| `clear` | Clears `selection.fromField`'s value. Hidden when no field is linked or nothing is selected. |
| `save` | No-op standalone. Dispatches `window` `CustomEvent('form:nav-action', { detail: { action: 'save', values, selectionFieldId } })` so a parent embed can hook in. |

`label` supports `{count}` (selection field's current value count) and any `{{token}}` from URL params / prefetch vars (same engine as labels elsewhere). `icon` is a lucide-react component name (e.g. `"Save"`, `"BookmarkPlus"`); resolved client-side via the `icons` export — invalid names fall back to the kind's default icon.

The optional `selection` block enables a dark popover above the bar that lists removable chips for each selected value on `fromField` (multiSelectField / recordPickerField). Chips read `label` + `image` from the field's resolved option list (populated by `optionsPrefetch.map` when applicable), so no extra data wiring is needed — the popover and the chips reflect whatever the underlying field already renders. `Clear all` clears the field; per-chip × dispatches `form:nav-action` with `action: 'remove-selection'` so a parent embed can react.

### `pageSearchFilter` — tab matches and `fromPrefetch` membership

Each tab on a `pageSearchFilter` can declare an optional `match` that gates the controlled field's options when the tab is active. Two shapes are supported:

```json
// Equality — option[field] === value
{ "key": "design", "label": "Design", "icon": "Palette",
  "match": { "field": "section", "value": "Design" } }

// Membership in a list pulled from the controlled field's RAW prefetch
// response. `fromPrefetch` is a JSON path into the un-narrowed response
// (so sibling slices outside the field's `path` are reachable). The
// resolved value must be an array; the option matches when option[field]
// is in it.
{ "key": "most-used", "label": "My Most Used",
  "match": { "field": "value", "fromPrefetch": "mostUsedTags.data.most_used_tag_ids" } }

// Same, but the list is built by iterating an array of rows. A `*`
// segment in the path walks every item in the current array, applies the
// remainder of the path, and flattens one level — so the example below
// unions every kit's `project_ids` into a single membership set.
{ "key": "squadkits", "label": "SquadKits",
  "match": { "field": "value", "fromPrefetch": "squadKits.data.*.project_ids" } }
```

Tabs without `match` are pass-through (e.g. "All Projects"). The field publishes its raw prefetch response into a shared context keyed by entity id, so the filter resolves `fromPrefetch` paths without re-fetching. `*` in the path is the only thing the SDK adds on top of plain dotted access — every other shape (flat scalar list, nested object property, multi-level traversal) is expressible with normal `a.b.c` segments.

### `pageSearchFilter` — bundle tabs via `optionsFromPrefetch`

A tab can also REPLACE the controlled field's options with rows from a different slice of the same prefetch response. Each card is a "bundle"; toggling it unions or removes its `valuesPath` array onto the underlying field value:

```json
{
  "key": "squadkits",
  "label": "SquadKits",
  "optionsFromPrefetch": {
    "path": "squadKits.data",
    "map": {
      "value": "id",
      "label": "title",
      "description": "description",
      "image": "image_url"
    },
    "valuesPath": "project_ids"
  }
}
```

When this tab is active the field renders one card per kit. The field's stored value is still the flat array of project ids — the bundle is just a higher-level interaction surface. A bundle card is "selected" iff every id in its `valuesPath` is already in the value; clicking it unions/diffs the bundle's ids.

**Conditional pages.** Pages use the same `visibilityRule` shape as fields ([Visibility rule shape](#visibility-rule-shape)). When the rule is false, the page is dropped from the stepper, Next/Back, and review; its children's values are cleared so nothing silently submits. If the user is on a page that becomes hidden, the current-page index clamps.

```json
{
  "type": "formPage",
  "attributes": {
    "label": "Billing",
    "visibilityRule": { "field": "$prefetch.subscription.plan", "operator": "equals", "value": "paid" }
  },
  "children": ["card_number", "billing_zip"]
}
```

## Headers and inline text

Layout-only entities — no value, skipped from `response`.

- `sectionHeader` — H2-ish header within a page. `attributes.label`, optional `description`.
- `alertBanner` — callout box. `attributes.label`, `description`, and a variant via `displayStyle` (`info | success | warning | error`).
- `richTextField` — renders rendered HTML/markdown supplied via attributes. Use for static body copy.
- `imageDisplay` — renders one or two inline images. `attributes.images` is an array of `{ url, caption?, alt? }`.

```json
{
  "section-1": {
    "type": "sectionHeader",
    "attributes": { "label": "Your details" },
    "parentId": "page-1"
  },
  "warn": {
    "type": "alertBanner",
    "attributes": {
      "label": "Heads up",
      "description": "This form takes about 5 minutes.",
      "displayStyle": "info"
    },
    "parentId": "page-1"
  }
}
```

## Attribute reference

What each attribute does. The catalog at the bottom shows which entities accept which.

### Identity & display

- `label` (string, optional) — visible label above the input. Empty string when omitted. Supports `{{tokens}}`.
- `questionShortLabel` (string, optional) — internal short name for the question (e.g. `"Back Panel"` vs the user-facing `"What information should be on the BACK of the design?"`). Never rendered to the user. Surfaced on each `response` entry in the submission payload as `questionShortLabel` so automations can key off a stable identifier even if the visible `label` changes.
- `description` (string, optional) — hint text under the input. Supports `{{tokens}}`.
- `placeholder` (string, optional) — placeholder text inside the input.
- `labelTooltip` (object, optional) — info icon next to the label. Either `{ content }` or `{ body: [{ heading?, text }] }`, plus optional `title`, `icon` (lucide name), `footnote`. Renders an educational popover.
- `imageUrl` (string, optional) — image source for `imageDisplay` legacy use.
- `images` (object[], required on `imageDisplay`) — array of 1–2 `{ url, caption?, alt? }`. `url` must be a valid URL.

### Validation

- `required` (boolean, default `false`) — when true, the field must have a value to submit the page.
- `minLength`, `maxLength` (number, optional) — character bounds on string fields.
- `min`, `max` (number, optional) — value bounds on `numberField` / `currencyField` / `sliderField`.
- `pattern` (string, optional) — regex source applied via `new RegExp(pattern)`. Use double-escaping in JSON: `"^\\d{5}$"`.
- `step` (number, optional) — increment for `numberField` / `sliderField`.
- `preventPastDates` (boolean, default `false`) — shorthand for `minDate: "today"`; blocks past dates on `dateField` / `dateTimeField` / `dateRangeField` (disabled in the picker and rejected for typed input). An explicit `minDate` or `minDateField` takes precedence.
- `minDate`, `maxDate` (string, optional) — `"YYYY-MM-DD"` or the relative sentinel `"today"` / `"today+N"` / `"today-N"`, evaluated at render time. Bounds are enforced in the calendar **and** for natural-language typed input (out-of-range typed values show an inline error). Used by `dateField`, `dateTimeField`, and `dateRangeField`. To disable past dates, set `minDate: "today"` (or `preventPastDates: true`).
- `minTime`, `maxTime` (string, optional) — `"HH:MM"` bounds for time pickers.
- `stepMinutes` (number, default `15`) — minute interval for time pickers.
- `minMinutes`, `maxMinutes`, `minSeconds`, `maxSeconds` (number, optional) — bounds on `durationField`.

### Choices

- `options` (object[], default `[]`) — `{ label, value }` (plus optional `description`) for `selectField`, `radioGroupField`, `multiSelectField`, `checkboxGroupField`, and `repeatableGroup` (when `mode = "selectable"`). On `multiSelectField` and `radioGroupField`, an option may also set `requiresDetail: true` with optional `detailLabel` / `detailPlaceholder`: selecting that option reveals an inline detail textarea — required: if it's left empty, Next/Submit is blocked and the option card + the textarea show their error state until it's filled. It only renders on the card display, at `columns` 1 or 2. When any of the field's options carry `requiresDetail`, the field value is persisted as `{ value, detail? }[]` instead of the field's normal shape (`string[]` for multi-select, `string` for radio) — for `radioGroupField` it's a single-element array since the field is single-select. Reads are back-compatible (a bare `string` / `{ value }` means no detail), so existing submissions still load, and `"contains"`/`"equals"` visibility rules keep matching the option value. Set it per-option in the schema JSON, e.g. `{ "label": "Recap / Story Reel", "value": "recap_story_reel", "requiresDetail": true, "detailLabel": "What story or moment do you want this reel to capture?" }`. Prefer this over a separate visibility-gated textarea when the follow-up is a single free-text question tied to one choice.
- `radioIconOptions` (object[], default `[]`) — `{ label, value, secondaryLabel?, tag?, icon? }` for `radioIconField`. `icon` is a lucide name.
- `imageOptions` (object[], default `[]`) — `{ label, value, imageUrl }` for `imagePickerField`.
- `imageCarouselOptions` (object[], default `[]`) — `{ label, value, description?, imageUrls[] }` for `imageCarouselPickerField`.
- `panels` (object[], required) — `{ key, label, side }` panel definitions for `panelContentField`. `side` is `"outside" | "inside" | "front" | "back"`.
- `fields` (object[], required) — sub-field config for `repeatableGroup`. Each: `{ key, type, label, description?, placeholder?, required?, options?, min?, max? }`. `type` is one of `text | textarea | email | phone | url | number | select | multiSelect | radioGroup | checkboxGroup | checkbox | toggle | date | dateRange | time`.
- `otherOption` (object, optional) — `{ value, label, description?, placeholder? }` on `radioGroupField` and `checkboxGroupField`. Appends an "Other" entry that reveals an inline text input when selected. Do **not** also include the same value in `options`. The free text is sentinel-encoded into the field's value as `"__other:<typed text>"` (a bare `value` means Other is selected but the input is empty, which fails validation). See ["Other" write-ins](#other-write-ins-two-variants).

### "Other" write-ins: two variants

There are two interchangeable ways to collect a free-text "Other" answer on a radio group; pick one per question.

**Variant A — inline `otherOption` (preferred).** The radio group renders as a vertical list; choosing "Other" reveals a text input directly underneath. One field, one stored value (`"__other:<text>"`). Review pages display it as `Other: <text>`.

```json
"purpose": {
  "type": "radioGroupField",
  "attributes": {
    "label": "What's the main purpose?",
    "required": true,
    "options": [
      { "label": "Church Promotion", "value": "church_promotion" },
      { "label": "Giveaway", "value": "giveaway" }
    ],
    "otherOption": { "value": "other", "label": "Other", "placeholder": "Tell us more..." }
  }
}
```

**Variant B — separate field gated by `visibilityRule`.** Keep `"Other"` as a regular entry in `options` (no `otherOption` attribute — without it, descriptionless options render as pills) and add a sibling `textField` that only appears when it's selected. Two fields, two stored values.

```json
"purpose": {
  "type": "radioGroupField",
  "attributes": {
    "label": "What's the main purpose?",
    "required": true,
    "options": [
      { "label": "Church Promotion", "value": "church_promotion" },
      { "label": "Giveaway", "value": "giveaway" },
      { "label": "Other", "value": "other" }
    ]
  }
},
"purpose-other": {
  "type": "textField",
  "attributes": {
    "label": "Please specify",
    "required": true,
    "placeholder": "Tell us more...",
    "visibilityRule": { "field": "purpose", "operator": "equals", "value": "other" }
  }
}
```

To switch from B to A: remove the `"Other"` entry from `options`, add the `otherOption` attribute, delete the gated text field, and remove its id from the page's `children`. To switch from A to B, do the reverse. `checkboxGroupField` supports the same `otherOption` shape (multi-select; the encoded `__other:<text>` entry lives alongside the other selected values in the array).

### Conditional rendering

- `visibilityRule` (rule, optional) — see "Visibility rule shape" below.

### Data fetching

- `recordSource` (string, optional) — endpoint identifier for `recordPickerField` (resolved server-side). The only built-in value is `"fileSizes"` (uses the form's prefetched account-scoped file sizes).
- `optionsPrefetch` (object, optional) — per-field declarative GET that populates a `recordPickerField`'s options from any HTTP endpoint. Shape: `{ url, pathParams?, queryParams?, headers?, path?, forwardPrefix?, labelKey, valueKey }`. Uses the same engine as the top-level `schema.prefetch` (token interpolation, `pf_*` URL-param auto-forwarding, path narrowing), then maps each row of the narrowed array to `{ label: row[labelKey], value: row[valueKey] }`. Endpoint-agnostic — no SDK changes needed to point at a new API. Example: `{"url":"/api/squad-api/v1/prf/project-submissions","path":"submissions","labelKey":"title","valueKey":"giid"}` — loaded with `?pf_account=12345&pf_project_types=60,64`, the call routes through the form app's server-side proxy at `/api/squad-api/[...path]` (which injects `Authorization: Bearer $SQUAD_API_KEY` and forwards to `https://api.thesqd.com/v1/prf/project-submissions?account=12345&project_types=60,64`), and each row in the response's `submissions` array becomes a picker option. Use the same relative-path pattern for any other Squad API endpoint — no SDK code changes needed.
- `formId` (string, required on `subformField`) — slug of the child form whose submissions are listed inline.
- `minSubmissions`, `maxSubmissions` (number, optional) — bounds on the number of subform submissions required.
- `minRecords`, `maxRecords` (number, optional) — bounds on `recordPickerField` selections.

### Files

- `acceptedFileTypes` (string[], optional) — MIME type allowlist (`["image/*", "application/pdf"]`).
- `maxFiles` (number, optional) — count cap.
- `maxFileSize` (string, optional) — per-file size like `"5MB"`.
- `maxTotalFileSize` (string, optional) — combined cap like `"20MB"`.
- `restrictionText` (string, optional) — overrides the auto-derived dropzone sub-line.
- `showNetworkStatus` (boolean, optional, default `false`) — show a live network-quality + estimated-time-left readout under the uploader while files are in flight (e.g. `Network good · ~1m 20s left`). Offline/stall states read as `Offline — will resume` / `Reconnecting…`.
- `progressDecimals` (number 0–3, optional, default `0`) — decimal places for the percent shown inside each FilePond upload card (`0` → `Uploading 42%`, `1` → `Uploading 42.5%`).
- `foldType` (enum, required on `panelContentField`) — `"bi_fold" | "tri_fold" | "no_fold"`.

Reliability (automatic, no attributes): Dropbox uploads run in resumable 4 MB sessions — a reload/crash can resume the same file from its last committed offset (re-add the file), with exponential-backoff retry, offline-wait, and final size verification. Files upload at most two at a time, smallest-first, so a large multi-file drop doesn't saturate the connection.

### Numbers & currency

- `decimalPlaces` (number 0–10, optional) — fraction digits.
- `useGrouping` (boolean, default `false`) — thousand-separators.
- `prefix`, `suffix` (string, optional) — input adornments (`"$"`, `" kg"`).
- `outputFormat` (string, optional) — formatter mode passed to the field (`"iso"`, `"local"`, etc.).

### Repeatable / instance counts

- `minInstances` (int ≥ 0, default `1`) — minimum rows for `repeatableGroup`.
- `maxInstances` (int ≥ 1, default `10`) — maximum rows.
- `mode` (`"standard" | "selectable"`, optional) — selectable mode requires `options` and adds a per-row `_selection` enum that must be unique across rows.
- `titleField` (string, optional) — key of the field whose value is used as each row's collapsed-summary title.
- `itemLabel` (string, optional) — singular noun used as each row's title prefix in a `repeatableGroup` (rendered as `<itemLabel> #1`, `<itemLabel> #2`, …). Falls back to the group's `label` when omitted. Overridden per-instance by `titleField` when that field has a value.
- `instancesFromField` (string, optional) — entity ID of a number field; the row count grows (never shrinks) to match that field's value.

### AI assist

- `aiGenerate` (object, optional) — `{ prompt, model?, contextFields? }` for fields that expose an "AI fill" button. `contextFields` is an array of entity IDs whose values are spliced into the prompt.

### Input UX

- `enableDictation` (boolean, optional) — show a mic button for speech-to-text.
- `enableSpellCheck` (boolean, optional) — enable native browser spellcheck.
- `disabled` (boolean, default `false`) — read-only mode.
- `displayStyle` (`"list" | "badges"` for checkbox/radio groups; `"info" | "success" | "warning" | "error"` for `alertBanner`).
- `hideHeader` (boolean, default `false`) — hide a `formPage`'s title block.

### Account-scoped

- `account` (number, optional) — explicit account / member number used to scope upstream lookups (file-size picker). Resolution order: form context (giid → account) → this attribute → `?account=` URL param.

### Color

- `color` (string, optional) — hex string for `colorPickerField` initial color hint.

### Miscellaneous

- `minSubmissions`, `maxSubmissions` — see "Data fetching" above.
- `images` — see "Identity & display" above.

## Visibility rule shape

The one mechanism for conditional rendering — used by fields and pages alike. Full TS in `src/lib/forms/types.ts`:

```ts
type VisibilityCondition = {
  field: string;        // entity ID of the field being read; dotted-path
                        // suffix walks into object-valued fields (e.g.
                        // "field-plan-dates.start" for dateRange).
  operator:
    | "equals" | "not_equals"
    | "contains" | "not_contains"
    | "not_empty" | "empty"
    | "gte" | "gt" | "lte" | "lt"
    | "within_next_days" | "not_within_next_days";
  value?: string | number | boolean;
};

type VisibilityGroup = {
  combinator: "AND" | "OR";
  conditions: VisibilityRule[]; // nested groups allowed
};

type VisibilityRule = VisibilityCondition | VisibilityGroup;
```

Operator semantics (from `src/lib/forms/should-be-processed.ts`):

- `equals` / `not_equals` — strict triple-equals.
- `contains` / `not_contains` — array `.includes(value)` for array fields; substring for string fields. `contains` is false (and `not_contains` true) for other value types, including empty/unset fields.
- `not_empty` / `empty` — arrays use `.length`; strings use `=== ""`; null/undefined count as empty.
- `gte` / `gt` / `lte` / `lt` — coerces strings to numbers; arrays compared by `.length`.
- `within_next_days` / `not_within_next_days` — LHS is an ISO date string, RHS is an integer N. True when today ≤ date ≤ today + N days. Pair with a dotted-path `field` (e.g. `field-plan-dates.start`) to read into a dateRange.

Group combinators short-circuit (`AND` ⇒ every, `OR` ⇒ some). Nested groups are evaluated recursively.

### Examples

Show `phone` only when `callback === "Yes"`:

```json
"visibilityRule": {
  "field": "callback",
  "operator": "equals",
  "value": "Yes"
}
```

Show "expedited" warning when shipping in less than 3 days:

```json
"visibilityRule": {
  "field": "days_until_event",
  "operator": "lt",
  "value": 3
}
```

Show advanced section when callback is yes AND consent is checked:

```json
"visibilityRule": {
  "combinator": "AND",
  "conditions": [
    { "field": "callback", "operator": "equals", "value": "Yes" },
    { "field": "consent", "operator": "equals", "value": true }
  ]
}
```

When a rule references a field that does not exist, the field value is `undefined` and most operators return false — meaning the dependent field stays hidden. `not_empty` / `empty` handle undefined correctly.

**Prefetch references.** `field` also accepts a dotted path into the prefetch response, prefixed with `$prefetch.` (or `prefetch.`). The path is resolved against the raw, path-narrowed JSON body returned by `schema.prefetch` (NOT the flattened template vars), so any nesting works. Example:

```json
"visibilityRule": {
  "combinator": "AND",
  "conditions": [
    { "field": "$prefetch.subscription.active", "operator": "equals", "value": true },
    { "field": "callback", "operator": "equals", "value": "Yes" }
  ]
}
```

If the prefetch hasn't resolved yet (or failed), `$prefetch.*` references read as `undefined` and behave the same as a missing field. Page-level and field-level rules share this resolver.

## URL params: prefill, template variables, prefetch keys

Every query param on `/f/{id}?key=value` is classified at submission time — you don't declare anything:

- **`prefill`** — key matches a field ID → seeds that field's initial value. Unmatched keys are silently ignored.
- **`variable`** — key is referenced as `{{key}}` in any string attribute. Token regex: `\{\{\s*[\w.-]+\s*\}\}` (letters, digits, `_`, `.`, `-`). Missing tokens collapse one adjacent space so copy stays clean.
- **`prefetch` (reserved)** — `embedded`, `submissionId`, `giid`, `t`, `account` drive runtime behavior (resume, theme, embed mode).
- **`pf_*`** — auto-forwarded to `schema.prefetch` as query params with the prefix stripped (`?pf_giid=123` → `?giid=123` on the upstream call). The raw key stays in templateVars too (`{{pf_giid}}`) for use in `prefetch.headers`/`pathParams`/etc. An explicit `prefetch.queryParams` disables auto-forwarding; `prefetch.forwardPrefix` changes or disables the prefix.
- **`user_id`** — `cuid` is written to `submitted_by` on the submission row.
- **`other`** — everything else; recorded but unused.

Field IDs must be URL-safe (letters, digits, `-`, `_`) to be prefillable.

## Prefilling fields — two mechanisms

You can seed initial values into fields two ways. They compose, and they answer different questions.

### A. URL param → field value (direct)

If a URL param's key matches a field ID, the param value becomes that field's initial value. Zero schema config.

```
https://form.thesqd.com/f/X?first-name=Jacob&church-name=Paradox%20Church
```

prefills the fields with IDs `first-name` and `church-name`. Unmatched keys are silently ignored. Saved progress and user edits override the seeded value.

**Use when:** the caller already has the value in hand (e.g. a PRF4 deep-link or a CRM redirect that knows the answers).

### B. `schema.prefetch` + `{{giid}}` → fetch a record → consume it

The form-level `schema.prefetch` runs a GET on load (see [Prefetching](#prefetching-external-data-into-template-vars)) and exposes the response to the schema two ways. With the proxy route (`/api/squad-api/[...path]`) you can target any Squad API endpoint with secrets staying server-side. Real response shape for `/v1/prf/general-submissions/{giid}`:

```json
{
  "account": {
    "account": 3005,
    "church_name": "Paradox Church",
    "timezone": "Pacific",
    "primary_email": ["craig@paradoxredlands.com"]
  },
  "raw_data": {
    "user": { "email": "craig@paradoxredlands.com", "username": "Henry Craig Hadley" },
    "generalInfo": {
      "projectTitle": "Paradox Tithe Envelopes",
      "description": "Tithe Envelopes for our congregation using our branding."
    }
  },
  "airtable_record": { "member_number": "3005" }
}
```

Schema:

```json
"prefetch": {
  "url": "/api/squad-api/v1/prf/general-submissions/{{giid}}",
  "mapping": {
    "church-name": "account.church_name",
    "account-number": "account.account",
    "submitter-email": "raw_data.user.email",
    "project-title": "raw_data.generalInfo.projectTitle"
  }
}
```

Loaded with `?giid=268fc7c3-cf03-4678-b998-b6effee64285`, this:

**(1) Seeds field values via `mapping`** — each `[fieldId, dottedPath]` walks the response and sets that field's initial value. The dotted path is relative to the (optionally narrowed) response; with no `path` it's the full root.

**(2) Exposes template vars** — the response is flattened into `{{prefetch.<path>}}` tokens usable in any string attribute:

```json
{ "type": "sectionHeader", "attributes": { "label": "Welcome back, {{prefetch.raw_data.user.username}}" } }
{ "type": "alertBanner",  "attributes": { "description": "Submitting for {{prefetch.account.church_name}} (#{{prefetch.account.account}})" } }
```

**Use when:** the caller only has a record ID and you need to look up the rest server-side, or when the same value needs to appear in multiple labels / alerts / defaults without re-typing.

### Resolution order

When the same field could be seeded multiple ways, the winner is:

**saved progress > URL param prefill > `mapping` > `defaultValues` > entity `defaultValue()`**

`{{prefetch.x}}` tokens in labels / alerts / `defaultValues` only resolve after the prefetch returns; if `giid` isn't in the URL the prefetch [skips](#prefetching-external-data-into-template-vars) and those tokens render as empty strings.

## Theme via URL

`?t=d` for dark, `?t=l` for light. Default light. Bypasses system preference. `t` is a reserved key so it doesn't leak into template vars.

## Prefetching external data into template vars

`schema.prefetch` runs a GET on form load and feeds three consumers from one response: template-var interpolation (`{{prefetch.x.y}}`), field prefill (`mapping`), and visibility rules (`$prefetch.x.y`). Fully config-driven.

```ts
interface PrefetchConfig {
  url: string;                              // {{token}} + :name / {name} path placeholders
  pathParams?: Record<string, string>;      // values support {{token}}; substituted into url
  queryParams?: Record<string, string>;     // values support {{token}}; when defined, disables pf_* auto-forward
  headers?: Record<string, string>;         // values support {{token}}; e.g. Authorization
  path?: string;                            // dotted path narrowing the JSON response
  mapping?: Record<string, string>;         // fieldId → dotted path; seeds field defaultValues
  forwardPrefix?: string;                   // default "pf_"; "" disables auto-forwarding
}
```

**Resolution order**

1. `url`, `pathParams`, `queryParams`, `headers` are each interpolated against URL params (`{{pf_giid}}`, `{{giid}}`, etc).
2. `pathParams` values are substituted into `:name` and `{name}` placeholders in the URL (URL-encoded).
3. If `queryParams` is undefined, every URL param whose key starts with `forwardPrefix` (default `pf_`) is appended to the URL as a query param with the prefix stripped. If `queryParams` is defined (even as `{}`), it wins — no auto-forwarding.
4. `headers` are sent on the GET. `Accept: application/json` is added automatically.
5. The JSON response is narrowed by `path` (optional).

**How the response is exposed**

- **Template vars** — the narrowed response is flattened into dotted keys under the `prefetch.` namespace. `{ user: { name: "x" } }` becomes `{ "prefetch.user.name": "x" }`, accessible as `{{prefetch.user.name}}` in any string attribute, including option labels and `defaultValues`.
- **Field prefill (`mapping`)** — each `[fieldId, dottedPath]` resolves a value out of the narrowed response and seeds that field's initial value. Schema-level `defaultValues` and saved progress both take precedence over `mapping`.
- **Visibility rules** — visibility-rule `field` strings starting with `$prefetch.` or `prefetch.` are resolved against the raw narrowed response (not the flat vars), so deep nesting and non-string values (e.g. booleans) work.

**Logging.** Each call is logged into the saved submission's `prefetch` array (`{ url, status, ok, durationMs, error? }`). The `url` field reflects the fully-resolved URL after interpolation, path substitution, and query-string assembly.

**Skip on missing tokens.** A prefetch only fires when every `{{token}}` reference in its config (`url`, `pathParams`, `queryParams`, `headers`) can be resolved from the URL params. If any are missing or empty, the call is skipped entirely — no HTTP request — and recorded in the log as `{ ok: false, status: 0, error: "Skipped: token \"<name>\" missing from URL params" }`. This prevents URLs like `/api/general-submission/{{giid}}` from collapsing to `/api/general-submission/` and 404-ing when the param isn't provided. Prefetches that rely only on `pf_*` auto-forwarding (no explicit `{{token}}` references) fire unconditionally, since auto-forward simply contributes zero query params when no `pf_*` keys exist.

**Server-side parity.** The same prefetch runs server-side during `generateMetadata()` so OG unfurls reflect resolved values. Relative URLs are resolved against the request host; an `x-api-key` header is added from `SQD_MODULE_KEY` for same-origin gating. Server-side does NOT apply `mapping` (no field state at metadata time).

**Example — `pf_*` auto-forwarding + headers + mapping + nested visibility**

URL: `https://form.thesqd.com/f/X?pf_giid=ABC123&pf_token=eyJhbG…&pf_project=xyz`

```json
"prefetch": {
  "url": "https://api.example.com/v1/accounts/:giid",
  "pathParams": { "giid": "{{pf_giid}}" },
  "headers":    { "Authorization": "Bearer {{pf_token}}" },
  "path": "data",
  "mapping": {
    "first_name": "user.firstName",
    "church_name": "church.name"
  }
}
```

This performs `GET https://api.example.com/v1/accounts/ABC123?project=xyz` with `Authorization: Bearer eyJhbG…`. The response's `data.user.firstName` seeds the `first_name` field. UI strings can reference `{{prefetch.church.name}}`, and visibility rules can reference `$prefetch.subscription.plan`.

## Embedding

`/f/:formId` sets `Content-Security-Policy: frame-ancestors *;` — any origin can iframe it:

```html
<iframe
  src="https://form.thesqd.com/f/VpFFPutB?giid=ABC"
  style="width:100%;height:800px;border:0;"
></iframe>
```

It sends `window.postMessage` events to the parent on lifecycle changes. All messages share this shape (`src/lib/forms/post-message.ts`):

```ts
type FormPostMessageEvent =
  | "form:loaded"        // schema parsed, first render
  | "form:started"       // user touched the first field
  | "form:field-change"  // any field value mutation
  | "form:page-complete" // user clicked Next on a page
  | "form:page-back"     // user clicked Back
  | "form:submitted";    // final submit succeeded

interface FormPostMessage {
  event: FormPostMessageEvent;
  formId: string;
  submissionId?: string;
  status: "idle" | "in_progress" | "completed";
  lastViewedPage?: number;
  response?: Array<{ id; label; type; value }>;
  url_params?: Array<{ id; value; type? }>;
  prefetch?: Array<{ url; status; ok; durationMs; error? }>;
}
```

Parents must validate `event.origin` themselves (`targetOrigin` is `"*"`); filter by `event.startsWith("form:")`. The payload mirrors what gets persisted, so listeners see the same shape as the DB. One-way only — no parent-to-child messaging.

## Submissions

Stored in `forms.form_submissions`. Key columns: `id uuid`, `form_id`, `data jsonb` (payload — see below), `submitted_by` (from `?cuid=`), `status` (`in_progress | completed | abandoned`), `created_at`, `updated_at`.

A row is created `in_progress` on first interaction and updated on every change/page transition. Hourly cron (`abandon-stale-submissions`) flips `in_progress` rows older than 12h to `abandoned`. Returning users with a matching localStorage entry get a resume modal.

## Submission shape

The `data` JSONB on each row (from `src/lib/forms/submission-shape.ts`):

```ts
interface SubmissionData {
  formId: string;
  submissionId: string;
  status: "in_progress" | "completed" | "abandoned";
  lastViewedPage?: number;
  response: Array<{
    id: string;     // entity ID
    label: string;  // entity label at time of submit
    questionShortLabel?: string; // optional internal short label, if authored
    type: string;   // simplified field type (e.g. "text", "select")
    value: unknown; // entity value (shape depends on field type)
  }>;
  url_params: Array<{
    id: string;
    value: string;
    type?: "prefill" | "variable" | "prefetch" | "user_id" | "other";
  }>;
  prefetch?: Array<{
    url: string;
    status: number;
    ok: boolean;
    durationMs: number;
    error?: string;
  }>;
}
```

Layout-only entities (`sectionHeader`, `alertBanner`, `imageDisplay`, `formPage`) are excluded from `response`. Hidden fields (visibility rule false) are also excluded. Order follows the page tree.

### Example payload

```json
{
  "formId": "VpFFPutB",
  "submissionId": "8c2a…",
  "status": "completed",
  "lastViewedPage": 1,
  "response": [
    { "id": "name", "label": "Your name", "type": "text", "value": "Ada Lovelace" },
    { "id": "callback", "label": "Need a callback?", "type": "radio", "value": "Yes" },
    { "id": "phone", "label": "Phone number", "type": "phone", "value": "+15555550100" }
  ],
  "url_params": [
    { "id": "giid", "value": "ABC123", "type": "prefetch" },
    { "id": "name", "value": "Ada Lovelace", "type": "variable" }
  ],
  "prefetch": [
    { "url": "https://api.example.com/intake/ABC123", "status": 200, "ok": true, "durationMs": 142 }
  ]
}
```

## In-progress, resume, abandonment

- First interaction → `in_progress` row created + `submissionId` cached to localStorage (key `df:resume:{formId}`).
- Every field change / page transition → row's `data` and `updated_at` are updated.
- Return visit with a matching localStorage entry + still-`in_progress` row → resume modal. "Start fresh" deletes the local key; the DB row stays for analytics.
- Hourly cron flips `in_progress` rows older than 12h to `abandoned`. Abandoned rows are never resurrected.

## Common authoring mistakes

- Forgetting to add the page entity to `root`. The form will 404 / render blank.
- Field IDs that include spaces or special chars. URL prefill will not work; quote them carefully in JSON.
- Visibility rule references a field that is not in the same form. Rules can only see same-form fields; the missing field reads as `undefined` and most operators return false.
- Setting `required: true` on a `sectionHeader`, `alertBanner`, or `imageDisplay`. No-op but misleading — these have no submitted value.
- Using `{{name}}` in a label without supplying `?name=…` and without a `schema.prefetch` source. The token collapses to empty space.
- Naming a `repeatableGroup` field key the same as an outer entity ID. They live in different namespaces, but it makes debugging painful.
- Setting `mode: "selectable"` on a `repeatableGroup` without supplying `options`. Validation throws on submit.
- Re-using an entity ID across forms when iframing them on the same page. Use distinct IDs per embedded child.

## Quick INSERT recipe

```sql
INSERT INTO forms.forms (id, name, description, schema, status, form_title, form_tags)
VALUES (
  'YourSlug',
  'internal-name',
  'Public description',
  $$ { "root": ["page-1"], "entities": { /* … */ } } $$::jsonb,
  'published',
  'Public Form Title',
  ARRAY['tag1']
);
```

To update:

```sql
UPDATE forms.forms
SET schema = $$ … $$::jsonb,
    form_title = 'New title',
    updated_at = now()
WHERE id = 'YourSlug';
```

To unpublish without deleting: `UPDATE forms.forms SET status = 'draft' WHERE id = 'YourSlug';`.

## End-to-end example schemas

### 1. Single-page contact form

Three fields, one page, all required.

```json
{
  "root": ["page-1"],
  "entities": {
    "page-1": {
      "type": "formPage",
      "attributes": { "label": "Contact us" },
      "children": ["name", "email", "message"]
    },
    "name": {
      "type": "textField",
      "attributes": { "label": "Your name", "required": true },
      "parentId": "page-1"
    },
    "email": {
      "type": "emailField",
      "attributes": { "label": "Email", "required": true },
      "parentId": "page-1"
    },
    "message": {
      "type": "textArea",
      "attributes": { "label": "Message", "required": true },
      "parentId": "page-1"
    }
  },
  "successTitle": "Thanks!",
  "successMessage": "We'll be in touch soon."
}
```

### 2. Multi-page form with visibility

Page 1 asks whether the user wants a callback. Page 2 only collects the phone number if they said "Yes".

The `phone` field is gated by a `visibilityRule` that reads the `callback` radio's value. When the user picks "No" the field is hidden and not validated, so submission still succeeds.

```json
{
  "root": ["page-1", "page-2"],
  "entities": {
    "page-1": {
      "type": "formPage",
      "attributes": { "label": "Preferences" },
      "children": ["callback"]
    },
    "callback": {
      "type": "radioGroupField",
      "attributes": {
        "label": "Need a callback?",
        "required": true,
        "options": [
          { "label": "Yes", "value": "Yes" },
          { "label": "No", "value": "No" }
        ]
      },
      "parentId": "page-1"
    },
    "page-2": {
      "type": "formPage",
      "attributes": { "label": "Contact" },
      "children": ["phone"]
    },
    "phone": {
      "type": "phoneField",
      "attributes": {
        "label": "Phone number",
        "required": true,
        "visibilityRule": {
          "field": "callback",
          "operator": "equals",
          "value": "Yes"
        }
      },
      "parentId": "page-2"
    }
  }
}
```

### 3. Prefetched form

The URL `https://form.thesqd.com/f/X?pf_giid=ABC123&pf_token=…` triggers a GET to `https://api.example.com/intake/ABC123` with a bearer token. The narrowed `data` response prefills `email`, interpolates `{{prefetch.name}}` into the title, and conditionally reveals a billing page based on `$prefetch.subscription.plan`.

```json
{
  "root": ["page-1", "page-billing"],
  "prefetch": {
    "url": "https://api.example.com/intake/:giid",
    "pathParams": { "giid": "{{pf_giid}}" },
    "headers":    { "Authorization": "Bearer {{pf_token}}" },
    "path": "data",
    "mapping": { "email": "user.email" }
  },
  "entities": {
    "page-1": {
      "type": "formPage",
      "attributes": { "label": "Welcome, {{prefetch.name}}" },
      "children": ["email"]
    },
    "email": {
      "type": "emailField",
      "attributes": {
        "label": "Confirm your email",
        "description": "We have {{prefetch.user.email}} on file.",
        "required": true
      },
      "parentId": "page-1"
    },
    "page-billing": {
      "type": "formPage",
      "attributes": {
        "label": "Billing",
        "visibilityRule": { "field": "$prefetch.subscription.plan", "operator": "equals", "value": "paid" }
      },
      "children": ["card_zip"]
    },
    "card_zip": {
      "type": "textField",
      "attributes": { "label": "Card ZIP", "required": true },
      "parentId": "page-billing"
    }
  }
}
```

### 4. File upload form

Single page, single `fileUploadField` with restrictions.

```json
{
  "root": ["page-1"],
  "entities": {
    "page-1": {
      "type": "formPage",
      "attributes": { "label": "Upload your files" },
      "children": ["intro", "files"]
    },
    "intro": {
      "type": "alertBanner",
      "attributes": {
        "label": "Upload guidelines",
        "description": "PDFs and images only. Max 5 files, 20MB total.",
        "displayStyle": "info"
      },
      "parentId": "page-1"
    },
    "files": {
      "type": "fileUploadField",
      "attributes": {
        "label": "Reference files",
        "required": true,
        "acceptedFileTypes": ["application/pdf", "image/*"],
        "maxFiles": 5,
        "maxFileSize": "5MB",
        "maxTotalFileSize": "20MB"
      },
      "parentId": "page-1"
    }
  },
  "successTitle": "Got it!",
  "successMessage": "We received your files."
}
```

### 5. Repeatable group form

Collect N team members. Mode `standard` lets the user add/remove rows freely between `minInstances` and `maxInstances`.

```json
{
  "root": ["page-1"],
  "entities": {
    "page-1": {
      "type": "formPage",
      "attributes": { "label": "Team roster" },
      "children": ["members"]
    },
    "members": {
      "type": "repeatableGroup",
      "attributes": {
        "label": "Team members",
        "description": "Add one row per teammate.",
        "required": true,
        "minInstances": 1,
        "maxInstances": 8,
        "mode": "standard",
        "titleField": "name",
        "fields": [
          { "key": "name", "type": "text", "label": "Name", "required": true },
          { "key": "email", "type": "email", "label": "Email", "required": true },
          { "key": "role", "type": "select", "label": "Role", "required": true,
            "options": [
              { "label": "Owner", "value": "owner" },
              { "label": "Editor", "value": "editor" },
              { "label": "Viewer", "value": "viewer" }
            ]
          }
        ]
      },
      "parentId": "page-1"
    }
  }
}
```

To drive the row count from another field instead, set `instancesFromField` to a `numberField` entity ID and the group will grow (never shrink) to that count.

## Field entity catalog

The catalog below is auto-generated from `src/lib/forms/entities/*.ts`. Every entry maps an entity `type` to its accepted `attributes`, lists the squad-ui component it renders with, summarizes how its value is stored, and shows a minimal example. When authoring a schema, set `"type": "<entityName>"` and populate `attributes` using only the keys listed for that entity. Attributes marked Required = no are optional.

### alertBanner

No description.

**squad-ui component:** [`alert`](https://sdk-components.thesqd.com/r/alert.json)

**Stores:** Layout-only; no value stored.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `color` | unknown | yes |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "alertBanner-1",
  "type": "alertBanner",
  "attributes": {
    "label": "Sample label",
    "color": null,
    "rowGroup": null
  }
}
```

### checkboxField

No description.

**squad-ui component:** [`checkbox`](https://sdk-components.thesqd.com/r/checkbox.json)

**Stores:** Stored as a boolean; must be true when required.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "checkboxField-1",
  "type": "checkboxField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### checkboxGroupField

No description.

**squad-ui component:** [`checkbox`](https://sdk-components.thesqd.com/r/checkbox.json)

**Stores:** Stored as a non-empty array of strings.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `options` | object[] | no |  |
| `displayStyle` | unknown | no |  |
| `otherOption` | unknown | yes | Optional "Other" checkbox/radio entry that reveals an inline text input  when selected. The user's free-text response is collected separately  (see the field component for the storage shape). |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "checkboxGroupField-1",
  "type": "checkboxGroupField",
  "attributes": {
    "label": "Sample label",
    "otherOption": null,
    "rowGroup": null
  }
}
```

### colorPickerField

No description.

**squad-ui component:** [`input`](https://sdk-components.thesqd.com/r/input.json)

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "colorPickerField-1",
  "type": "colorPickerField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### currencyField

No description.

**squad-ui component:** [`text-field`](https://sdk-components.thesqd.com/r/text-field.json)

**Stores:** Stored as a number; honors min/max from attributes.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `min` | number | no |  |
| `max` | number | no |  |
| `decimalPlaces` | number | no |  |
| `useGrouping` | boolean | no |  |
| `prefix` | string | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "currencyField-1",
  "type": "currencyField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### dateField

No description.

**squad-ui component:** [`date-picker`](https://sdk-components.thesqd.com/r/date-picker.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minDate` | string | no | Earliest selectable date (inclusive). Days before it are disabled in the calendar and rejected for typed natural-language input. Accepts an absolute `"YYYY-MM-DD"` or a relative sentinel — `"today"`, `"today+N"`, `"today-N"` — evaluated at render time. Set `minDate: "today"` to block past dates (equivalent to `preventPastDates: true`). |
| `maxDate` | string | no | Latest selectable date (inclusive). Days after it are disabled in the calendar and rejected for typed natural-language input. Accepts an absolute `"YYYY-MM-DD"` or a relative sentinel — `"today"`, `"today+N"`, `"today-N"` — evaluated at render time (e.g. `maxDate: "today+30"` allows up to 30 days out). |
| `minDateField` | string | no |  |
| `maxDateField` | string | no |  |
| `preventPastDates` | boolean | no | When true, clamps the picker's lower bound to today so past dates can't be selected (or typed). Defaults to false. Shorthand for `minDate: "today"`; an explicit `minDate` takes precedence if both are set. |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "dateField-1",
  "type": "dateField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### dateRangeField

No description.

**squad-ui component:** [`date-picker`](https://sdk-components.thesqd.com/r/date-picker.json)

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minDate` | string | no | Earliest selectable date (inclusive). Days before it are disabled in the calendar and rejected for typed natural-language input. Accepts an absolute `"YYYY-MM-DD"` or a relative sentinel — `"today"`, `"today+N"`, `"today-N"` — evaluated at render time. Set `minDate: "today"` to block past dates (equivalent to `preventPastDates: true`). |
| `maxDate` | string | no | Latest selectable date (inclusive). Days after it are disabled in the calendar and rejected for typed natural-language input. Accepts an absolute `"YYYY-MM-DD"` or a relative sentinel — `"today"`, `"today+N"`, `"today-N"` — evaluated at render time (e.g. `maxDate: "today+30"` allows up to 30 days out). |
| `preventPastDates` | boolean | no | When true, clamps the picker's lower bound to today so past dates can't be selected (or typed). Defaults to false. Shorthand for `minDate: "today"`; an explicit `minDate` takes precedence if both are set. |
| `variant` | enum: field|inline | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "dateRangeField-1",
  "type": "dateRangeField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### dateTimeField

No description.

**squad-ui component:** [`date-picker`](https://sdk-components.thesqd.com/r/date-picker.json)

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minDate` | string | no | Earliest selectable date (inclusive). Days before it are disabled in the calendar and rejected for typed natural-language input. Accepts an absolute `"YYYY-MM-DD"` or a relative sentinel — `"today"`, `"today+N"`, `"today-N"` — evaluated at render time. Set `minDate: "today"` to block past dates (equivalent to `preventPastDates: true`). |
| `maxDate` | string | no | Latest selectable date (inclusive). Days after it are disabled in the calendar and rejected for typed natural-language input. Accepts an absolute `"YYYY-MM-DD"` or a relative sentinel — `"today"`, `"today+N"`, `"today-N"` — evaluated at render time (e.g. `maxDate: "today+30"` allows up to 30 days out). |
| `minDateField` | string | no |  |
| `maxDateField` | string | no |  |
| `preventPastDates` | boolean | no | When true, clamps the picker's lower bound to today so past dates can't be selected (or typed). Defaults to false. Shorthand for `minDate: "today"`; an explicit `minDate` takes precedence if both are set. |
| `quickSelections` | mixed | no | Nth upcoming occurrence of `weekday`, strictly after today (1-based). |
| `stepMinutes` | number | no |  |
| `minTime` | string | no |  |
| `maxTime` | string | no |  |
| `outputFormat` | string | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "dateTimeField-1",
  "type": "dateTimeField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### disclaimerField

No description.

**squad-ui component:** [`checkbox`](https://sdk-components.thesqd.com/r/checkbox.json)

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "disclaimerField-1",
  "type": "disclaimerField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### durationField

No description.

**squad-ui component:** [`duration-input`](https://sdk-components.thesqd.com/r/duration-input.json)

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minMinutes` | number | no |  |
| `maxMinutes` | number | no |  |
| `minSeconds` | number | no |  |
| `maxSeconds` | number | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "durationField-1",
  "type": "durationField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### emailField

No description.

**squad-ui component:** [`text-field`](https://sdk-components.thesqd.com/r/text-field.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "emailField-1",
  "type": "emailField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### fileSizeField

No description.

**squad-ui component:** [`file-size-input`](https://sdk-components.thesqd.com/r/file-size-input.json)

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `allowedUnits` | object[] | no | Restrict the FileSizeInput unit selector to a subset (e.g. inches only). |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "fileSizeField-1",
  "type": "fileSizeField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### fileSizePickerField

Multi-select picker backed by the Squad file-size catalog (api.thesqd.com),
wrapping the squad-ui `file-size-picker-block`. Selections are stored as an
array of `{ id, label }` tuples — the label is captured at selection time so
review pages and submissions render without re-fetching the catalog. Legacy
`string[]` payloads are still accepted on read and normalized to
`[{ id, label: id }]`.

Selection count is bounded by `minRecords` (defaults to 1 in the UI) and an
optional `maxRecords`; once `maxRecords` is reached the picker disables every
unselected card so no further sizes can be added (selected cards stay
removable).

**squad-ui component:** n/a

**Stores:** Stored as an array of `{ id, label }` selection tuples (defaults to []). Legacy `string[]` payloads are accepted on read.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minRecords` | number | no | Minimum number of selections required. Defaults to 1 when omitted. |
| `maxRecords` | number | no | Maximum number of selections allowed. Optional — when reached, the picker disables every unselected option so no more can be added. |
| `account` | unknown | no |  |
| `projectTypeIds` | unknown | no |  |
| `pageSize` | number | no | How many file-size cards to show per page in the file-size picker.  Defaults to 8 (set by the field component) when omitted. |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "fileSizePickerField-1",
  "type": "fileSizePickerField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### fileUploadField

No description.

**squad-ui component:** [`file-upload-beta`](https://sdk-components.thesqd.com/r/file-upload-beta.json)

**Stores:** Stored as an array of uploaded file refs (objects from the upload component).

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `acceptedFileTypes` | unknown | no |  |
| `maxFiles` | unknown | no |  |
| `maxFileSize` | unknown | no |  |
| `maxTotalFileSize` | unknown | no |  |
| `restrictionText` | unknown | no |  |
| `showNetworkStatus` | unknown | no |  |
| `progressDecimals` | unknown | no |  |
| `upload` | unknown | no | S3 object metadata template (string→string, values interpolated).  Ignored for `dropbox` (Dropbox has no per-object metadata here). |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "fileUploadField-1",
  "type": "fileUploadField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### formPage

No description.

**squad-ui component:** n/a

**Stores:** Layout container; not part of the submitted response.

**Children:** accepts a `children` array of entity IDs.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `hideHeader` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `pageNav` | unknown | yes | Override the default "Selected (n)" heading. Supports  `{count}` interpolation. |

```json
{
  "id": "formPage-1",
  "type": "formPage",
  "attributes": {
    "label": "Sample label",
    "pageNav": null
  },
  "children": ["child-id-1"]
}
```

### imageCarouselPickerField

No description.

**squad-ui component:** [`carousel-tile-radio-group`](https://sdk-components.thesqd.com/r/carousel-tile-radio-group.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `imageCarouselOptions` | object[] | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "imageCarouselPickerField-1",
  "type": "imageCarouselPickerField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### imageDisplay

No description.

**squad-ui component:** n/a

**Stores:** Layout-only; no value stored.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `images` | object[] | yes |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "imageDisplay-1",
  "type": "imageDisplay",
  "attributes": {
    "label": "Sample label",
    "images": [{"url":"https://example.com/a.png"}],
    "rowGroup": null
  }
}
```

### imagePickerField

No description.

**squad-ui component:** [`image-picker`](https://sdk-components.thesqd.com/r/image-picker.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `imageOptions` | object[] | no |  |
| `columns` | unknown | yes |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "imagePickerField-1",
  "type": "imagePickerField",
  "attributes": {
    "label": "Sample label",
    "columns": null,
    "rowGroup": null
  }
}
```

### multiSelectField

No description.

**squad-ui component:** [`checkbox`](https://sdk-components.thesqd.com/r/checkbox.json)

**Stores:** Stored as a structured object (see entity definition).

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `options` | object[] | no |  |
| `optionsPrefetch` | unknown | yes | Agnostic projection: each entry maps a *result option field* to a  *dotted source path* into the row. Schema authors decide which  fields to project. `label` + `value` required at runtime. |
| `displayStyle` | unknown | no |  |
| `columns` | unknown | yes |  |
| `sections` | unknown | yes | Lucide-react icon name (e.g. "Palette", "Video"). Resolved  client-side via `lucide-react`'s `icons` lookup. Missing icons  render the header without an icon. |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "multiSelectField-1",
  "type": "multiSelectField",
  "attributes": {
    "label": "Sample label",
    "optionsPrefetch": null,
    "columns": null,
    "sections": null,
    "rowGroup": null
  }
}
```

### numberField

No description.

**squad-ui component:** [`number-input`](https://sdk-components.thesqd.com/r/number-input.json)

**Stores:** Stored as a number; honors min/max from attributes.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `min` | number | no |  |
| `max` | number | no |  |
| `decimalPlaces` | number | no |  |
| `useGrouping` | boolean | no |  |
| `prefix` | string | no |  |
| `suffix` | string | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "numberField-1",
  "type": "numberField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### pageSearchFilter

Control widget — renders the PageSearchFilter block and writes its
 state into a shared filter context keyed by `controlsField`. The
 targeted field reads that state to filter its options. No value is
 written to the submission.

**squad-ui component:** n/a

**Stores:** Layout-only; no value stored.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `controlsField` | string | yes | Entity id of the field this control should filter. Used by  `pageSearchFilter` to point at the multi-select / record-picker whose  options it gates. |
| `tabs` | unknown | yes | Path on each row to the array of underlying values the  bundle stands for (e.g. `project_ids`). Selecting the  card adds these to the field value; deselecting removes  them. |
| `dividerAfter` | string | no | Tab `key` after which to insert a hairline divider in the pill row. |
| `searchPlaceholder` | string | no | Placeholder for the search input. |
| `searchFields` | string[] | no | Option-row keys to scan for free-text matches. Defaults applied at the  consumer (typically `["label", "description"]`). |
| `defaultActiveTab` | string | no | Optional active tab key on first render. Otherwise the first declared  tab is used. |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "pageSearchFilter-1",
  "type": "pageSearchFilter",
  "attributes": {
    "controlsField": "value",
    "tabs": null,
    "rowGroup": null
  }
}
```

### panelContentField

No description.

**squad-ui component:** [`panel-editor`](https://sdk-components.thesqd.com/r/panel-editor.json)

**Stores:** Stored as a structured object (see entity definition).

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `panels` | object[] | yes |  |
| `foldType` | enum: bi_fold|tri_fold|no_fold | yes |  |
| `enableDictation` | unknown | yes |  |
| `editor` | enum: textarea|dictation|rich-text | yes | Which per-panel editor a panelContentField renders: a plain textarea, a  dictation-enabled textarea, or the full rich-text toolbar. Optional — the  field falls back to dictation/textarea based on `enableDictation`. |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "panelContentField-1",
  "type": "panelContentField",
  "attributes": {
    "label": "Sample label",
    "panels": [{"key":"p1","label":"Front","side":"front"}],
    "foldType": "bi_fold",
    "enableDictation": null,
    "editor": "textarea",
    "rowGroup": null
  },
  "parentId": "page-1"
}
```

### passwordField

No description.

**squad-ui component:** [`text-field`](https://sdk-components.thesqd.com/r/text-field.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minLength` | number | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "passwordField-1",
  "type": "passwordField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### phoneField

No description.

**squad-ui component:** [`text-field`](https://sdk-components.thesqd.com/r/text-field.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "phoneField-1",
  "type": "phoneField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### radioGroupField

No description.

**squad-ui component:** [`radio-group`](https://sdk-components.thesqd.com/r/radio-group.json)

**Stores:** Stored as a structured object (see entity definition).

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `options` | object[] | no |  |
| `columns` | unknown | yes |  |
| `otherOption` | unknown | yes | Optional "Other" checkbox/radio entry that reveals an inline text input  when selected. The user's free-text response is collected separately  (see the field component for the storage shape). |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "radioGroupField-1",
  "type": "radioGroupField",
  "attributes": {
    "label": "Sample label",
    "columns": null,
    "otherOption": null,
    "rowGroup": null
  }
}
```

### radioIconField

No description.

**squad-ui component:** [`radio-group`](https://sdk-components.thesqd.com/r/radio-group.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `radioIconOptions` | object[] | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "radioIconField-1",
  "type": "radioIconField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### recordPickerField

No description.

**squad-ui component:** [`record-picker-block`](https://sdk-components.thesqd.com/r/record-picker-block.json)

**Stores:** Stored as a non-empty array of strings.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `options` | object[] | no |  |
| `minRecords` | number | no | Minimum number of selections required. Defaults to 1 when omitted. |
| `maxRecords` | number | no | Maximum number of selections allowed. Optional — when reached, the picker disables every unselected option so no more can be added. |
| `recordSource` | string | no |  |
| `allowAdd` | boolean | no |  |
| `autoSelectSingle` | boolean | no | When the field's prefetched options resolve to exactly one entry and the  field is still empty, auto-select that entry so the user doesn't have to  open the picker for a foregone choice. |
| `optionsPrefetch` | unknown | yes | Agnostic projection: each entry maps a *result option field* to a  *dotted source path* into the row. Schema authors decide which  fields to project. `label` + `value` required at runtime. |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "recordPickerField-1",
  "type": "recordPickerField",
  "attributes": {
    "label": "Sample label",
    "optionsPrefetch": null,
    "rowGroup": null
  }
}
```

### repeatableGroup

No description.

**squad-ui component:** [`repeatable-group-block`](https://sdk-components.thesqd.com/r/repeatable-group-block.json)

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `itemLabel` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `fields` | object[] | yes | fileUpload — upload destination. `path` is a folder prefix template  (supports `{{token}}`); resolves with URL params, prefetch, top-level  field ids, and this row's own field keys (row values win). |
| `minInstances` | number | no |  |
| `maxInstances` | number | no |  |
| `instancesFromField` | string | no |  |
| `mode` | unknown | yes |  |
| `selectableDisplay` | unknown | yes |  |
| `options` | object[] | no |  |
| `titleField` | string | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "repeatableGroup-1",
  "type": "repeatableGroup",
  "attributes": {
    "label": "Sample label",
    "fields": [{"key":"name","type":"text","label":"Name","required":true}],
    "mode": null,
    "selectableDisplay": null,
    "rowGroup": null
  },
  "parentId": "page-1"
}
```

### repeatableInput

No description.

**squad-ui component:** n/a

**Stores:** Stored as the raw value passed by the field component.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `inputType` | unknown | no |  |
| `minInstances` | number | no |  |
| `maxInstances` | number | no |  |

```json
{
  "id": "repeatableInput-1",
  "type": "repeatableInput",
  "attributes": {
    "label": "Sample label"
  },
  "parentId": "page-1"
}
```

### richTextField

No description.

**squad-ui component:** [`field-label`](https://sdk-components.thesqd.com/r/field-label.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `enableDictation` | unknown | yes |  |
| `enableSpellCheck` | unknown | yes |  |
| `aiGenerate` | unknown | yes |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "richTextField-1",
  "type": "richTextField",
  "attributes": {
    "label": "Sample label",
    "enableDictation": null,
    "enableSpellCheck": null,
    "aiGenerate": null,
    "rowGroup": null
  }
}
```

### sectionHeader

No description.

**squad-ui component:** [`section-header`](https://sdk-components.thesqd.com/r/section-header.json)

**Stores:** Layout-only; no value stored.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "sectionHeader-1",
  "type": "sectionHeader",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### selectField

No description.

**squad-ui component:** [`select`](https://sdk-components.thesqd.com/r/select.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `options` | object[] | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "selectField-1",
  "type": "selectField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### sliderField

No description.

**squad-ui component:** [`slider`](https://sdk-components.thesqd.com/r/slider.json)

**Stores:** Stored as a number; honors min/max from attributes.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `min` | number | no |  |
| `max` | number | no |  |
| `step` | number | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "sliderField-1",
  "type": "sliderField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### subformField

No description.

**squad-ui component:** [`card`](https://sdk-components.thesqd.com/r/card.json)

**Stores:** Stored as an array of objects (shape defined by the entity).

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `formId` | unknown | yes |  |
| `minSubmissions` | unknown | no |  |
| `maxSubmissions` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "subformField-1",
  "type": "subformField",
  "attributes": {
    "label": "Sample label",
    "formId": null,
    "rowGroup": null
  }
}
```

### switchCalloutField

No description.

**squad-ui component:** [`switch`](https://sdk-components.thesqd.com/r/switch.json)

**Stores:** Stored as a boolean; must be true when required.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `imageUrl` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "switchCalloutField-1",
  "type": "switchCalloutField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### textArea

No description.

**squad-ui component:** [`textarea-field`](https://sdk-components.thesqd.com/r/textarea-field.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `labelTooltip` | object | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minLength` | number | no |  |
| `maxLength` | number | no |  |
| `enableDictation` | unknown | yes |  |
| `aiGenerate` | unknown | yes |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "textArea-1",
  "type": "textArea",
  "attributes": {
    "label": "Sample label",
    "enableDictation": null,
    "aiGenerate": null,
    "rowGroup": null
  }
}
```

### textField

No description.

**squad-ui component:** [`text-field`](https://sdk-components.thesqd.com/r/text-field.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `minLength` | number | no |  |
| `maxLength` | number | no |  |
| `pattern` | string | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |
| `autocomplete` | boolean | no | Text-field opt-in for autocomplete suggestions sourced from the account's previous submissions. When false (default), the field never shows the suggestion dropdown even if prior values exist. Only honored by text inputs. |

```json
{
  "id": "textField-1",
  "type": "textField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### timeField

No description.

**squad-ui component:** [`text-field`](https://sdk-components.thesqd.com/r/text-field.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "timeField-1",
  "type": "timeField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### timePickerField

No description.

**squad-ui component:** [`select`](https://sdk-components.thesqd.com/r/select.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `stepMinutes` | number | no |  |
| `minTime` | string | no |  |
| `maxTime` | string | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "timePickerField-1",
  "type": "timePickerField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### toggleField

No description.

**squad-ui component:** [`switch`](https://sdk-components.thesqd.com/r/switch.json)

**Stores:** Stored as a boolean (defaults to false).

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "toggleField-1",
  "type": "toggleField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

### urlField

No description.

**squad-ui component:** [`text-field`](https://sdk-components.thesqd.com/r/text-field.json)

**Stores:** Stored as a non-empty string.

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no |  |
| `questionShortLabel` | string | no |  |
| `description` | string | no |  |
| `placeholder` | string | no |  |
| `required` | boolean | no |  |
| `disabled` | boolean | no |  |
| `visibilityRule` | unknown | no |  |
| `accountVisibility` | unknown | no |  |
| `rowGroup` | unknown | yes | Optional row-grouping key. Consecutive visible fields on the same page that  share the same non-empty `rowGroup` string render side-by-side in one row  (a responsive grid: stacked on mobile, columns on `sm+`). Omit it (the  default) to keep the field full-width on its own row. |

```json
{
  "id": "urlField-1",
  "type": "urlField",
  "attributes": {
    "label": "Sample label",
    "rowGroup": null
  }
}
```

