Tools

Beta
Last updated  Oct 5, 2026

Tools are your app’s own actions, like looking up a balance or creating a draft invoice, that the assistant can call when a user asks. You write them in one file, in your frontend, as functions that call your own API. No server code and no auth setup.

Available on every key. There is nothing to turn on: pass your tools to <Usher> and the assistant can use them.

The file

src/usher.tools.ts
// src/usher.tools.tsimport { defineTools } from "@voqal/usher-react";export default defineTools({  open_customer: {    description:      "Open a customer's record in the Customers tab. Use when the user asks to open, show, or go to a customer.",    input: {      customer_id: { type: "string", description: "The customer's id from find_customer, e.g. cust_acme" },      name: { type: "string", description: "The customer's name, e.g. Birch House" },    },    kind: "navigation",    announce: "Opening {name}.",    run: ({ customer_id }, { signal }) => openCustomerPanel(customer_id, { signal }),  },  get_customer_balance: {    description:      "Read what a customer owes on open and overdue invoices. Use when the user asks how much a customer owes. Read-only.",    input: { customer_id: { type: "string", description: "The customer's id from find_customer, e.g. cust_acme" } },    run: ({ customer_id }, { signal }) => fetch(`/api/customers/${customer_id}/balance`, { signal }).then((r) => r.json()),  },  create_invoice: {    description:      "Create a draft invoice for a customer. Use when the user asks to bill or invoice a customer for an amount. Nothing is sent to the customer.",    input: { customer_id: "string", amount: "number", due_date: "date?" },    confirm: "Create a draft invoice",    run: (args) => fetch("/api/invoices", { method: "POST", body: JSON.stringify(args) }).then((r) => r.json()),  },});
App.tsx
import tools from "./usher.tools";<Usher voqalKey={key} router={adapter} destinations={map} tools={tools} />
  • The name is snake_case, verb first (create_invoice, get_order_status). It can’t be one of Usher’s own tools: navigate, offer_navigation, clarify, decline_navigation.
  • Keep them in their own file, as above, or write them inline or inside a component. A re-render never restarts the assistant. Only a change to a tool’s name, description, input, confirm, kind, or announce does, and the newest run is always the one called.
  • Up to 32 tools. A handful of well-described ones work better than many vague ones.

Who the tool runs as

run executes in the user’s browser, inside your app, as the logged-in user. It calls your API the same way the rest of your frontend does: the same fetch wrapper, cookie, or token. Voqal never sees, stores, or forwards a token, and there is nothing to configure. Your API’s own permission checks decide what the user may do, just as they do for a click.

Never put a secret or server-only key in this file. It ships to the browser like the rest of your frontend code.

Describing the input

Write input in whichever of three formats suits your code:

input formats
// 1. The short mapinput: {  customer_id: { type: "string", description: "The customer's id, e.g. cust_acme" },  amount: "number",  due_date: "date?",              // ? = optional; date = YYYY-MM-DD  status: "enum:draft|sent",  tags: "string[]?",}// 2. JSON Schemainput: {  type: "object",  properties: { customer_id: { type: "string" }, amount: { type: "number" } },  required: ["customer_id", "amount"],}// 3. zod (the schema your app already has; zod is not a dependency of Usher)input: z.object({ customer_id: z.string(), amount: z.number().positive(), due_date: z.iso.date().optional() })
Short typeMeans
stringText
numberAny number
integerA whole number
booleantrue or false
dateA date, YYYY-MM-DD
datetimeAn ISO 8601 date and time
enum:a|b|cOne of the listed values
string[]A list (of any type above)
type?Optional: the model may leave it out
{ type, description }Any of the above, with a description the model reads

Leave input out for a tool that takes no arguments.

Writing descriptions

The model chooses a tool, and fills in its arguments, from your descriptions alone. Say what the tool does, when to use it (“Use when the user asks …”), and whether it’s read-only or what it changes. Describe every id and amount: what it is, where it comes from, its unit, and an example. If a tool needs an id the user won’t know, add a look-up tool that returns it, like find_customer.

WeakStrong
Gets balance.Read what a customer owes on open and overdue invoices. Use when the user asks how much a customer owes. Read-only.
Invoice toolCreate a draft invoice for a customer. Use when the user asks to bill or invoice someone. Nothing is sent to the customer.
id: "string"customer_id: { type: "string", description: "The customer's id from find_customer, e.g. cust_acme" }

In development builds, the console warns about a description that’s too short, never says when to use the tool, or leaves parameters undescribed. Production builds stay quiet. A tool with an invalid name or no description is left out, with a console error. It never breaks your page.

Asking before it runs

