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).
| Property | Value |
|---|---|
| Topic | form.{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.
| Property | Value |
|---|---|
| Topic | voice.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.
| Property | Value |
|---|---|
| Topic | Whatever 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.
| Property | Value |
|---|---|
| Topic | form.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.
| Property | Value |
|---|---|
| Topic | confirmation_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.
| Property | Value |
|---|---|
| Topic | form.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).
| Property | Value |
|---|---|
| Topic | voice.user_text |
Payload:
{
"type": "user_text",
"text": "Can you reschedule for next Monday instead?"
}Topic reference summary
| Topic | Direction | Description |
|---|---|---|
form.{form-id} | Agent → Widget | Open a form (optionally pre-filled) |
voice.agent_handoff | Agent → Widget | Transfer to a different character |
{custom_topic} | Agent → Widget | Client-event tool output |
form.state | Widget → Agent | Live form field values (250 ms debounce) |
voice.user_text (default) | Widget → Agent | Form submission confirmation or text input |