Skip to main content
The language model can call tools during a conversation. Tools you register from the client are executed by your code: the server sends a tool_call, you run it and send back the result, and the agent continues with it. Built-in tools (below) run on the server.

How it works

  1. Define tools using the OpenAI function calling schema
  2. Register them when creating the VoiceAgentClient
  3. Respond to the tool_call event, which fires once per call with { name, arguments, call_id } (arguments is a JSON string)
  4. Submit the result with submitToolResult(call_id, result); result may be a string or an object

Defining tools

Handling tool calls

In Python, on_tool_call receives (name, arguments, call_id):

Multi-step tool chains

The agent can chain tool calls — e.g. look up availability, then book it — within a single turn, calling a tool, feeding the result back to the LLM, and calling another tool if the LLM asks for one, before finally speaking a response. Each tool_call gets its own call_id; answer every one.

Built-in tools

Every voice-agent session (browser and phone) automatically has a search_knowledge_base tool available, in addition to whatever you register yourself:
It searches the documents you’ve uploaded to that agent’s knowledge base (scoped to your agent_id) and returns whichever documents’ name or content contain the query terms — a straightforward keyword/substring match, not vector or semantic search, so results are exact-text matches rather than “closest in meaning.” Write knowledge-base documents with the wording your users are likely to ask about, and keep them reasonably short — matching content is truncated to 2000 characters per document in the result the LLM sees. You don’t need to register or implement this tool yourself; it’s handled entirely server-side. It only does anything if the agent has an agent_id and knowledge-base documents configured — otherwise it returns an empty result.

web_search (opt-in)

An agent with Web search turned on in the dashboard (agent → Tools → Built-in, stored as web_search: true in the agent’s config) is also offered a web_search tool:
It runs server-side and returns { "answer": "...", "sources": ["page title", ...] }, a short answer grounded in current search results. The agent is told to treat that answer as information, not instructions. It is off by default. Each search adds a few seconds before the agent answers, and results come from the open web. A call can make at most 3 searches, and each search that returns an answer costs $0.15 from your balance (the search provider bills per search; see Pricing and billing). A search that takes longer than 8 seconds returns an error the agent can apologise for.

Google Calendar (opt-in)

Connect a Google account once (agent → Tools → Google Calendar → Connect Google Calendar; Google asks you to allow access to your calendar, through Composio, the integration service that also runs the CRM connections), then switch it on per agent (google_calendar: true in the agent’s config). The agent is then offered two tools:
  • check_availability { "date": "YYYY-MM-DD" } returns the free start times that day, e.g. { "date": "2026-09-24", "timezone": "Europe/Madrid", "appointment_minutes": 30, "free": ["09:00", "09:30", "11:00"] }.
  • book_appointment { "date", "time": "HH:MM", "caller_name", "caller_phone"?, "caller_email"?, "reason"? } creates the event on your primary calendar and returns { "booked": true, ... }. With caller_email, the caller is sent the invitation, which includes a Google Meet link. If the time was taken in the meantime, it returns { "booked": false, "free": [...] } so the agent can offer another time. The agent reads the email back to the caller before booking. An address that cannot be right (not an address, a made-up subdomain of a mail provider such as [email protected], or a domain that does not exist) stops the booking with { "booked": false, "reason": ... }, and the agent confirms it with the caller instead of sending the invitation to nobody. Addresses the caller dictates (“ana punto ruiz arroba gmail punto com”, “ana dot ruiz at gmail dot com”) appear written out ([email protected]) in the transcript and the collected data.
Set when the agent may book in the dashboard (agent → Tools → Google Calendar), or in the agent’s config: Agents set up before calendar_hours existed use calendar_open / calendar_close for every day of the week, and keep doing so until you edit their hours. The agent only offers a time the calendar shows as free, and checks again right before booking, so it never books over another event, including one added while the call was going on. When a day is full, check_availability also returns next_available (the next days with free times) and the week’s bookable_hours, so the agent can offer alternatives straight away. As the call starts, the agent is also given the free times of the next five bookable days, read from your calendar, so it proposes times that exist rather than guessing a day; it still checks the day again before confirming. Busy times are read from your primary calendar. Disconnecting stops every agent on the account from booking. Both tools run server-side. Like every tool call, they appear in the call’s transcript at the moment the agent used them.

