Overview
Client-event tools let the agent trigger actions in the browser during a live call. When the LLM calls a client_event tool, the worker publishes a JSON message on a LiveKit data-channel topic. Your JavaScript code on the page subscribes to that topic and handles the message — opening a modal, navigating to a page, prefilling a form in your own UI, etc.
This is the escape hatch for anything the widget itself doesn’t natively handle.
Tool definition
{
"name": "open_pricing_page",
"kind": "client_event",
"description": "Navigate the user to the pricing page. Use when the user asks about pricing or wants to see plans.",
"input_schema": {
"type": "object",
"properties": {
"highlight_plan": {
"type": "string",
"description": "Which plan to scroll to (e.g. 'pro', 'enterprise')"
}
},
"required": []
},
"config": {
"topic": "custom.open_pricing"
}
}config reference
| Field | Type | Description |
|---|---|---|
topic | string | LiveKit data-channel topic on which the message is published. Pick a unique name — prefix with your domain (e.g. custom.my_action) to avoid collisions. |
Handling the event in the browser
Subscribe to the room’s data channel after the session starts. The widget exposes the underlying LiveKit Room object at window.__OsharaVoiceWidget?.room, or you can use the LiveKit client SDK directly.
import { Room, RoomEvent } from "livekit-client";
// After connecting to the room...
room.on(RoomEvent.DataReceived, (payload, participant, kind, topic) => {
const message = JSON.parse(new TextDecoder().decode(payload));
if (topic === "custom.open_pricing") {
const plan = message.highlight_plan;
window.location.href = `/pricing${plan ? `?plan=${plan}` : ""}`;
}
});Payload shape
The payload is whatever JSON object the LLM passed as arguments to the tool. For the example above:
{
"highlight_plan": "pro"
}There is no wrapper envelope — the raw arguments object is the payload.
Use cases
| Scenario | Topic | Payload |
|---|---|---|
| Navigate to a page | custom.navigate | { "url": "/pricing" } |
| Open a modal | custom.open_modal | { "modal_id": "upgrade" } |
| Pre-fill your own form | custom.fill_form | { "name": "Alice", "email": "alice@example.com" } |
| Show a product card | custom.show_product | { "product_id": "prod_123" } |
| Trigger a confetti animation | custom.celebrate | {} |
| Log a custom analytics event | custom.analytics | { "event": "demo_interested", "tier": "enterprise" } |
Combining with forms
A common pattern is to use a client-event tool to open your own UI component (not the widget’s built-in form panel), while using a kb or http tool to pre-fetch the data needed to pre-fill it:
[
{
"name": "prefetch_user_details",
"kind": "http",
"description": "Look up the current user's details for pre-filling forms.",
"config": { "url": "https://api.yoursite.com/users/me", "method": "GET" }
},
{
"name": "open_checkout",
"kind": "client_event",
"description": "Open the checkout drawer, optionally pre-selecting a plan.",
"input_schema": {
"type": "object",
"properties": {
"plan": { "type": "string" },
"email": { "type": "string" }
}
},
"config": { "topic": "custom.open_checkout" }
}
]