Give Tambo Access to Your Functions
This guide helps you register custom JavaScript functions as tools that Tambo can call to retrieve data or take actions.
This guide shows you how to register custom JavaScript functions as tools so Tambo can call them to retrieve data or take actions in response to user messages.
Prerequisites
- TamboProvider set up in your application
- Zod installed for schema definitions
Step 1: Define your tool function
Create a JavaScript function that performs the action:
const getWeather = async (city: string) => {
const res = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
};Step 2: Create the tool definition
Wrap your function in a TamboTool object with a schema. The description field helps Tambo decide when to use your tool, and .describe() on schema fields helps Tambo understand how to call it with the right parameters.
import { TamboTool } from "@tambo-ai/react";
import { z } from "zod";
export const weatherTool: TamboTool = {
name: "get_weather",
// Clear description of what the tool does and when to use it
description: "Fetch current weather information for a specified city",
tool: getWeather,
// Use .describe() to explain each parameter
inputSchema: z.string().describe("The city to fetch weather for"),
outputSchema: z.object({
location: z.object({
name: z.string(),
}),
}),
};Write clear, specific descriptions so Tambo knows when to call your tool and what values to pass.
Step 3: Register with TamboProvider
Pass your tools array to the provider:
<TamboProvider tools={[weatherTool]}>
<App />
</TamboProvider>Tambo can now call your tool when relevant to user messages.
Return Rich Content (Optional)
To return images, audio, or mixed media instead of plain text, add transformToContent to your tool definition:
const getProductImage = async (productId: string) => {
const product = await fetchProductData(productId);
return {
name: product.name,
description: product.description,
imageUrl: product.imageUrl,
price: product.price,
};
};
export const productTool: TamboTool = {
name: "get_product",
description: "Fetch product information with image",
tool: getProductImage,
inputSchema: z.string().describe("Product ID"),
outputSchema: z.object({
name: z.string(),
description: z.string(),
imageUrl: z.string(),
price: z.number(),
}),
transformToContent: (result) => [
{
type: "text",
text: `${result.name} - $${result.price}\n\n${result.description}`,
},
{
type: "image_url",
image_url: { url: result.imageUrl },
},
],
};Content types: text, image_url, input_audio
Enable Streaming Execution (Optional)
By default, Tambo waits for complete tool arguments before executing. For tools that can handle partial arguments gracefully, you can enable streaming execution to call the tool incrementally as arguments are generated. To understand how streamable execution works under the hood, see Tools > How Tools Execute.
When to Use Streamable Tools
Use annotations.tamboStreamableHint: true when your tool:
- ✅ Can handle incomplete or partial data gracefully
- ✅ Has no side effects (safe to call multiple times)
- ✅ Benefits from incremental execution (state updates, visualizations, real-time feedback)
- ✅ Is idempotent or can merge partial updates safely
When NOT to Use Streamable Tools
Avoid annotations.tamboStreamableHint: true when your tool:
- ❌ Makes API calls or database writes (would cause duplicate requests)
- ❌ Has side effects that shouldn't be repeated
- ❌ Requires complete arguments to function correctly
- ❌ Returns data that the AI needs immediately
Example: Enable Streaming Execution
import { TamboTool } from "@tambo-ai/react";
import { z } from "zod";
const updateChart = (data: { title?: string; values?: number[] }) => {
// Called incrementally as arguments stream in
// Handles partial data gracefully
setChartState((prev) => ({
...prev,
...(data.title && { title: data.title }),
...(data.values && { values: data.values }),
}));
};
export const chartTool: TamboTool = {
name: "update_chart",
description: "Update the chart visualization with new data",
tool: updateChart,
inputSchema: z.object({
title: z.string().optional(),
values: z.array(z.number()).optional(),
}),
outputSchema: z.void(),
annotations: {
tamboStreamableHint: true, // Enable streaming execution
},
};Handling Partial Data
Your streamable tool should handle incomplete data gracefully. Use optional properties and defensive checks:
const updateDashboard = (data: {
title?: string;
metrics?: { name: string; value: number }[];
timeRange?: string;
}) => {
// Only update fields that are present
if (data.title !== undefined) {
setTitle(data.title);
}
if (data.metrics !== undefined) {
setMetrics(data.metrics);
}
if (data.timeRange !== undefined) {
setTimeRange(data.timeRange);
}
};
export const dashboardTool: TamboTool = {
name: "update_dashboard",
description: "Update dashboard with metrics and time range",
tool: updateDashboard,
inputSchema: z.object({
title: z.string().optional(),
metrics: z
.array(z.object({ name: z.string(), value: z.number() }))
.optional(),
timeRange: z.string().optional(),
}),
outputSchema: z.void(),
annotations: {
tamboStreamableHint: true,
},
};This pattern ensures your tool processes data incrementally as the AI generates each field, providing a smooth real-time experience.
Good vs Bad Examples
// ❌ Bad: API call tool - would make duplicate requests
export const createUserTool: TamboTool = {
name: "create_user",
description: "Create a new user account",
tool: async (data) => await api.createUser(data),
inputSchema: z.object({
name: z.string(),
email: z.string(),
}),
outputSchema: z.object({ userId: z.string() }),
annotations: { tamboStreamableHint: true }, // Don't do this!
};
// ✅ Good: State update tool - safe to call multiple times
export const updateFormTool: TamboTool = {
name: "update_form",
description: "Update form fields in real-time",
tool: (fields) => setFormState(fields),
inputSchema: z.object({
name: z.string().optional(),
email: z.string().optional(),
}),
outputSchema: z.void(),
annotations: { tamboStreamableHint: true }, // Safe for repeated calls
};