Interactivity

Handle actions, forms, and state in OpenUI components.

OpenUI components can be interactive. The Renderer manages form state automatically and exposes callbacks for actions and persistence.

Actions

When a user clicks a button or follow-up, the component calls triggerAction. The Renderer wraps this into an ActionEvent and fires onAction.

<Renderer
  library={myLibrary}
  response={content}
  onAction={(event) => {
    if (event.type === "continue_conversation") {
      // event.humanFriendlyMessage — button label or follow-up text
      // event.formState — field values at time of click
      // event.formName — scoping form name, if any
    }
  }}
/>

ActionEvent

FieldTypeDescription
typestringAction type (see built-in types below).
paramsRecord<string, any>Extra parameters from the component.
humanFriendlyMessagestringDisplay label for the action.
formStateRecord<string, any> | undefinedRaw field state at time of action.
formNamestring | undefinedForm that scoped the action, if any.

Built-in action types

enum BuiltinActionType {
  ContinueConversation = "continue_conversation",
  OpenUrl = "open_url",
}
  • ContinueConversation — sends the user's intent back to the LLM.
  • OpenUrl — opens a URL in a new tab. Expects params.url.

Using triggerAction in components

Inside defineComponent, use the useTriggerAction hook:

const MyButton = defineComponent({
  name: "MyButton",
  description: "A clickable button.",
  props: z.object({ label: z.string() }),
  component: ({ props }) => {
    const triggerAction = useTriggerAction();
    return <button onClick={() => triggerAction(props.label)}>{props.label}</button>;
  },
});

triggerAction(userMessage, formName?, action?) — the second and third arguments are optional.


Form state

The Renderer tracks field values automatically. Components use useSetFieldValue and useGetFieldValue to read and write state.

Persistence

Use onStateUpdate to persist field state (e.g. to a message in your thread store) and initialState to hydrate it on load.

<Renderer
  library={myLibrary}
  response={content}
  onStateUpdate={(state) => {
    // state is a raw Record<string, any> of all field values
    saveToBackend(state);
  }}
  initialState={loadedState}
/>

onStateUpdate fires on every field change. The state format is opaque — persist and hydrate it as-is.

Field hooks

Use these inside defineComponent renderers:

HookSignatureDescription
useGetFieldValue(formName: string | undefined, name: string) => anyRead a field's current value.
useSetFieldValue(formName: string | undefined, componentType: string | undefined, name: string, value: any, shouldTriggerSaveCallback?: boolean) => voidWrite a field value.
useFormName() => string | undefinedGet the enclosing form's name.
useSetDefaultValue(options: { formName?, componentType, name, existingValue, defaultValue, shouldTriggerSaveCallback? }) => voidSet a default if no value exists.

Validation

Form fields can declare validation rules. The Form component provides a validation context via useFormValidation.

interface FormValidationContextValue {
  errors: Record<string, string | undefined>;
  validateField: (name: string, value: unknown, rules: ParsedRule[]) => boolean;
  registerField: (name: string, rules: ParsedRule[], getValue: () => unknown) => void;
  unregisterField: (name: string) => void;
  validateForm: () => boolean;
  clearFieldError: (name: string) => void;
}

Built-in validators include required, minLength, maxLength, min, max, pattern, and email. Custom validators can be added via builtInValidators.


Next Steps

On this page