Skip to main content
A tool is a function in your code that the LLM can call during a live meeting. You define it once on your account, attach it to one or more scenarios, and the runtime invokes it (over webhook or in the browser) when the LLM emits a tool call. The wire format is identical to OpenAI / Tavus function-calling — port your existing tools with a name change.

When to use a tool

  • Look up a CRM record, calendar slot, or knowledge-base entry mid-call
  • Place an order, book a meeting, or write back to your system
  • Branch the dialogue on real-world state (account tier, inventory, eligibility)
  • Surface UI in the participant’s browser (open a doc, prefill a form)
  • Navigate the visitor around your own site mid-conversation (example)

The Tool object

Example tool object


Execution modes


Conversational modes

A tool call can take 1–8 seconds. Two extra fields control what the persona does during and after the call so the conversation doesn’t go dead.

on_call — what the persona does while the tool runs

static_filler is required when on_call = "static_filler" and forbidden otherwise.

on_resolve — what happens with the result

On Gemini Live, response_in_result falls back to add_to_context — the native-audio API can’t be forced to speak an exact string. The result is still in context for the next turn. Use cloud-TTS providers (OpenAI, ElevenLabs, Cartesia) for true verbatim playback.

How it works at runtime


1. Create a tool

Most people do this in Developer → Tools → New tool in Studio; the API is here for provisioning tools from your own code.
Response — 201 Created:
webhook_secret is never returned — only has_webhook_secret: true. Rotate it any time by PATCHing the tool with a new value; every attached scenario picks up the change instantly.

2. Attach to a scenario

Response — 201 Created (or 200 OK if already attached):

Per-scenario fields


3. Handle the webhook

When the LLM calls the tool, CoreBackend POSTs this payload to your webhook_url:

Request headers

Respond with the result

Return any JSON within timeout_ms. Your response body IS the tool result the LLM sees on its next turn.
  • Non-JSON body → wrapped as { "result": "<your-text>" }
  • Responses larger than 256 KB are truncated
  • Non-2xx response → LLM sees { status: "error", error_code: "http_error", http_status: 500 } and recovers gracefully

Verify the signature

Use the raw request body for HMAC verification. express.json() and similar parsers mutate whitespace and break the signature.

Client-mode tools

Set execution_mode: "client" and omit webhook_url — in Studio, that is Developer → Tools → New tool with Execution mode set to Client. Use it when the action belongs in the participant’s browser rather than on a server — navigating your app, prefilling a form, opening a record, or calling your own API as the user who is already signed in. The point of client mode is where the credentials live. The tool runs inside your page, in the visitor’s existing session, so Waterr never holds your users’ cookies, tokens, or API keys. Your handler is also the authorization boundary: the agent can only invoke tools you registered, and your code decides whether this particular visitor may run them.

The round trip

The persona’s turn stays parked until your handler answers or the timeout fires — it will never narrate an action your page did not perform.

Handling a call

If you embed with the standard loader snippet, register one handler per tool name. That is the whole integration:
Once the loader has landed you can also register directly:
Handlers may be sync or async. Whatever you return becomes the tool result the LLM reads on its next turn, and throwing tells the persona the action failed so it can say so and carry on:
The handler map is your authorization boundary. The agent can only invoke names you registered, and your code decides whether this visitor may run them — check and throw, or show your own confirmation dialog first.
A hard navigation kills the call. window.location.href = ... reloads the document, which destroys the widget’s iframe and tears down the WebRTC session mid-sentence. Use your router’s soft navigation, and mount the widget from a root layout outside the routed subtree so your framework does not unmount it on route change either. If you must hard-navigate, expect the session to drop and reconnect.
Return a page name from an enum in your parameters_schema, not a URL. The model then picks from routes you actually have instead of inventing a 404, and no string from the conversation can be turned into a redirect.

If you embed the iframe yourself

The loader adds ?host=<your origin> to the iframe URL, and the widget refuses to send tool calls to an unidentified origin — a tool call asks your page to act, so broadcasting it to "*" is not an option. Hand-rolling the iframe means adding that yourself and speaking the protocol directly:
Prefer the loader unless you have a reason not to — it handles the origin, the trust gates, call_id matching and error replies for you.

Timing and limits

Client-mode handlers run in a page, so they should return in well under a second. If yours needs to do real work, do it asynchronously: return { status: "started" } immediately and let the persona ask again, or use webhook mode instead.

Which embeds support this

Client-mode tools need a page listening for them, so they work on the sidebar widget only: Attaching a client-mode tool to a scenario used on another surface is not an error, but nothing will answer the call: the persona plays its filler line, waits out timeout_ms, and tells the participant the action did not complete. If a scenario is reused across surfaces, keep its client-mode tools on a variant that only the widget uses.

Example: navigating your site

Attach it to the scenario and the persona can move the visitor around your site mid-conversation, narrating as it goes.

Errors


Limits


API surface

Tools (account-scoped): Scenario attachments:

Worked example — booking a meeting

Two tools (list_slots, book_slot) defined once, attached to both a sales and a support scenario.
During a meeting:
  1. User: “Can we set up a follow-up sometime this week?”
  2. LLM calls list_slots(timezone="Asia/Kolkata").
  3. Your webhook returns [{slot:"2026-06-20T10:00:00+05:30"}, …].
  4. LLM proposes the slots verbally.
  5. User: “Friday at 10 works.”
  6. LLM calls book_slot(slot_iso="2026-06-20T10:00:00+05:30", title="Follow-up").
  7. Your webhook books it and returns { "ok": true, "booking_id": "bk_…" }.
  8. LLM confirms: “Booked — you’ll get a calendar invite shortly.”
No frontend changes, no SDK. The conversation just works.