Your CRM (opt-in)

Connect your CRM once (agent → Tools → CRM → Connect a CRM, then pick yours), then switch it on per agent (crm: true in the agent’s config). Supported: HubSpot, Salesforce, Zoho CRM, Zoho Bigin, Attio, Capsule and Intercom (sign in with your CRM account) and Pipedrive, ActiveCampaign, Copper, Fireberry and Salesflare (paste an API key from your CRM’s settings). The connection belongs to your account, so every agent you switch on uses the same CRM. ActiveCampaign cannot store a contact without an email, so a new caller who gives none is not created there. With it on, three things happen on every call:
  • The caller is recognised. When the call starts, the agent looks the caller up by phone number (the caller’s number on an inbound phone call, the number dialled on an outbound one) or by the caller_phone / caller_email a web session passes (see Voice Chat). If the CRM has a record, the agent gets a short summary of it (name, company, email, open deals, the latest note) a few seconds into the call, and uses it instead of asking again. The lookup runs alongside the greeting; it never delays the agent’s first words.
  • A caller calling from another number can still be recognised. The agent is offered one server-side tool, crm_identify_caller { "email"?, "phone"?, "name"? }, for when the caller was not recognised and tells it who they are. It returns { "found": true, "record": "..." } or { "found": false }, and takes a few seconds, so the agent says it is checking.
  • The call is logged. When it ends, the call’s summary, outcome, transcript and everything the agent’s data fields collected are written on the caller’s record, as a call where the CRM has calls and as a note otherwise. Collected values fill in the contact’s name, email, phone, company or job title only where those are empty; nothing the CRM already holds is overwritten. A caller the CRM has never seen is created as a new contact (a lead in Salesforce and Zoho).
Lokutor never deletes or merges anything in your CRM: the tools it may use are a fixed list per CRM, with no delete, merge or archive among them. CRM data given to the agent is marked as data, not instructions. Disconnecting (Integrations → Disconnect) removes the stored connection, and every agent on the account stops using the CRM. Each call that uses your CRM costs $0.04 from your balance, once per call (Pricing and billing): a call on which the caller was looked up or identified, or that was logged. A web session with no caller_phone / caller_email that ends without a transcript never reaches the CRM and adds nothing.

Transfer to a person (opt-in, phone calls)

With Transfer to a person switched on (agent → Tools; transfer_enabled: true and transfer_targets in the agent’s config), the agent on a phone call is offered transfer_call { "reason", "summary" }. It uses it when the caller asks for a person or when it cannot resolve what they need. The caller is put through to the numbers you set, one after another; the person who answers hears the summary first. If nobody answers, the agent is told so and offers to take a message. It runs server-side: you do not register or answer it. Setup, what the caller hears, pricing and every config key: Transfer calls to a person.

Earlier calls (always on, not a tool)

On a phone call, and on a web session that passes caller_phone, the agent is told before its first words what your account’s earlier calls with that number left behind: how many there were and when, the last call’s summary, and what its data fields collected. It greets a returning caller by name and confirms what it already has instead of asking again; if the caller says something different now, what they say now wins. When a call leaves a data field empty that an earlier call filled, the call’s collected data keeps the earlier value, and the dashboard marks it as coming from an earlier call. Only the fields the agent itself collects are carried over.

Date and time (always on, not a tool)

Every dashboard agent (any session with an agent_id) knows the current date and time. They are written into its instructions when the call starts, in the agent’s timezone (agent → Tools → Built-in; timezone in the agent’s config, an IANA name such as America/Mexico_City; default Europe/Madrid). So “tomorrow at ten” or “are you open now” needs no tool call and adds no latency. A session without an agent_id, which sets its own prompt through the SDK, gets nothing added. If you need the date there, put it in your prompt.

Best practices

  • Keep it fast: tool handlers should return within a few seconds. Slow tools leave the agent in a “thinking” state.
  • Clear descriptions: the model relies on description to decide when to use a tool.
  • Handle errors: return an error message as the result; the agent tells the user.
  • Answer every call: a tool_call without a tool_result leaves the agent waiting.

Raw WebSocket protocol

Send tools after connecting:
Server notifies on tool execution:
Submit results: