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.
The file
// src/usher.tools.tsimport { defineTools } from "@voqal/usher-react";export default defineTools({ 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 }) => fetch(`/api/customers/${customer_id}/balance`).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,operate. - 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, or
confirmdoes, 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.
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. Its second argument carries an abortsignalyou can pass to fetch. After a timeout, the assistant says it can’t tell whether the action went through.- 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.
Tools and Usher Pro
Tools work on every key. When a user asks for something, the assistant tries them in this order:
- If one of your app’s tools fits the request, the assistant uses it. This is the fastest and most reliable path.
- With Usher Pro, when no tool fits, it does the task on the page, the way the user would.
- Without Usher Pro, it takes the user to the right page and tells them how to finish.
So tools are worth writing whether or not you have Usher Pro. They cover your most common actions, and Pro Pro covers the long tail. Compare the two in Usher vs Usher Pro.
