---
title: "Dynamic Forms Module"
url: "https://developer.incode.com/sdk-reference/web-sdk-2-module-dynamic-forms/"
section: "sdk-reference"
group: "Incode Web SDK 2 Reference / Web SDK 2 Individual Modules"
version: "v1.1"
status: "live"
---
# 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.

This module follows the [form-based pattern](/sdk-reference/web-sdk-2-module-patterns/#1-form-based-modules) across multiple screens.

## Tag

`<incode-dynamic-forms>` is a standard Web Component. Importing the UI subpath registers the custom element; importing the CSS applies the module's styles.

```ts
import '@incodetech/web/dynamic-forms';
import type { DynamicForms } from '@incodetech/web/dynamic-forms';
import type { ComponentProps } from 'preact';
import '@incodetech/web/dynamic-forms/styles.css';
```

This is the Web SDK 2.0 equivalent of Web SDK 1.x `renderForms`. SDK 2.0 uses the same Web Component integration pattern as the other standalone modules instead of an imperative render helper.

## Properties

| Property   | Type                                                | Required | Description                                                                                                                                                       |
| ---------- | --------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config`   | `DynamicFormsConfig`                                | ❌        | Form configuration. If omitted, the component resolves the `DYNAMIC_FORMS` configuration from the active Dashboard Flow.                                         |
| `manager`  | `DynamicFormsManager`                               | ❌        | Advanced: a pre-built headless manager. Providing one bypasses standalone configuration resolution.                                                               |
| `onFinish` | `() => void`                                        | ❌        | Called when the module finishes.                                                                                                                                   |
| `onError`  | `(error?: string, errorCode?: number) => void`      | ❌        | Called when standalone Dashboard configuration cannot be resolved.                                                                                                 |

## Standalone example

This example mirrors the SDK 1.x `renderForms(container, options)` use case with a locally supplied screen definition:

```ts
import { setup } from '@incodetech/web';
import '@incodetech/web/base.css';
import '@incodetech/web/themes/light.css';
import '@incodetech/web/dynamic-forms';
import type { DynamicForms } from '@incodetech/web/dynamic-forms';
import type { ComponentProps } from 'preact';
import '@incodetech/web/dynamic-forms/styles.css';

await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: sessionToken,
  flow: false,
  theme: false,
});

const forms = document.createElement('incode-dynamic-forms') as HTMLElement & ComponentProps<typeof DynamicForms>;
forms.config = { screens };
forms.onFinish = () => showNextStep();
forms.onError = (error) => console.error(error);

document.querySelector('#forms-container')?.replaceChildren(forms);
```

| SDK 1.x `renderForms` input | SDK 2.0 equivalent                                                     |
| --------------------------- | ---------------------------------------------------------------------- |
| `element`                   | Mount or append `<incode-dynamic-forms>` in that container             |
| `token`                     | `setup({ token })`                                                      |
| `interviewId`               | No separate property; the session token identifies the onboarding     |
| `flowId`, `screens`         | `forms.config = { flowId, screens }`                                   |
| `onSuccess`                 | `forms.onFinish`                                                       |
| `onError`                   | `forms.onError`                                                        |

Recoverable submission failures stay inside the component and show a retry screen. They do not call `onError`.

To use the Dashboard-configured module instead, omit `forms.config`. The standard standalone bootstrap loads the `DYNAMIC_FORMS` configuration for the active session. Inside `<incode-flow>`, the orchestrator continues to mount and configure the same component automatically.

## Configuration

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

| 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.                                                                                                                                                                                                                                                  |
| `prefillValue` | `string`   | ❌        | Prefills the answer on a single-question first screen — a login hint your page read from its own URL, for example. Applies only when the form is exactly one screen with one question, and is ignored otherwise. The field stays editable. Your code reads the value; the SDK never touches the URL. |

When bypassing Dashboard Flow resolution, supply `flowId` or `screens`. If neither value is available after configuration resolution, 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[];
  prefillSource?: string | null;
  editable?: boolean;
};
```

| 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.                                               |
| `prefillSource` | `string \| null` (optional) | Dashboard-selected source for a captured-ID value. |
| `editable` | `boolean?` | Whether a successfully prefilled value can be edited. Default `true`; meaningful with `prefillSource`. |
| `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.

### Prefill from captured ID

Questions with `prefillSource` can receive values from the customer's captured ID. `PHONE` and `MULTISELECT` inputs are excluded. If a source does not resolve, the question stays blank and editable.

In `inputting` and `submitting`, `prefilledQuestionIds: string[]` identifies populated questions and `nonEditableQuestionIds: string[]` identifies read-only ones. Render the latter read-only with a “from ID” marker. Their values still submit; do not remove them from the payload. Resolve display text with your translations.

## 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.                                                                                    |
| `submitFailed`      | An answer did not reach the backend because the server failed. Show an error screen with a Try again button that calls `retry()`.                                              |
| `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.                                                                                     |
| `attemptsExhausted` | Terminal. The backend rejected the login hint too many times and locked the user out. Show the title and subtitle with no retry — the user has to contact their administrator. |
| `closed`            | The user dismissed the module.                                                                                                                                                 |

`attemptsExhausted` stays inside this module rather than routing through the orchestrator's terminal error screen, because that screen's retry would reload the flow and fail against a single-use token. An invalid hint that still has attempts left keeps the user on the screen with an inline field error instead.

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.                                           |
| `prefilledQuestionIds` | `string[]` | Questions populated from captured ID. |
| `nonEditableQuestionIds` | `string[]` | Successfully prefilled questions that must render read-only. |
| `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; translate it for display. |
| `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.

The module treats the two kinds of submission failure differently. If the server fails (HTTP 5xx) or the request does not complete, the module enters `submitFailed`. Call `retry()` to send the answers again. If the backend refuses the content of one answer (HTTP 4xx), the module drops that answer, submits the remaining ones and continues to the next screen, because the same request would fail again. Blank optional answers are not sent at all.

> `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.                         |
| `retry()`                                | Send the current screen again after `submitFailed`. Skips answers the backend already took.  |
| `close()`                                | Dismiss the module from `misconfigured`. Fix the configuration rather than calling `reset()`. |

Plus the universal lifecycle: `subscribe`, `getState`, `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/)