> ## Documentation Index
> Fetch the complete documentation index at: https://docs.platform.embeddables.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Forms SDK

> @embeddables/forms — collect, validate, and persist form data across a funnel

## Overview

Forms handles the state of a multi-step form: it saves answers locally as the visitor moves through it, checks each input against your rules, and sends the completed data to Embeddables in the background.

The walkthrough below covers install and setup. For every option, method, hook, and analytics event,
open the [reference](/sdks/forms/reference).

## Field types

A form is a list of fields, each with one of these types:

| Field type    | Description                         |
| ------------- | ----------------------------------- |
| `text`        | Free text                           |
| `email`       | An email address (format-validated) |
| `number`      | Numeric values                      |
| `boolean`     | True/false                          |
| `select`      | Single choice from a list           |
| `multiselect` | Multiple choices from a list        |
| `json`        | Structured data                     |

Each field can have validation rules: `required`, min/max length, min/max value, pattern matching, and your own custom checks.

## Implementation

### Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @embeddables/core @embeddables/forms
  ```

  ```bash pnpm theme={null}
  pnpm add @embeddables/core @embeddables/forms
  ```

  ```bash yarn theme={null}
  yarn add @embeddables/core @embeddables/forms
  ```
</CodeGroup>

The React bindings work with React 18 or newer. Add Forms with the CLI so `em build` includes it in the generated `modules` file — the React example imports that file. For form event tracking (`data:updated`, `form:submitted`), add `@embeddables/analytics` with the CLI as well; see [Analytics](#analytics).

### Define a schema

Each form is one object: an `id`, an optional `name`, and its `fields`.

```typescript lines theme={null}
import type { FormSchema } from '@embeddables/forms'

