# Structured Data Forms — collecting typed fields from a viewer

This page shows how to make an agent ask the viewer for structured, typed data mid-conversation. Examples: a support ticket's category and urgency, a booking's preferred date, a survey rating, or a sales lead's email and phone. It also covers how the SDK renders that request, and how to read the submitted values back.

`user_properties_forms` is a general-purpose "collect typed fields from the viewer" primitive. The fields, the instruction the agent acts on, rendering, and reporting all work identically no matter what the fields represent. The one place this shows up in the SDK's own surface is naming: the method you call to report values back is `session.submitStructuredDataForm()`, over a wire event named `setFormLeadInfo`. Both are named after the feature's most common use case, not its only one.

## What it is and isn't

You configure `user_properties_forms` once, at [intellect](https://github.com/kaltura/intelligent-agents-sdk/blob/main/README.md#architecture) creation or update, as a list of "stages":

<div data-nova-target="structured-data-forms-example" data-nova-label="Configure structured data forms example">

```js
await mgmt.intellectConfig.setUserPropertiesForms(configId, [
  { callStage: 'middle', properties: [
    { key: 'issueCategory', type: 'str' },
    { key: 'urgency', type: 'str' },
  ] },
], adminKs);
```

</div>

`callStage` (sent as `call_stage`) is one of `start` / `middle` / `end` (`CALL_STAGES` in `src/management/intellect-config.js`). Each property is `{key, type}`. Two sets of `type` values exist:

| Type kind | Values |
|---|---|
| Base types you configure (`ARG_TYPES`, re-exported from `core/stream.js`'s `ARG_TYPE_NAMES`, the single source of truth) | `str`, `int`, `float`, `bool`, `list`, `dict` |
| Renderer-only presentation hints, kept when an incoming form carries them (see "How the form is rendered" below) | `email`, `phone`, `text` |

`buildUserPropertiesForms()` accepts only the six base types. It validates before any network call and throws `bad_request` on an invalid stage, an empty `properties` array, or any other type, including `email`, `phone` and `text`.

You can pass one form object or an array of several. Each stage gets its own field set. For example, ask for a booking date early and a payment preference later, or an email early and a phone number later, in the same conversation.

## What configuring a stage actually does

Each configured stage becomes an instruction for that conversation. When the stage arrives, the agent asks for exactly that stage's fields, in addition to its normal spoken reply. Your app receives the request as a form widget (see below). Two consequences:

- **Timing is approximate.** The model interprets `call_stage` as a general instruction, not a deterministic trigger. A `start` stage can fire on the very first turn. Treat the stage as a signal for when the form appears, not an exact turn number.
- **Pre-fill is real.** If the agent has already heard a value for a configured field earlier in the conversation (e.g. the viewer mentioned their preferred date in passing), it attaches a `known_value` to that field. Your form should pre-fill it rather than ask again.

The form arrives with a default heading. **`buildUserPropertiesForms()` doesn't accept a heading.** It accepts `callStage`/`properties` only. To show your own copy (e.g. "Tell us about your issue" on a support form), render the widget yourself and replace the descriptor's `data.title`; see "How to customize or style the form" below.

## What's possible / what's not

**Possible:**

- Multiple stages (`start`/`middle`/`end`), each with its own field list. Each list can be any shape of structured data, not just contact info.
- Six field types (`str`/`int`/`float`/`bool`/`list`/`dict`), plus `email`/`phone`/`text` as renderer-side presentation hints.
- A `required` flag and a `description` per field when the incoming form carries them. The default renderer maps them to `aria-required`/`aria-describedby`.
- Model-side pre-fill (`known_value`) from information already in the transcript.
- Reporting the collected values back over the wire via `session.submitStructuredDataForm(values)`. It works identically for any field shape.

**Not possible / not designed for:**

- No conditional logic beyond the model's own judgment. There's no "only ask if X wasn't already provided" you can express declaratively.
- No precise timing control. `call_stage` is an instruction the model interprets, not a deterministic trigger pinned to a specific turn number.
- Not a general dynamic-form builder. It's a flat list of typed fields per stage, not richer patterns (multi-step wizards, validation rules, fields dependent on other fields).
- Not tunable per session. You author `user_properties_forms` once at provisioning time through the SDK's intellect configuration (`mgmt.intellectConfig`).

## How the SDK handles it: two observation points, one descriptor

The model's form emission reaches your app through **one of two equivalent paths**, both normalized to the same shape by the SDK:

1. **The tool-call path**: the model's first emission of the form arrives as a `show_widget`-style tool call with `kind: "user_properties_form"`, parsed via `collectConverse()`/`parseToolCall` (headless/HTTP converse).
2. **The unisphere-tool segment path**: on the live avatar socket, it arrives as a `unisphere-tool` segment with `metadata.runtimeName: "user-properties-form-tool"` (one of the nine first-class runtimes, see [GenUI Reference](https://kaltura.github.io/intelligent-agents-sdk/reference/genui-reference/index.md)).

Both are routed to `ExperienceRenderer` (`src/experience/genui/renderer.js`), which `normalizeRuntime()`s the runtime name (stripping a trailing `-tool`) and calls the registered renderer. By default that's `renderUserPropertiesForm()` (`src/experience/genui/renderers/user-properties-form.js`). That function is pure and defensive:

- Accepts `fields`/`properties`/`items` interchangeably.
- Coerces an unrecognized type to `'str'`.
- Runs every string through `safeText` (XSS-safe truncation).
- Drops any field with no key.

The output is a framework-agnostic descriptor:

```js
{ kind: 'user-properties-form', data: { title, fields: [
  { key, type, label, knownValue, required, description }
] } }
```

## How the form is rendered

If you hand `mountWidget` (`src/experience/genui/renderers/mount.js`) a real DOM element as the mount target, it builds the whole thing for you:

- One `<form class="kgenui__form">`.
- One `<div class="kgenui__field">` per field, with a `<label>` and an `<input>`. The input `type` is inferred from the field type via `htmlInputType()`: `email`→`email`, `phone`→`tel`, `int`/`float`→`number`, `bool`→`checkbox`, anything else→`text`.
- `aria-required` and `aria-describedby` wired on each field.
- Fields pre-filled from `knownValue`.
- A submit button.

It never uses `innerHTML`, so text supplied by the model can't inject markup. On submit, it calls your `opts.onAction('submit', {values})` callback.

`user_properties_forms` is not the only way to reach the widget. An app can instead reach the same `user-properties-form` widget through a different path: keep `kaltura_genie_experiences` off, and expose the widget as one enum value of your own native `show_widget` **client** tool. The model then decides when to call it, based on your own prompt-level timing rules, with no `call_stage`-driven form needed at all.

## How to customize or style the form

Two independent axes:

- **Behavior / DOM structure.** Override the whole renderer function via `ExperienceRenderer`'s `renderers` config or its `.register()` method:

  ```js
  renderer.register('user-properties-form', (model) => ({
    kind: 'user-properties-form',
    data: myOwnShape(model),
  }));
  ```

Or skip the SDK's default DOM builder entirely by passing a mount **function** instead of an `Element`. You get the descriptor and build any UI you want (React, a modal, whatever), calling `session.submitStructuredDataForm(values)` yourself when the viewer submits.
- **Visual styling.** The default DOM path emits plain, unstyled class names only: `.kgenui__form`, `.kgenui__field`, `.kgenui__label`, `.kgenui__input`, `.kgenui__help`, `.kgenui__submit`. The SDK ships **zero CSS**. Any consumer styles the same class names in their own stylesheet.

## Reading back what the viewer submitted

`session.submitStructuredDataForm(info)` is a fire-and-forget socket emit of `setFormLeadInfo`. It returns nothing and has no acknowledgment. The SDK has no call that returns the submitted values as structured `{key: value}` data.

`threads.transcript(threadId, ks)` returns the plain-text transcript of the thread (`human:` / `ai:` lines, in the result's `data` field). It carries what the viewer said and typed and what the model replied. It does not carry the raw form object.

For durable, structured access, forward the values yourself. Bridge the submitted values into `request_vars`, then have the model call your own server-side `api` tool that posts them to a CRM, a support system, a spreadsheet, or any other external API. See [External API Integrations](https://kaltura.github.io/intelligent-agents-sdk/guides/external-api-integrations/index.md).

## Not to be confused with `kaltura_genie_experiences`

Intellects that use custom `tool_ids` need `kaltura_genie_experiences` off. See [External API Integrations § Don't skip `kaltura_genie_experiences: 'off'`](https://kaltura.github.io/intelligent-agents-sdk/guides/external-api-integrations/index.md#dont-skip-kaltura_genie_experiences-off).

**That setting does not affect `user_properties_forms`.** The two are independent. `kaltura_genie_experiences` governs the widget families like flashcards, summarization, followups, sources and content galleries. `user_properties_forms` is its own config field. With experiences off, your forms still arrive as a `show_widget` call with `kind: "user_properties_form"` or as a `user-properties-form-tool` segment.

## Related docs

| Doc | What it adds |
|-----|---------------|
| [External API Integrations](https://kaltura.github.io/intelligent-agents-sdk/guides/external-api-integrations/index.md) | Wiring a durable write for the values this doc's forms collect |
| [Dynamic Data Injection](https://kaltura.github.io/intelligent-agents-sdk/guides/dynamic-data-injection/index.md) | The opposite direction: feeding data *into* the conversation instead of collecting it |
| [Client-Side Commands](https://kaltura.github.io/intelligent-agents-sdk/guides/client-commands/index.md) | The avatar-driving-your-UI channel, a different silent mechanism from this doc's forms |
| [GenUI Reference](https://kaltura.github.io/intelligent-agents-sdk/reference/genui-reference/index.md) | The full GenUI runtime map this form is one entry in |

