Skip to main content
New to tool calling? Start with the Tools overview and Tool Calling for LLM. This page focuses on best practices for building reliable tool integrations. Maintaining legacy inline tools? See Legacy inline tool calling.

How Tavus tool calls work

A tool dispatches over exactly one channel, set per tool via the delivery field - see Tool Delivery:
  • App message (default) - the call arrives in your frontend as a conversation.tool_call event; your app runs the logic and sends a conversation.tool_result event back.
  • API call (HTTP) - Tavus makes an HTTPS request to an endpoint you configure; your backend runs the logic and returns the result in the HTTP response.
What the PAL does with the result - speak it, summarize it, absorb it silently, or ignore it - is set by the tool’s on_resolve, independent of which channel delivered the call. App-message flow:
User speaks

The LLM emits a `conversation.tool_call` event (with a tool_call_id)
    ↓ (Tavus → your frontend)
Your app executes the logic (API calls, DB queries, etc.)
    ↓ (your frontend → Tavus)
Your app sends `conversation.tool_result` with the matching tool_call_id
    ↓ (Tavus → user)
The PAL responds per the tool's on_resolve
API (HTTP) flow:
User speaks

The LLM decides to call the tool
    ↓ (Tavus → your backend)
Tavus sends an HTTPS request to your configured URL
    ↓ (your backend → Tavus)
Your endpoint returns the result in a 2xx response body
    ↓ (Tavus → user)
The PAL responds per the tool's on_resolve
With app-message delivery, Tavus does not run the tool for you - your frontend handles conversation.tool_call events and runs your own logic. With API delivery, Tavus calls your endpoint directly, so no frontend handling is needed. Pick one per tool; see Tool Delivery.
Because Tavus agents operate in live conversational environments, tool design should prioritize reliability, clarity, and conversational continuity. Below are the six most important principles for building effective tool integrations.

1. Keep Tool Schemas Clear and Explicit

Tool definitions should be as clear and specific as possible. Ambiguous parameters make it harder for the model to choose and populate tools correctly. Prefer narrow tools with explicit parameters. Bad:
{
  "name": "lookup_customer",
  "parameters": {
    "type": "object",
    "properties": {
      "query": { "type": "string" }
    },
    "required": ["query"]
  }
}
Better:
{
  "name": "lookup_customer_by_email",
  "parameters": {
    "type": "object",
    "properties": {
      "customer_email": { "type": "string" }
    },
    "required": ["customer_email"]
  }
}
Clear schemas reduce incorrect tool usage and improve consistency.

2. Separate Read Tools from Write Tools

Tools generally fall into two categories. Read tools retrieve information and are safe to call frequently. Examples:
  • retrieving account data
  • searching knowledge bases
  • checking order status
Write tools modify system state. Examples:
  • creating support tickets
  • sending emails
  • updating records
Write tools should only run when user intent is clear and parameters are validated.

3. Keep Tool Results Small

Whatever you return - the output of a conversation.tool_result (app message) or the body of your HTTP response (API) - is injected back into the model’s context. Large payloads increase token usage and can degrade conversational quality. Return only the fields needed for the next response:
{
  "message_type": "conversation",
  "event_type": "conversation.tool_result",
  "conversation_id": "<conversation_id>",
  "properties": {
    "tool_call_id": "<id from the tool_call event>",
    "output": "Customer Jane Doe is on the Enterprise plan.",
    "status": "success"
  }
}
This keeps conversations efficient and improves response quality.

4. Avoid Triggering Tools Too Early

Tavus agents operate in real-time conversations where users may interrupt or revise their requests. If a tool executes too early, it may perform the wrong action. The LLM does not truly know intent is clear. You make “intent is clear” operational by defining concrete criteria such as:
  • required slots are present (for example, email, issue_type, etc.)
  • no unresolved ambiguity (for example, “today or tomorrow?”)
  • user gave explicit confirmation for write actions (for example, “yes, submit it”)
