
# Form Success & Error Handling

The submission commits and the request returns. What happens on screen next is a product decision, not a technical one: a support ticket wants a reference number, a newsletter signup wants to stay put and accept another address, a checkout wants to move on.

`onSuccess` and `onError` make that decision declarative:

```yaml
forms:
  - id: 1
    name: contact
    title: Contact Sales
    submitTo: { table: leads }
    fields:
      - { kind: table-field, column: email, required: true }
    onSuccess:
      type: successPage
      title: Thank you
      message: We received your message and will reply within one business day.
    onError:
      type: message
      message: Something went wrong saving your request. Please try again.
```

## `onSuccess` Types

`onSuccess` is a union discriminated by `type`. Each type carries its own properties.

| `type`        | Properties                                                                  | Behavior                                                                   |
| ------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `successPage` | `title`, `message`, `buttonLabel`, `buttonHref`, `actions[]`, `showSummary` | Replaces the form with a success screen.                                   |
| `redirect`    | `url` (required), `delaySeconds`                                            | Navigates away. `delaySeconds` defaults to `2`; `0` navigates immediately. |
| `reset`       | `message`, `preserveFields[]`                                               | Clears the form for another submission, keeping the listed fields' values. |
| `toast`       | `message` (required), `variant` (`success` \| `info`)                       | Shows a transient notification and leaves the filled form in place.        |
| `message`     | `message` (required)                                                        | Replaces the form with an inline message.                                  |

Omit `onSuccess` entirely and the form falls back to a `success` toast reading "Submitted." — a sane default, but rarely the one you want in production.

`reset` is the type to reach for on a single-page form a submitter fills repeatedly. On a `multi-step` layout it clears the answers but does not return the submitter to the first step, so prefer `successPage` with a `reset` action there.

## Success-Page Actions

A `successPage` can offer the submitter a next move. Each entry in `actions[]` renders as a button:

```yaml
onSuccess:
  type: successPage
  title: Ticket received
  message: Our team has been notified.
  showSummary: true
  actions:
    - { label: Submit another, action: reset }
    - { label: Back to help centre, action: navigate, url: /help }
```

| Property | Description                                                              | Default   |
| -------- | ------------------------------------------------------------------------ | --------- |
| `label`  | Button text. Supports `$t:` keys.                                        | Required. |
| `action` | `reset` returns the empty form; `navigate` sends the submitter to `url`. | Required. |
| `url`    | Navigation target. Required for `navigate`, ignored by `reset`.          | —         |

`showSummary: true` lists the submitted values below the message. It skips hidden fields and applies per-field read permissions, so a summary never shows a submitter something they were not allowed to read back.

## Template Variables

Success copy and redirect targets can quote the submission that just happened. Three variables are substituted at submit time, in `title`, `message` and `url` alike:

| Variable           | Resolves to                                                                    |
| ------------------ | ------------------------------------------------------------------------------ |
| `$submission.id`   | The ledger row id. Empty when `submitTo.storeSubmission` is `false`.           |
| `$record.id`       | The bound-table row id. Empty when the form has no `submitTo.table`.           |
| `$record.<column>` | A column of the inserted row — but only one the submitter themselves supplied. |

```yaml
onSuccess:
  type: redirect
  url: /thank-you?ref=$submission.id
  delaySeconds: 0
```

Values substituted into a `url` are percent-encoded, so an email address survives the query string intact. The `$record.<column>` restriction is deliberate: server-computed and privileged columns are never exposed through a redirect URL, however tempting the shortcut.

:::callout
**An unresolved variable becomes empty, never the literal token.** A `$record.id` on a form with no bound table substitutes to nothing, so `/done?recId=$record.id` navigates to `/done?recId=`. The redirect still fires and the submission is still recorded — you get a silently empty parameter rather than a broken URL or a leaked template string. Check the target page tolerates an empty value before templating one in.
:::

## `onError`

`onError` covers whole-submission failures — the write was rejected, the request did not complete.

| Property  | Description                                              | Default   |
| --------- | -------------------------------------------------------- | --------- |
| `type`    | `toast`, `message` (inline), or `errorPage` (full page). | Required. |
| `message` | Body text. Supports `$t:` keys.                          | Required. |
| `title`   | Heading, used by `errorPage`.                            | —         |
| `variant` | `error` or `warning`. Only meaningful for `type: toast`. | `error`   |

Omitted, it falls back to a toast reading "Submission failed."

Per-field validation errors are a different channel. An invalid email or a missing required value comes back as a `fieldErrors` list and is rendered inline against the offending input — `onError` fires alongside it as the whole-form summary, not instead of it.

## Related Pages

- [Submissions](/en/docs/form-submissions) — the dual write that has to commit before `onSuccess` runs.
- [Form Fields](/en/docs/form-fields) — the per-field read permissions `showSummary` honors.
- [Multi-Step Forms](/en/docs/form-multi-step) — what "after the last step" means for these types.
- [Forms Overview](/en/docs/forms-overview) — where `onSuccess` and `onError` sit in the full form schema.
