How-to Guide

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 creation or update, as a list of "stages":

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

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

Form field Behavior
title, secondaryTitle Optional. Stored with the form. secondaryTitle is sent as secondary_title.
id Read-back forms carry an integer id. An id you pass is sent as-is. When you omit it and exactly one stored form has the same callStage, the SDK sends that form's id. On read-back, id is the form's position in the list (0, 1, ...), whatever id you sent.
type One of FORM_FIELD_TYPES, in two groups (below).
Type kind Values
Base types (ARG_TYPES, shared with tool args) str, int, float, bool, list, dict
Form-only types, the default renderer's input hints (see "How the form is rendered" below) email, phone, text

buildUserPropertiesForms() accepts any type in FORM_FIELD_TYPES. It validates before any network call and throws bad_request on an invalid stage, an empty properties array, an unknown type, a non-string title or secondaryTitle, or a bad id.

To remove every form, call mgmt.intellectConfig.clearUserPropertiesForms(configId, adminKs). setUserPropertiesForms with an empty list throws bad_request.

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:

The form arrives with a default heading. 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:

Not possible / not designed for:

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).

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:

The output is a framework-agnostic descriptor. Its shape and limits are in GenUI · Per-Runtime Widget Detail.

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:

It never uses innerHTML, so text supplied by the model can't inject markup. On submit, it calls your opts.onAction('submit', {values, formId}) callback. formId is the descriptor's formId, or null. A non-empty secondaryTitle shows under the title.

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:

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 updateRequestVars() or speak() with the values yourself when the viewer submits (and submitStructuredDataForm(values) if you want the submission recorded).

Reading back what the viewer submitted

session.submitStructuredDataForm(values) records the submission. It emits setFormLeadInfo, returns nothing, and has no acknowledgment. It does not change what the agent knows. To make the agent act on the values, send them in a message with speak(), or as request variables with updateRequestVars() that your prompt reads. The SDK logs one console warning per session the first time you call it. 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.

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'.

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.

Doc What it adds
External API Integrations Wiring a durable write for the values this doc's forms collect
Dynamic Data Injection The opposite direction: feeding data into the conversation instead of collecting it
Client-Side Commands The avatar-driving-your-UI channel, a different silent mechanism from this doc's forms
GenUI Reference The full GenUI runtime map this form is one entry in
Click to talk with Nova — she knows this whole SDK.
Nova AI assistant — knows this whole site

Nova is an AI assistant and can make mistakes. Check important details in the docs. Reloading starts a fresh chat.