Best practice:
  • wait until the user’s intent is clear
  • avoid executing write actions mid-sentence
  • allow the conversation to stabilize before triggering tools

Add the following policy to your PAL’s system prompt to improve tool-call quality and safety:
Tool invocation policy:
- Only call write tools when user intent is explicit and all required parameters are present.
- If any required parameter is missing, ask a follow-up question instead of calling a tool.
- If the user's wording is ambiguous, ask for clarification before calling a tool.
- For irreversible/state-changing actions (create, update, send, submit, charge, delete), require explicit user confirmation immediately before calling the tool.
- Do not call the same write tool repeatedly for the same request unless the user explicitly asks to retry.
- Read-only tools may be called without confirmation when they directly answer the user's request.
- Keep tool results small; summarize succinctly before returning them.

5. Log Tool Calls for Observability

Production systems should log tool activity so issues can be debugged easily. For app-message delivery, listen to Tavus app-events (Daily app-message events) for end-to-end observability. For API delivery, log on your backend. Either way, you can trace the full lifecycle:
  • when the conversation.tool_call (or HTTPS request) fired
  • what payload was received
  • what your app executed
  • what result you returned (the conversation.tool_result output or the HTTP response body)
At minimum, log:
  • conversation_id
  • tool_call_id
  • tool_name
  • parameters
  • execution result
  • status (success / error)
  • timestamp
This helps identify duplicate calls, incorrect parameters, or unexpected behavior during conversations.

6. Return Results Back to the Conversation

After executing a tool call, return the result so the LLM can use it. How you return it depends on the delivery channel; what the PAL does with it is controlled by the tool’s on_resolve.

App-message delivery

The call arrives as a conversation.tool_call event carrying a tool_call_id:
{
  "message_type": "conversation",
  "event_type": "conversation.tool_call",
  "conversation_id": "<conversation_id>",
  "properties": {
    "name": "get_current_time",
    "arguments": "{\"location\": \"New York\"}",
    "tool_call_id": "call_abc123"
  }
}
properties.arguments is a JSON string - parse it to read the parameters. Execute your logic, then send a conversation.tool_result back with the matching tool_call_id:
if (message.message_type === 'conversation' && message.event_type === 'conversation.tool_call') {
  const { name, arguments: rawArgs, tool_call_id } = message.properties;

  // Execute your tool logic
  const result = await executeTool(name, JSON.parse(rawArgs));

  // Return the result, matching the tool_call_id
  call.sendAppMessage({
    message_type: "conversation",
    event_type: "conversation.tool_result",
    conversation_id: message.conversation_id,
    properties: {
      tool_call_id,            // must match the tool_call event
      output: result,          // string passed through; object is JSON-serialized
      status: "success"        // or "error" to have the PAL acknowledge a failure
    }
  }, '*');
}
If your client never sends a result, the dispatch eventually drops out of context - the PAL just won’t have the data.

API (HTTP) delivery

Tavus calls your configured HTTPS endpoint with the tool arguments; you return the result in the response. A 2xx is treated as success and the response body becomes the tool result; non-2xx is marked error. There’s nothing to send back over the data channel - see Tool Delivery for the request/response shapes, retries, and signature verification.

Controlling What the PAL does with the result

Set the tool’s on_resolve (it applies to both channels):
You wantSet on_resolve to
The PAL speaks a natural-language summary of the resultgenerate_response
The PAL speaks the result text verbatimresponse_in_result
Add the result to context silently, with nothing spoken this turnadd_to_context
Don’t wait for or process a result (side-effect-only tools)fire_and_forget
See the on_resolve reference for full details.

Example Implementation

For a complete working example of how to implement tool calling with Tavus, see the official example repository: https://github.com/Tavus-Engineering/tavus-examples/tree/main/examples/cvi-tool-calling This example demonstrates how to:
  • Create tools and attach them to a PAL
  • Listen for conversation.tool_call events
  • Execute backend logic when a tool is triggered
  • Return results with conversation.tool_result