Overview

The widget and the voice agent communicate through the LiveKit data channel using topic-keyed JSON messages. Understanding this protocol is useful when:
  • Building custom agent logic that opens or controls forms
  • Implementing client-event tools that the agent fires back to the browser
  • Building your own widget or native-app integration against the same backend

Agent → Widget messages

Messages sent from the agent to the browser widget.

Open a form

The agent sends this to open a form panel (optionally pre-filling fields).
PropertyValue
Topicform.{form-id} (e.g. form.book-demo)
Fallback topics{form-id}.form, any topics defined on the form
Payload:
{
  "field_name": "prefilled_value"
}
The payload keys map to field name values in the form definition. Any matching key is pre-filled. The payload can be empty ({}) to open the form with no pre-fills. Legacy format (also supported):
{ "type": "book_demo_form", "form": { "field_name": "value" } }

Agent handoff

Sent when the agent transfers the session to a different AI character mid-call.
PropertyValue
Topicvoice.agent_handoff
Payload:
{
  "agent_name": "Escalation Agent"
}
The widget displays a transitional screen while the new agent loads.

Client-event tool

When you define a tool with kind: "client_event" and a topic in its config, the agent publishes to that topic. Your page JavaScript can subscribe to the LiveKit room’s data channel and handle the message.
PropertyValue
TopicWhatever config.topic is set to in the tool definition
Payload:
{
  "field1": "value1"
}
The exact payload shape is whatever the LLM decided to pass as arguments to the tool.

Widget → Agent messages

Messages sent from the widget to the agent.

Form state (continuous)

Published every ~250 ms (debounced) while a form is open. Lets the agent track what the user has filled in and guide them verbally.
PropertyValue
Topicform.state
Payload:
{
  "type": "form_state",
  "form_id": "book-demo",
  "is_open": true,
  "step_index": 1,
  "total_steps": 3,
  "values": {
    "first_name": "Alice",
    "email": "alice@"
  },
  "fields": [
    { "name": "first_name", "label": "First name", "type": "text", "required": true },
    { "name": "email",      "label": "Email",       "type": "email","required": true }
  ]
}
The fields array gives the agent the full schema of the current step so it can build contextual prompts (e.g. “It looks like your email address isn’t complete yet.”).

Form submission confirmation

Published once after a successful form submit. Consumed by the agent to continue the conversation.
PropertyValue
Topicconfirmation_topic from form def (default "voice.user_text")
Payload:
{
  "type": "book-demo_submitted",
  "form_id": "book-demo",
  "text": "I have confirmed the form submission.",
  "form": {
    "first_name": "Alice",
    "email": "alice@example.com",
    "date": "2025-06-15"
  }
}

Form submission failure

Published when the form submit HTTP call fails.
PropertyValue
Topicform.state
Payload:
{
  "type": "form_submit_failed",
  "form_id": "book-demo",
  "text": "The form submission failed. Please try again or continue via voice."
}

User text input

Published when the user types a message in the text input box (only visible when textInputEnabled: true in audio preferences).
PropertyValue
Topicvoice.user_text
Payload:
{
  "type": "user_text",
  "text": "Can you reschedule for next Monday instead?"
}

Topic reference summary

TopicDirectionDescription
form.{form-id}Agent → WidgetOpen a form (optionally pre-filled)
voice.agent_handoffAgent → WidgetTransfer to a different character
{custom_topic}Agent → WidgetClient-event tool output
form.stateWidget → AgentLive form field values (250 ms debounce)
voice.user_text (default)Widget → AgentForm submission confirmation or text input