18 Aug 2026 · 2 min read
Giving a voice agent tools it can't misuse
A conversational agent is an untrusted caller of your API. Validate every argument, keep results short, and make each tool safe to retry.
It is tempting to think of a voice agent as "your code, but talking". It isn't. A language model decides which tool to call and with what arguments, based on a noisy phone transcript. Treat it like any other untrusted client of your API.
Here is the pattern I use for booking tools.
1. Validate arguments like public input
The model might send "tomorrow" where you expect an ISO date, or omit a field entirely. Parse strictly and return a useful error the agent can recover from:
import { z } from "zod";
const BookArgs = z.object({
dentistId: z.string().min(1),
startsAt: z.string().datetime(),
patientName: z.string().min(2).max(80),
phone: z.string().regex(/^\+?[0-9 ()-]{7,20}$/),
});
export function parseBook(args: unknown) {
const r = BookArgs.safeParse(args);
return r.success
? { ok: true as const, data: r.data }
: { ok: false as const, say: "I didn't catch the time. Could you say the date again?" };
}Notice the failure value is phrased as something the agent can say.
2. Keep tool results tiny and speakable
Don't return a database row. Return one or two sentences:
return { result: "Booked for Tuesday at 3 pm with Dr. Rahman." };Long JSON blobs slow the response and tempt the model to read them aloud.
3. Make every tool safe to retry
Calls can repeat when a network hiccup makes the platform resend. Use the tool-call identifier as an idempotency key so a retry returns the original result instead of creating a second booking:
const key = `tool:${toolCall.id}`;
const cached = await cache.get(key);
if (cached) return cached;
const result = await bookAppointment(args);
await cache.set(key, result, { ttl: 600 });
return result;The exact payload shape varies between platform versions, so check the current docs for where the call id lives, but the principle holds anywhere.
4. Let the database be the final judge
The agent may confidently propose a slot that was taken a second ago. Don't rely on the model to know. Back the tool with a real constraint (see the note on double-booking) and convert violations into polite alternatives.
5. Log the conversation, not just the call
When something goes wrong you want the transcript, the tool arguments and your validation result side by side. Store them together, and you can reproduce almost any bug in minutes.
Summary
- Validate like it is the public internet
- Answer in sentences, not rows
- Idempotent by default
- Constraints in the database, manners in the response
Do those and the agent feels magical while your data stays boring, which is exactly what you want.