Add confirm: true (the card names the tool), or a short line like confirm: "Create a draft invoice". Usher then shows its confirmation card, and run is called only when the user taps Do it. Use it for anything that creates, changes, sends, or charges. Look-ups don’t need it.

Add kind: "navigation" to a tool that opens or shows something in your app in one call: a tab, a panel, a record, or a view. The assistant then uses that tool alone to open it. It doesn’t also use its built-in navigation, or click through the page, for the same request.

  • Use it when your app has its own way to open a thing that a route alone can’t reach, like a record inside a tab, or a side panel.
  • Pages you list in your destinations still work as before. A navigation tool is for the places in between.
  • A tool named like open_…, show_…, view_…, go_…, goto_…, or switch_… (also after a prefix, like crm_open_record) counts as opening something for announcements and for stopping. Set kind: "navigation" anyway: it is what tells the assistant to use the tool on its own.
  • Any other kind value is ignored, with a console warning, and the tool works as an ordinary one.

A line before it runs

In a voice conversation, the assistant can say a short line before a tool runs, so the user hears what is about to happen instead of watching the page change in silence: “Opening Birch House.” The tool runs as soon as the line starts playing.

Available on request: contact team@voqal.ai to turn on spoken announcements for your account. Until then, announce has no effect and tools run without a line first.
ValueWhat the assistant says
announce: "Opening {name}."That line, with the tool’s arguments filled in: “Opening Birch House.”
announce: trueA short line in the assistant’s own words. Use it to announce a tool that doesn’t open anything.
announce: falseNothing. The tool runs straight away.
Left outTools that open something get a line in the assistant’s own words. Other tools get none.
  • Each {argument} is filled from the call. If a value doesn’t read well out loud, like an id or a long string, the assistant words the line itself instead.
  • Tools with confirm aren’t announced unless you set announce on them: the confirmation card is the announcement.
  • Typed chat never announces. A message gets at most one line, and none if the assistant has already spoken in that reply.

Stopping a tool

run gets an abort signal as part of its second argument. Pass it to your fetch and router calls:

src/usher.tools.ts
run: ({ customer_id }, { signal }) =>  fetch(`/api/customers/${customer_id}`, { signal }).then((r) => r.json()),
  • Usher aborts it when the user interrupts (“stop”, “never mind”) while a tool that opens something is still running. The assistant is then told nothing was done.
  • It is also aborted when the tool times out, when the call is cancelled, and when the session ends.
  • A tool that ignores the signal still finishes in your app, and a page it opens still opens. Pass the signal along so a “stop” really stops.
  • A look-up that is already running when the user interrupts keeps running, and its answer still comes back.

Results and errors

  • Return what the user needs, as plain JSON (usually your API’s response). Results are cut at about 4,000 characters. A fetch Response returned unread reports only its status, so call .json().
  • Throw when something fails. The model gets a short, fixed message, never your error’s text, and tells the user it didn’t work.
  • run has 15 seconds by default. After a timeout, the assistant says it can’t tell whether the action went through. See Stopping a tool for the abort signal it gets.
  • When one message leads to several tool calls, they start in the order the assistant made them. “Open Birch House and set the notes” opens Birch House first. Each call starts without waiting for the one before it to finish, so a slow tool, or a confirmation card the user hasn’t answered, never holds up the next.
  • The assistant reports the result once, in a sentence or two, in voice and in chat.

What Usher checks for you

  • Arguments are checked against input before run is called: types, required fields, enum values, dates, and no extra fields. If any are wrong, the model is told what to fix, and your code never sees them.
  • Your descriptions can’t change the rules. They sit below Voqal’s instructions, in a fenced section. Navigation stays limited to your listed pages, and confirmation can’t be skipped.
  • Results are data, not instructions. The model is told never to follow instructions inside a result. Text that tries to break out of that framing is removed.
  • Not logged. The conversation log records a tool’s name and how the call ended, never its arguments or result. Diagnostics record tool:run with the status and duration only. Arguments and results do go to the AI model provider during the session, like the rest of the conversation. See What leaves the browser.
  • Limits: each message can trigger at most a few tool calls, so the model can’t loop.

When the assistant uses a tool

When a user asks for something, the assistant works through this order:

  • If the answer is on the screen, the assistant reads the page and answers from it.
  • If the user asks to open something, it uses your navigation tool for it when you have one, and otherwise opens that thing’s page. Your other tools are for questions about data that isn’t on screen, and for actions.
  • If one of your app’s tools fits the request, the assistant uses it.
  • Otherwise, it takes the user to the right page and tells them how to finish.

So the tools you write are what let the assistant act for your users. Cover your most common actions first.

© 2026 VoqalVoqal SDK & engine documentation