const schema = {
  id: 'signup',
  name: 'Signup',
  fields: [
    {
      key: 'email',
      label: 'Email',
      type: 'email',
      validations: { required: true },
    },
  ],
} as const satisfies FormSchema
```

Available validation rules: `required`, `minLength`, `maxLength`, `min`, `max`, `pattern` (a string, not a `RegExp`), `oneOf`, and an optional `validations.custom` function.

`pattern` is the regular expression source without delimiters. To apply regex flags, put them in an optional leading `(?flags)` prefix on the pattern itself — for example `'(?u)^\\p{L}+$'` for a Unicode-aware match. Any of `dgimsuvy` are accepted; `g` and `y` are stripped before the pattern compiles, so validation stays stateless across repeated checks.

<Note>
  Add `as const satisfies FormSchema` to schemas you write by hand — it makes `.set()`, `.get()`,
  and `.getAll()` aware of your field keys. CLI-generated schemas already have this.
</Note>

With a schema defined, pick your setup:

<Tabs>
  <Tab title="React">
    Follow the example below: wrap your app once, then connect each field with `useForm` and `useFormField`.

    ```tsx lines theme={null}
    import { EmbeddablesProvider } from '@embeddables/core/react'
    import { useForm, useFormField } from '@embeddables/forms/react'
    import { config } from './embeddables/_dist'
    import { modules } from './embeddables/_dist/modules'

    function SignupField() {
      // Pass the form id — no type arguments needed.
      const { form } = useForm({ formId: 'signup' })
      const { value, error, setValue, onBlur } = useFormField({ form, key: 'email' })

      // `form` is null until it's ready — show a loading state.
      if (!form) return <p>Loading…</p>

      return (
        <label>
          Email
          <input
            value={value ?? ''}
            onChange={(event) => setValue(event.target.value)}
            onBlur={() => void onBlur()}
          />
          {error?.[0]}
        </label>
      )
    }

    export function App() {
      return (
        <EmbeddablesProvider
          config={{ ...config, publishableKey: 'pk_sandbox_<your-key>' }}
          modules={modules}
        >
          <SignupField />
        </EmbeddablesProvider>
      )
    }
    ```

    If you used the CLI, the `modules` import in the example already turns Forms on — you rarely need
    extra wiring. Schemas come from Core config (`config.forms`); optional per-form settings go on
    [`forms()`](/sdks/forms/reference#forms-module) (`customValidations`, `serverFormData`, and
    Analytics). For `useForm`, `useFormField`, and `useFormErrors`, see
    [hooks](/sdks/forms/reference#hooks).

    <Tip>
      Save answers when the visitor leaves a field (or taps "next"), not on every keystroke — the example
      does this for you. Saving too often creates noisy analytics and extra backend traffic.
    </Tip>

    <Note>
      Two screens that use the same form id share one form — you will not get duplicate copies.
    </Note>

    <Warning>
      Show a short loading state until the form is ready (`form` is empty at first). If it never becomes
      ready, add Forms with the CLI so it is included in the generated `modules` file.
    </Warning>
  </Tab>

  <Tab title="JavaScript">
    ```typescript lines theme={null}
    import { initEmbeddables } from '@embeddables/core'
    import { initForms } from '@embeddables/forms'
    import { config } from './embeddables/_dist'

    const core = initEmbeddables({
      ...config,
      publishableKey: 'pk_sandbox_<your-key>',
      forms: { signup: schema },
    })

    const forms = initForms({ core })
    const form = forms.getForm({ formId: 'signup' })

    const result = await form.set({ email: 'maria@gmail.com' })
    if (!result.ok) console.log(result.errors)

    const email = form.get('email')
    await form.submit()
    ```

    The form from `getForm` gives you `set`, `get` / `getAll`, `getValueByProtocolFieldId`, `validate`, `submit`, `errors`, and `clear`. See the [reference](/sdks/forms/reference#methods) for what each one does.

    <Warning>
      `set`, `submit`, and `validate` never throw on a validation failure — always check `result.ok` /
      `result.errors`. Saving to the backend happens in the background too: if it fails, `.set()` and
      `.submit()` still don't throw — your form keeps working.
    </Warning>

    <Tip>
      Save when the visitor leaves a field or moves to the next step — not on every keystroke. Each save
      can send analytics and update the backend; doing it per character adds noise. Disable double-submit
      on your submit button — each submit sends again.
    </Tip>

    Pass your Core setup into `initForms`. Optional validation rules, values from the server, and Analytics
    are in the [reference](/sdks/forms/reference#options). Form definitions live in your project config
    (from the CLI), not as a separate argument on `initForms`.
  </Tab>
</Tabs>

### Analytics

Forms does not depend on Analytics. In non-React code, set up Analytics first, then pass it into `initForms` when you connect the two (see [Analytics](#analytics) below):

```typescript lines theme={null}
import { initAnalytics } from '@embeddables/analytics'

const analytics = initAnalytics({ core })
const forms = initForms({ core, analyticsInstance: analytics })
```

In React, adding both SDKs with the CLI connects Analytics to Forms for you — no extra steps.

When Analytics is connected:

* A successful `.set()` emits `data:updated` plus one `field:updated` per changed field
* `.submit()` emits `form:submitted`

If tracking fails, you get a `trackError` on the result — the values are still saved. Leaving analytics out just turns off event tracking; the backend save still happens as long as Core has a publishable key.

<Warning>
  When Analytics is connected to Forms, do not also send the same events yourself — you will count
  them twice (`data:updated`, `field:updated`, and `form:submitted`).
</Warning>

### Errors

| When                           | What you get                                         |
| ------------------------------ | ---------------------------------------------------- |
| Invalid Core instance at init  | `FormsError` thrown                                  |
| Invalid schema at init         | `SchemaError` thrown                                 |
| Validation failure / bad patch | `{ ok: false, errors }` on the result — never thrown |
| Analytics call fails           | `trackError` on the result — values are still saved  |

If one of your custom validators throws, that error comes straight out of `.set()` / `.submit()`. Everything else is caught and reported on the result instead.
