# Client Tools
Extend your agent with custom capabilities. Tools let the agent take actions beyond just talking.
## Tool Types
| Type | Execution | Use Case |
|------|-----------|----------|
| **Webhook** | Server-side via HTTP | Database queries, API calls, secure operations |
| **Client** | Browser-side JavaScript | UI updates, local storage, navigation |
| **System** | Built-in ElevenLabs | End call, transfer, standard actions |
## Where Tools Live
Tools are defined inside `conversation_config.agent.prompt`. Webhook and client tools go in the `tools` array. System tools go in `built_in_tools`:
```python
conversation_config={
"agent": {
"prompt": {
"prompt": "You are helpful.",
"llm": "gemini-2.0-flash",
"tools": [...], # Webhook and client tools
"built_in_tools": {...} # System tools (end_call, transfer, etc.)
}
}
}
```
## Webhook Tools
Execute server-side logic when the agent needs external data or actions.
### Basic Webhook
```python
agent = client.conversational_ai.agents.create(
name="Weather Assistant",
conversation_config={
"agent": {
"prompt": {
"prompt": "You are a helpful assistant that can check the weather.",
"llm": "gemini-2.0-flash",
"tools": [{
"type": "webhook",
"name": "get_weather",
"description": "Get current weather for a city. Use when user asks about weather.",
"api_schema": {
"url": "https://api.example.com/weather",
"method": "POST",
"request_headers": {
"Authorization": "Bearer {{API_KEY}}"
},
"request_body_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g., 'San Francisco'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units"
}
},
"required": ["city"]
}
}
}]
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}
}
)
```
### Webhook Request Format
When the agent calls a webhook tool, ElevenLabs sends:
```json
{
"tool_call_id": "call_abc123",
"tool_name": "get_weather",
"parameters": {
"city": "San Francisco",
"units": "fahrenheit"
},
"conversation_id": "conv_xyz789"
}
```
### Webhook Response Format
Your server should respond with:
```json
{
"result": "The weather in San Francisco is 68°F and sunny."
}
```
Or for structured data:
```json
{
"result": {
"temperature": 68,
"condition": "sunny",
"humidity": 45
}
}
```
### Webhook with Authentication
```python
# Inside conversation_config.agent.prompt.tools:
{
"type": "webhook",
"name": "lookup_order",
"description": "Look up order status by order ID",
"response_timeout_secs": 10,
"api_schema": {
"url": "https://api.mystore.com/orders/lookup",
"method": "POST",
"request_headers": {
"Authorization": "Bearer {{ORDER_API_KEY}}",
"X-Store-ID": "store_123"
},
"request_body_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order ID (e.g., ORD-12345)"
}
},
"required": ["order_id"]
}
}
}
```
Use workspace environment variables to keep a single server tool configuration working across
staging and production. `{{system_env__label}}` works in server tool URLs, secret environment
variables can populate `request_headers`, and auth-connection environment variables can populate
`api_schema.auth_connection`. The same environment-variable resolution model also applies to MCP
server connections.
```json
{
"api_schema": {
"url": "https://{{system_env__api_host}}.example.com/orders",
"method": "GET",
"request_headers": {
"X-Api-Key": { "env_var_label": "orders_api_key" }
},
"auth_connection": { "env_var_label": "orders_oauth" }
}
}
```
Workspace auth connections support OAuth2 client credentials, OAuth2 JWT, private key JWT,
basic auth, bearer auth, custom header auth, and mutual TLS (`mtls`).
System dynamic variables are also available in tool parameters and headers. Use
`{{system__conversation_history}}` when a webhook or sub-agent needs the full conversation
context as a lazily evaluated JSON history object with user, agent, and tool entries.
### Webhook Tool Options
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `response_timeout_secs` | int | `20` | Timeout in seconds (5-120) |
| `interruption_mode` | string | `"allow"` | Controls whether the user can interrupt around this tool call: `allow`, `disable_during_tool`, or `disable_during_tool_and_turn` |
| `execution_mode` | string | `"immediate"` | `immediate`, `post_tool_speech`, or `async` |
| `tool_call_sound` | string | - | Sound during execution: `typing`, `elevator1`-`elevator4` |
| `pre_tool_speech` | string | `"auto"` | Controls whether the agent speaks before execution: `auto`, `force`, or `off` |
| `tool_error_handling_mode` | string | `"auto"` | `auto`, `summarized`, `passthrough`, or `hide` |
| `api_schema.response_filter` | object | - | Filters JSON webhook responses before the LLM sees them. Use `mode: "allow"` with `filters` dot-paths to keep selected fields, or `mode: "hide_all"` to hide the response |
MCP server configuration supports the same `pre_tool_speech`, `interruption_mode`, `execution_mode`, and
`response_timeout_secs` controls at the server level, with per-tool overrides in
`tool_config_overrides`. Set a per-tool `tool_call_sound` override to `"off"` to silence that tool
while retaining the server default for other tools. MCP timeouts default to 30 seconds and must be
5-300 seconds.
Set `request_meta` on an MCP server configuration to send entries in the MCP `_meta` field of
`tools/call` requests. Values can be JSON scalars or references to workspace secrets, dynamic
variables, or environment variables that resolve for each call.
**Note:** The default `api_schema.method` is `GET`. Always set `"method": "POST"` explicitly for webhook tools that send request bodies.
### Server Implementation (Node.js)
```javascript
app.post("/webhook/get_weather", async (req, res) => {
const { parameters, conversation_id } = req.body;
const { city, units = "fahrenheit" } = parameters;
// Fetch weather from your data source
const weather = await weatherService.get(city, units);
res.json({
result: `It's ${weather.temp}°${units === "celsius" ? "C" : "F"} and ${weather.condition} in ${city}.`,
});
});
```
### Server Implementation (Python)
```python
@app.post("/webhook/get_weather")
async def get_weather(request: Request):
data = await request.json()
city = data["parameters"]["city"]
units = data["parameters"].get("units", "fahrenheit")
# Fetch weather from your data source
weather = weather_service.get(city, units)
return {
"result": f"It's {weather['temp']}°{'C' if units == 'celsius' else 'F'} and {weather['condition']} in {city}."
}
```
## Client Tools
Execute JavaScript in the user's browser. Useful for UI updates, navigation, or accessing browser APIs.
### Defining Client Tools
Client tools are registered when starting a conversation:
```javascript
import { Conversation } from "@elevenlabs/client";
const conversation = await Conversation.startSession({
agentId: "your-agent-id",
clientTools: {
show_product: async ({ productId }) => {
// Update UI to show product
const modal = document.getElementById("product-modal");
modal.innerHTML = await fetchProductCard(productId);
modal.showModal();
return { success: true, message: "Showing product" };
},
navigate_to: async ({ page }) => {
// Navigate to a page
window.location.href = `/${page}`;
return { success: true };
},
save_preference: async ({ key, value }) => {
// Store in localStorage
localStorage.setItem(key, value);
return { saved: true };
},
},
});
```
### React Registration with `useConversationClientTool`
When you use the React SDK, wrap your component tree in `ConversationProvider` and register
client tools from components with `useConversationClientTool`. Handlers are cleaned up
automatically on unmount and always use the latest closure value. Prefer granular hooks such as
`useConversationControls` and `useConversationStatus` for the session UI; `useConversation`
remains available when you want the full conversation object in one hook.
```typescript
import {
ConversationProvider,
useConversationClientTool,
useConversationControls,
useConversationStatus,
} from "@elevenlabs/react";
function Storefront() {
useConversationClientTool("show_product", async ({ productId }) => {
const modal = document.getElementById("product-modal");
modal.innerHTML = await fetchProductCard(productId);
modal.showModal();
return { success: true };
});
const { startSession, endSession } = useConversationControls();
const { status } = useConversationStatus();
if (status === "connected") {
return ;
}
return (
);
}
function App() {
return (
);
}
```
### Registering Client Tools with Agent
Tell the agent about available client tools in `conversation_config.agent.prompt.tools`:
```python
agent = client.conversational_ai.agents.create(
name="Shopping Assistant",
conversation_config={
"agent": {
"prompt": {
"prompt": """You are a shopping assistant.
When users want to see a product, use show_product.
When users want to go somewhere, use navigate_to.""",
"llm": "gemini-2.0-flash",
"tools": [
{
"type": "client",
"name": "show_product",
"description": "Display a product card to the user",
"parameters": {
"type": "object",
"properties": {
"productId": {
"type": "string",
"description": "Product ID to display"
}
},
"required": ["productId"]
}
},
{
"type": "client",
"name": "navigate_to",
"description": "Navigate user to a different page",
"parameters": {
"type": "object",
"properties": {
"page": {
"type": "string",
"enum": ["cart", "checkout", "account", "home"],
"description": "Page to navigate to"
}
},
"required": ["page"]
}
}
]
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}
}
)
```
### Client Tool Options
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `expects_response` | bool | `false` | Whether the tool returns data to the agent |
### Client Tool Return Values
Return data that the agent can use in conversation:
```javascript
clientTools: {
check_cart: async () => {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
return {
itemCount: cart.length,
total: cart.reduce((sum, item) => sum + item.price, 0),
items: cart.map((item) => item.name),
};
};
}
```
The agent receives this data and can say: "You have 3 items in your cart totaling $45.99."
## System Tools (built_in_tools)
Built-in tools provided by ElevenLabs. These are configured in `conversation_config.agent.prompt.built_in_tools` (not in the `tools` array):
```python
"built_in_tools": {
"end_call": {},
"transfer_to_number": {...},
"transfer_to_agent": {...},
"language_detection": {},
"skip_turn": {},
"voicemail_detection": {...},
"play_keypad_touch_tone": {}
}
```
Current API schemas also expose `agent_prompt_change`, `memory_entry_create`, `memory_entry_delete`, `memory_entry_search`, and `memory_entry_update` in `built_in_tools`.
Set `only_at_conversation_start: true` on the `language_detection` tool to constrain language
switching when no switch occurs in the first two user turns. Leave it `false` when the conversation
must remain able to switch languages later.
### end_call
Ends the current conversation:
```python
"built_in_tools": {
"end_call": {}
}
```
The agent can say "Goodbye!" and then end the call programmatically.
### transfer_to_number
Transfer to a phone number (requires telephony integration):
```python
"built_in_tools": {
"transfer_to_number": {
"transfers": [{
"transfer_destination": {"type": "phone", "phone_number": "+1234567890"},
"condition": "User asks to speak with a human agent"
}]
}
}
```
### transfer_to_agent
Transfer to another ElevenLabs agent or workflow node:
```python
"built_in_tools": {
"transfer_to_agent": {
"transfers": [{
"agent_id": "other-agent-id",
"node_id": "destination-workflow-node-id",
"preserve_client_tts_overrides": true,
"condition": "User asks about sales"
}]
}
}
```
Use `node_id` when the transfer should start at a specific workflow node. Omit
`agent_id` when the transfer stays within the current agent's workflow.
Set `preserve_client_tts_overrides` when client-side TTS overrides should continue
after the transfer.
## Best Practices
### Tool Descriptions
Write clear descriptions so the LLM knows when to use tools:
```python
# Good - specific and actionable
"description": "Look up order status. Use when customer asks about their order, delivery, or shipping."
# Bad - vague
"description": "Order tool"
```
### Parameter Descriptions
Help the LLM extract correct values:
```python
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order ID in format ORD-XXXXX (e.g., ORD-12345)"
},
"email": {
"type": "string",
"description": "Customer email address for verification"
}
}
}
```
For optional tool parameters that should never be sent in the request payload, set
`is_omitted: true` on the JSON schema property. Do not combine it with `description`,
`dynamic_variable`, `is_system_provided`, or `constant_value`.
For an LLM-supplied parameter that must match a runtime list, set
`allowed_values: {"dynamic_variable": "allowed_ids"}`. The dynamic variable must resolve to a
JSON array. Use `allowed_values` only with a `description`-sourced property; do not combine it with
`dynamic_variable`, `is_system_provided`, `constant_value`, or `is_omitted`.
### Error Handling
Configure how tool errors are shared with the agent using `tool_error_handling_mode`:
| Mode | Behavior |
|------|----------|
| `auto` | ElevenLabs automatically decides how to handle errors |
| `summarized` | Errors are summarized before being sent to the agent |
| `passthrough` | Full error details are passed to the agent |
| `hide` | Errors are hidden from the agent |
Return helpful error messages:
```javascript
// Server webhook
app.post("/webhook/lookup_order", async (req, res) => {
const { order_id } = req.body.parameters;
const order = await db.orders.find(order_id);
if (!order) {
return res.json({
result: {
error: true,
message: `Order ${order_id} not found. Please verify the order ID.`,
},
});
}
res.json({ result: order });
});
```
### Timeouts
Set reasonable timeouts for webhooks using `response_timeout_secs` (5-120 seconds, default 20). MCP server tool calls use the same field with a 30-second default and a 5-300 second range:
```python
{
"type": "webhook",
"name": "slow_operation",
"description": "Run a slow operation",
"response_timeout_secs": 30,
"api_schema": {
"url": "https://api.example.com/slow-operation",
"method": "POST"
}
}
```
## Complete Example
```python
agent = client.conversational_ai.agents.create(
name="E-commerce Assistant",
conversation_config={
"agent": {
"first_message": "Hi! How can I help you today?",
"language": "en",
"prompt": {
"prompt": """You are an e-commerce support assistant.
Available actions:
- lookup_order: Check order status
- show_product: Display products to customer
- end_call: End conversation politely
- transfer_to_number: Transfer to human support
Always verify order ID before lookup. Offer transfer for complex issues.""",
"llm": "gemini-2.0-flash",
"tools": [
# Webhook: Server-side order lookup
{
"type": "webhook",
"name": "lookup_order",
"description": "Look up order status by order ID or email",
"api_schema": {
"url": "https://api.mystore.com/orders/lookup",
"method": "POST",
"request_headers": {"Authorization": "Bearer {{API_KEY}}"},
"request_body_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"email": {"type": "string"}
}
}
}
},
# Client: Browser-side product display
{
"type": "client",
"name": "show_product",
"description": "Display product details to the customer",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"}
},
"required": ["product_id"]
}
}
],
"built_in_tools": {
"end_call": {},
"transfer_to_number": {
"transfers": [{
"transfer_destination": {"type": "phone", "phone_number": "+1234567890"},
"condition": "User asks for human support"
}]
}
}
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb", "model_id": "eleven_flash_v2_5"}
}
)
```