# Dynamic Forms Module

:::note
This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation [here](/sdk-reference/web-sdk-reference).  Contact your Incode Representative for upgrade information and check if you are a candidate for this upgrade. <br /><br />Full rollout to all clients still TBD.
:::

The Dynamic Forms module steps the user through a multi-screen questionnaire whose screens, questions, and input types come from the backend per session. Use it for jurisdiction-specific questions that change without an SDK release.

> Follows the [form-based pattern](/sdk-reference/web-sdk-2-module-patterns/#1-form-based-modules) across multiple screens. See the patterns page for the shared lifecycle.

## Availability

This module is headless-only — there is no public `<incode-dynamic-forms>` web component. Inside `<incode-flow>` the orchestrator renders it for you. To drive it yourself, use `createDynamicFormsManager` from `@incodetech/core/dynamic-forms` and render your own form.

## Configuration

```typescript
type DynamicFormsConfig = {
  flowId?: string;
  screens?: Screen[];
};
```

| Option    | Type       | Required | Description                                                                          |
| --------- | ---------- | -------- | ------------------------------------------------------------------------------------ |
| `flowId`  | `string`   | ❌       | Fetches the screen configuration from the backend. The orchestrator sets this.        |
| `screens` | `Screen[]` | ❌       | Pre-loaded screens. Providing them skips the fetch.                                   |

Supply one of the two. If both are absent the module enters `misconfigured` rather than failing at runtime.

### Screens and questions

```typescript
type Screen = {
  title?: string;
  hideTitle?: boolean;
  questions: Question[];
};

type Question = {
  questionId: string;
  question: string;
  inputType?: InputType;
  overrides?: InputType;
  isPredefined?: boolean;
  isOptional?: boolean;
  options?: string[];
};
```

| Property       | Type         | Description                                                                                 |
| -------------- | ------------ | ------------------------------------------------------------------------------------------- |
| `questionId`   | `string`     | Stable identifier. Use it as the key for every answer and validation call.                   |
| `question`     | `string`     | The label to display.                                                                       |
| `inputType`    | `InputType?` | Which control to render. Absent values fall back to `overrides`, then to `TEXT`.              |
| `overrides`    | `InputType?` | Fallback type used when `inputType` is absent.                                               |
| `isPredefined` | `boolean?`   | Whether the question comes from Incode's predefined set rather than a tenant-authored one.    |
| `isOptional`   | `boolean?`   | Whether the user can leave the answer empty.                                                 |
| `options`      | `string[]?`  | Choices for `SELECT` and `MULTISELECT`.                                                      |

`InputType` is `'TEXT'`, `'DATE'`, `'COUNTRY'`, `'NATIONALITY'`, `'NUMBER'`, `'EMAIL'`, `'PHONE'`, `'CPF'`, `'NAME'`, `'YESNO'`, `'SELECT'`, or `'MULTISELECT'`. Treat the union as open — an unrecognized value renders as text rather than breaking the screen.

## State machine

`DynamicFormsState` is a discriminated union over `status`:

| Status           | Description                                                                       |
| ---------------- | --------------------------------------------------------------------------------- |
| `idle`           | Initial state, waiting for `load()`.                                              |
| `loadingScreens` | Fetching the screen configuration from the backend.                                |
| `inputting`      | Current screen rendered; the user answers questions.                               |
| `submitting`     | Sending the current screen's answers. The screen stays available so you can show a spinner. |
| `success`        | All screens submitted. Shows briefly before `finished`.                            |
| `finished`       | Terminal. Carries `result`.                                                       |
| `misconfigured`  | Neither `screens` nor `flowId` was supplied. A configuration problem, not a runtime error. |
| `closed`         | The user dismissed the module.                                                     |

When `status === 'inputting'`:

| Property           | Type                             | Description                                                                                       |
| ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------- |
| `currentScreen`    | `Screen`                         | The screen to render.                                                                             |
| `screenIndex`      | `number`                         | Zero-based index of the current screen.                                                            |
| `totalScreens`     | `number`                         | How many screens the form has. Use both to show progress.                                          |
| `answers`          | `Record<string, string>`         | Current values, keyed by `questionId`.                                                             |
| `answerValidity`   | `Record<string, boolean>`        | Format validity you reported through `setAnswerValidity`.                                          |
| `validationErrors` | `DynamicFormsValidationErrors?`  | Errors currently shown to the user, keyed by `questionId`. Each value is an i18n key. Render as-is. |
| `canSubmit`        | `boolean`                        | `true` when no validation error is displayed. See the note below.                                   |

`submitting` carries the same screen, answers, and validity, frozen at submission time.

`finished` carries `result`: `'completed'` when every screen was submitted, or `'skipped'` when the screen configuration could not be loaded. Loading failures skip the module rather than blocking the flow.

> **`canSubmit` is optimistic.** It starts `true` on an empty screen and only turns `false` after a submit attempt populates `validationErrors`. Binding a Continue button to `canSubmit` matches the SDK's own behavior; it does not mean every required answer is present.

## API methods

| Method                                  | Purpose                                                                                       |
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
| `load()`                                | Starts the module. Call this before anything else.                                             |
| `setAnswer(questionId, value)`          | Record the raw user input for one question.                                                    |
| `setAnswerValidity(questionId, isValid)` | Report whether a format-constrained value is valid. Use for `PHONE`, `EMAIL`, and `CPF`.        |
| `validateField(questionId)`             | Validate one question, typically on blur. Sets or clears its `validationErrors` entry.          |
| `submit()`                              | Submit the current screen, then advance to the next screen or finish.                          |
| `close()`                               | Dismiss the module from `misconfigured`. Fix the configuration rather than calling `reset()`.   |

Plus the universal lifecycle: `subscribe`, `getState`, `reset`, `stop`.

## Headless example

```typescript
import { setup } from '@incodetech/core';
import { createDynamicFormsManager } from '@incodetech/core/dynamic-forms';

const manager = createDynamicFormsManager({ config: { flowId } });

manager.subscribe((state) => {
  if (state.status === 'inputting') {
    renderScreen({
      screen: state.currentScreen,
      progress: `${state.screenIndex + 1} of ${state.totalScreens}`,
      answers: state.answers,
      // validationErrors values are i18n keys — resolve them for display.
      errors: state.validationErrors,
      canSubmit: state.canSubmit,
    });
  }

  if (state.status === 'misconfigured') {
    console.error('Dynamic Forms needs either screens or a flowId');
  }

  if (state.status === 'finished') {
    console.log('Dynamic Forms', state.result);
  }
});

manager.load();
```

## See also

- [Module: Custom Fields](/sdk-reference/web-sdk-2-module-custom-fields/): dashboard-defined single-screen field schema
- [Module: eKYC](/sdk-reference/web-sdk-2-module-ekyc/): identity-data form with a fixed field catalog
- [Module Patterns → form-based](/sdk-reference/web-sdk-2-module-patterns/#1-form-based-modules)
- [Individual Modules](/sdk-reference/web-sdk-2-individual-modules/)
