Overview
Client tools allow your AI persona to trigger events in your client application. When the LLM invokes a client tool, an event is sent via WebSocket to your application, enabling the persona to:
- Navigate to specific pages or sections
- Open modals, dialogs, or overlays
- Update UI state based on conversation
- Trigger animations or visual effects
- Control media playback
- Submit forms or initiate actions
This creates a seamless voice-driven or chat-driven user experience where the AI can guide users through your application.
Beta Feature: Tool calling is currently in beta. You may encounter some
issues as we continue to improve the feature. Please report any feedback or
issues to help us make it better.
User makes a request
User: “Show me the pricing page”
LLM decides to call tool
The LLM recognizes this requires a client-side action and generates a tool call:{
"name": "navigate_to_page",
"arguments": {
"page": "pricing"
}
}
Event sent to client
The Anam engine sends a tool_call event via WebSocket to your client
application.
Your app handles the event
Your event handler receives the tool call and executes the action:if (toolName === "navigate_to_page") {
window.location.href = `/${arguments.page}`;
}
LLM continues conversation
The persona confirms: “I’ve opened the pricing page for you!”The user experiences a seamless interaction between voice/chat and UI.
A client tool requires four components:
Must be "client" for client-side tools
Unique identifier for the tool (snake_case, 1-64 characters)
Describes when the LLM should use this tool (helps with decision-making)
JSON Schema defining the parameters the tool accepts
Example: Page Navigation
{
"type": "client",
"name": "navigate_to_page",
"description": "Navigate to a specific page when user asks to see pricing, features, documentation, or other sections",
"parameters": {
"type": "object",
"properties": {
"page": {
"type": "string",
"description": "The page to navigate to (pricing, features, docs, contact)",
"enum": ["pricing", "features", "docs", "contact", "dashboard"]
},
"section": {
"type": "string",
"description": "Optional section anchor to scroll to"
}
},
"required": ["page"]
}
}
Example: Open Modal
{
"type": "client",
"name": "open_modal",
"description": "Open a modal dialog when user wants to perform an action like checkout, signup, or view details",
"parameters": {
"type": "object",
"properties": {
"modalType": {
"type": "string",
"description": "The type of modal to open",
"enum": ["checkout", "signup", "login", "contact", "product_details"]
},
"data": {
"type": "object",
"description": "Additional data to pass to the modal",
"properties": {
"productId": { "type": "string" },
"userId": { "type": "string" }
}
}
},
"required": ["modalType"]
}
}
Example: Update UI State
{
"type": "client",
"name": "update_filters",
"description": "Update product filters when user describes what they're looking for",
"parameters": {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Product category"
},
"priceRange": {
"type": "object",
"properties": {
"min": { "type": "number" },
"max": { "type": "number" }
}
},
"inStock": {
"type": "boolean",
"description": "Only show in-stock items"
}
}
}
}
Setting Up Event Listeners
import { AnamClient } from '@anam-ai/js-sdk';
const client = new AnamClient('YOUR_SESSION_TOKEN');
// Listen for tool calls
anamClient.addListener(AnamEvent.CLIENT_TOOL_EVENT_RECEIVED, (event) => {
const { eventName, eventData } = event;
switch(eventName) {
case 'navigate_to_page':
handleNavigation(eventData.page);
break;
case 'open_modal':
openModal(eventData.modalType, eventData.data);
break;
}
});
// Start the session
await client.streamToVideoElement('video-element-id');
See the complete SDK
Reference for all
available events and methods.
Real-World Examples
Create a shopping assistant that can guide users through your product catalog:
const tools = [
{
type: "client",
name: "show_product",
description: "Display a product when user asks about specific items",
parameters: {
type: "object",
properties: {
productId: { type: "string" },
productName: { type: "string" },
},
required: ["productId"],
},
},
{
type: "client",
name: "add_to_cart",
description: "Add a product to cart when user wants to purchase",
parameters: {
type: "object",
properties: {
productId: { type: "string" },
quantity: { type: "number", default: 1 },
},
required: ["productId"],
},
},
{
type: "client",
name: "apply_filter",
description: "Filter products when user describes preferences",
parameters: {
type: "object",
properties: {
category: { type: "string" },
maxPrice: { type: "number" },
inStock: { type: "boolean" },
},
},
},
{
type: "client",
name: "open_checkout",
description: "Open checkout when user is ready to purchase",
parameters: {
type: "object",
properties: {},
},
},
];
// Event handler
function handleToolCall(toolName, args) {
switch (toolName) {
case "show_product":
router.push(`/products/${args.productId}`);
break;
case "add_to_cart":
cart.addItem(args.productId, args.quantity);
showNotification(`Added ${args.quantity}x to cart`);
break;
case "apply_filter":
productList.filter(args);
break;
case "open_checkout":
router.push("/checkout");
break;
}
}
Example conversation:
- User: “Show me wireless headphones under $100”
- AI: Calls
apply_filter with category: “headphones”, maxPrice: 100
- User: “I like the Sony ones”
- AI: Calls
show_product with productId: “sony-wh-1000xm4”
- User: “Add them to my cart”
- AI: Calls
add_to_cart “Added to your cart! Ready to checkout?”
Customer Support Dashboard
Create a support agent that can navigate your dashboard:
const tools = [
{
type: "client",
name: "show_ticket",
description: "Display a support ticket when user mentions a ticket number",
parameters: {
type: "object",
properties: {
ticketId: { type: "string" },
},
required: ["ticketId"],
},
},
{
type: "client",
name: "open_chat",
description:
"Open live chat with a human agent when issue needs escalation",
parameters: {
type: "object",
properties: {
reason: { type: "string", description: "Why escalating to human" },
},
},
},
{
type: "client",
name: "show_analytics",
description:
"Show analytics dashboard when user asks for metrics or reports",
parameters: {
type: "object",
properties: {
dateRange: { type: "string", enum: ["today", "week", "month"] },
},
},
},
];
SaaS Application Navigator
const tools = [
{
type: "client",
name: "navigate_to_feature",
description: "Navigate to a specific feature or section of the application",
parameters: {
type: "object",
properties: {
feature: {
type: "string",
enum: [
"dashboard",
"analytics",
"settings",
"billing",
"team",
"integrations",
],
},
},
required: ["feature"],
},
},
{
type: "client",
name: "create_new",
description:
"Open creation modal for new items (project, user, campaign, etc.)",
parameters: {
type: "object",
properties: {
itemType: {
type: "string",
enum: ["project", "campaign", "user", "report"],
},
prefill: {
type: "object",
description: "Data to prefill in the creation form",
},
},
required: ["itemType"],
},
},
{
type: "client",
name: "run_export",
description: "Export data when user requests a download",
parameters: {
type: "object",
properties: {
exportType: { type: "string", enum: ["csv", "pdf", "json"] },
dataType: { type: "string" },
},
},
},
];
Best Practices
Provide Clear Descriptions
The description helps the LLM decide when to use the tool:
// ✅ Good - Specific about when to use
{
name: 'open_checkout',
description: 'Open the checkout page when user explicitly says they want to purchase, buy, checkout, or complete their order'
}
// ❌ Bad - Too vague
{
name: 'open_checkout',
description: 'Opens checkout'
}
Use Enums for Constrained Values
When parameters have a limited set of valid values, use enums:
{
parameters: {
type: 'object',
properties: {
page: {
type: 'string',
enum: ['home', 'pricing', 'features', 'contact'],
description: 'The page to navigate to'
}
}
}
}
This prevents the LLM from generating invalid values.
Handle Errors Gracefully
Always validate tool arguments in your event handler:
function handleToolCall(toolName, args) {
try {
switch (toolName) {
case "show_product":
if (!args.productId) {
console.error("Missing productId");
return;
}
// Validate product exists
if (!productExists(args.productId)) {
showNotification("Product not found");
return;
}
router.push(`/products/${args.productId}`);
break;
default:
console.warn("Unknown tool:", toolName);
}
} catch (error) {
console.error("Tool execution error:", error);
showNotification("Something went wrong");
}
}
Provide User Feedback
Give immediate feedback when tools execute:
case 'add_to_cart':
cart.addItem(args.productId, args.quantity);
// Visual feedback
showNotification(`Added ${args.quantity}x to cart`, 'success');
// Update cart icon with animation
cartIcon.classList.add('bounce');
setTimeout(() => cartIcon.classList.remove('bounce'), 300);
break;
Use browser console to debug tool calls:
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === "tool_call") {
console.group("🔧 Tool Call");
console.log("Tool:", message.toolName);
console.log("Arguments:", message.arguments);
console.log("Timestamp:", new Date().toISOString());
console.groupEnd();
handleToolCall(message.toolName, message.arguments);
}
};
Security Considerations
Validate All Parameters
Never trust client tool arguments without validation:
case 'navigate_to_page':
// ✅ Validate against whitelist
const validPages = ['home', 'pricing', 'features', 'contact'];
if (!validPages.includes(args.page)) {
console.error('Invalid page:', args.page);
return;
}
window.location.href = `/${args.page}`;
break;
Avoid Exposing Sensitive Data
Don’t include sensitive information in tool parameters:
// ❌ Bad - Exposes sensitive data
{
name: 'show_user_profile',
parameters: {
userId: { type: 'string' },
email: { type: 'string' },
creditCard: { type: 'string' } // Never expose this!
}
}
// ✅ Good - Only IDs, fetch sensitive data server-side
{
name: 'show_user_profile',
parameters: {
userId: { type: 'string' }
}
}
Prevent abuse by tracking and limiting tool execution:
const toolCallCounts = {};
const MAX_CALLS_PER_MINUTE = 20;
function handleToolCall(toolName, args) {
// Track calls
const now = Date.now();
toolCallCounts[toolName] = toolCallCounts[toolName] || [];
toolCallCounts[toolName].push(now);
// Remove old entries
toolCallCounts[toolName] = toolCallCounts[toolName].filter(
(time) => now - time < 60000
);
// Check limit
if (toolCallCounts[toolName].length > MAX_CALLS_PER_MINUTE) {
console.warn("Too many tool calls");
return;
}
// Execute tool...
}
Next Steps