Overview
Client tools are custom functions that the voice agent can use during the conversation. They are defined and implemented on the client side.
Simple Example
Here’s a complete example with one tool:
const getTimeSchema = {
name: "get_time",
type: "function",
description: "Get the current time",
parameters: {
type: "object",
properties: {},
required: [],
},
};
Step 2: Implement the Function
import { type ClientTool } from "@outspeed/client";
// params & context can be skipped here since this tool doesn't use them
const getTime: ClientTool<{}> = (params, context) => {
return new Date().toLocaleTimeString();
};
The return value from your function is sent directly to the AI model, which uses it to generate its response to the user.
Always return a value - even for action-based tools:
- Data tools (weather, calculations): Return the actual data
- Action tools (generate image, open browser): Return acknowledgment like “Image generated successfully” or “Browser tab opened”
- On failure: Return error description like “Failed to generate image: rate limit error”
This tells the AI whether your tool succeeded or failed. See the implementation section for more details.
const sessionConfig = {
// rest of config...
tools: [getTimeSchema],
};
const conversation = useConversation({
clientTools: {
get_time: getTime,
},
});
That’s it! When the user asks “What time is it?”, the agent will:
- Call your
getTime()
function
- Receive the return value (e.g., “2:30:45 PM”)
- Use that information to respond to the user
Advanced Example with Context
Here’s a more complex tool with typed parameters and context usage:
import { type ClientTool } from "@outspeed/client";
const setTimer: ClientTool<{ time: number; prompt: string }> = ({ time, prompt }, context) => {
setTimeout(() => {
// show a toast if you want to
// toast.info("Timer completed!");
// we let the model know that the timer is done so that it can respond to the user
context.sendText(
`🔔 TIMER ALERT: The timer you set ${time} seconds ago has finished.
Timer prompt: "${prompt}"
Completed at: ${new Date().toLocaleString()}
This is an automated system notification. Please proceed with any actions related to this timer.`,
);
}, time * 1000);
// we set the timer and let the model know that the timer is set
return "Timer set";
};
The context
parameter provides access to conversation methods like sendText()
for sending messages back to the AI after your tool completes.
Tool schemas are OpenAI compatible. See the OpenAI Function Calling guide for more details on defining functions.
{
name: string, // Unique tool identifier
type: "function", // Always "function" for client tools
description: string, // Clear description for the AI
parameters: {
type: "object",
properties: {
[paramName]: {
type: string, // "string", "number", "boolean", "array", "object"
description: string // Parameter description
}
},
required: string[] // Required parameter names
}
}
Best Practices
- Clear descriptions: Help the AI understand when and how to use each tool
- Specific parameters: Define precise parameter types and descriptions
- Single purpose: Each tool should do one thing well
- Predictable naming: Use descriptive, consistent naming conventions
Implementation
Always return meaningful values - the AI uses your return value to respond to the user:
import { type ClientTool } from "@outspeed/client";
// ✅ Good: Return actual data
const getWeather: ClientTool<{ city: string }> = ({ city }, context) => {
return "72°F and sunny in San Francisco";
};
// ✅ Good: Return success confirmation
const sendEmail: ClientTool<{ to: string; subject: string }> = ({ to, subject }, context) => {
// ... send email logic
return `Email sent to ${to}`;
};
// ✅ Good: Return error details
const uploadFile: ClientTool<{ filename: string }> = ({ filename }, context) => {
try {
// ... upload logic
return "File uploaded successfully";
} catch (error) {
return `Upload failed: ${error.message}`;
}
};
// ❌ Bad: Don't return undefined/null
const badTool: ClientTool<{}> = (params, context) => {
// The AI gets nothing to work with
return null;
};
Handle errors gracefully:
const robustTool: ClientTool<{ param: string }> = ({ param }, context) => {
try {
if (!param?.trim()) {
return "Parameter is required";
}
const result = performOperation(param);
// notice that we're returning something that model can use to respond to the user
return result || "Operation completed but no data returned";
} catch (error) {
return `Error: ${error.message}`; // for the model to understand the error
}
};
Use async/await for API calls:
const fetchData: ClientTool<{ query: string }> = async ({ query }, context) => {
try {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
return `Found ${data.results.length} results for "${query}"`;
} catch (error) {
return "Search service unavailable"; // again, for the model to understand what went wrong
}
};
- Cache results: Cache API responses when appropriate
- Timeout handling: Set reasonable timeouts for external calls
- Rate limiting: Respect API rate limits
- Graceful degradation: Provide fallbacks when tools fail
Error Handling
export async function robustToolFunction({ param }: { param: string }) {
try {
// Validate input
if (!param || param.trim() === "") {
return "Parameter is required";
}
// Perform operation
const result = await someApiCall(param);
// Validate result
if (!result) {
return "No data available";
}
return result;
} catch (error) {
console.error("Tool error:", error);
// Return user-friendly error message
if (error instanceof Error) {
return `Error: ${error.message}`;
}
return "An unexpected error occurred";
}
}
You can use client tools alongside system tools:
const sessionConfig = {
// rest of config...
tools: [getTimeSchema],
system_tools: [
{ name: "end_call", enabled: true },
{ name: "skip_turn", enabled: true },
],
};