Skip to Content

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

FieldTypeDescription
topicstringLiveKit 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

ScenarioTopicPayload
Navigate to a pagecustom.navigate{ "url": "/pricing" }
Open a modalcustom.open_modal{ "modal_id": "upgrade" }
Pre-fill your own formcustom.fill_form{ "name": "Alice", "email": "alice@example.com" }
Show a product cardcustom.show_product{ "product_id": "prod_123" }
Trigger a confetti animationcustom.celebrate{}
Log a custom analytics eventcustom.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" } } ]
Last updated on