Tools
BetaTools 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.
<Usher> and the assistant can use them.The file
// 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()), },});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, orannouncedoes, and the newestrunis 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.
Describing the input
Write input in whichever of three formats suits your code:
// 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 type | Means |
|---|---|
string | Text |
number | Any number |
integer | A whole number |
boolean | true or false |
date | A date, YYYY-MM-DD |
datetime | An ISO 8601 date and time |
enum:a|b|c | One 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.
| Weak | Strong |
|---|---|
| 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 tool | Create 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.
Tools that open something
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_…, orswitch_…(also after a prefix, likecrm_open_record) counts as opening something for announcements and for stopping. Setkind: "navigation"anyway: it is what tells the assistant to use the tool on its own. - Any other
kindvalue 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.
announce has no effect and tools run without a line first.| Value | What the assistant says |
|---|---|
announce: "Opening {name}." | That line, with the tool’s arguments filled in: “Opening Birch House.” |
announce: true | A short line in the assistant’s own words. Use it to announce a tool that doesn’t open anything. |
announce: false | Nothing. The tool runs straight away. |
| Left out | Tools 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
confirmaren’t announced unless you setannounceon 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:
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
Responsereturned 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.
runhas 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 abortsignalit 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
inputbeforerunis 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:runwith 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.
