Typed tool calls: why our agents don't make things up
Under the hood of our action layer: input schemas, output validation and a handful of hard rules.
If you built with an AI agent in 2024 or 2025, you know the problem: the bot suddenly decides to call a tool that doesn't exist, or grabs a field it invents, or gives a random number as an ID. The fix everyone applies is reinforcement: 'only use these 5 tools', 'IDs are always integers', 'if you're not sure, ask the user'. Works sometimes. Fails sometimes. And you never know in advance which.
At Conveya we chose to design the room for errors away rather than trying to prompt it away. The agent only sees tool calls that are syntactically valid, and can only produce output validated by a schema. Here's how that works.
Step 1: typed tool definitions
Every tool has a JSON schema that defines the input, verified at compile time. We generate them from TypeScript types, no hand-written schemas that drift out of sync with the implementation.
defineTool({
name: "shopify_get_order",
description: "Look up a Shopify order by id, email, or phone.",
input: z.object({
orderId: z.string().regex(/^\d+$/).optional(),
email: z.string().email().optional(),
phone: z.string().regex(/^\+?[0-9 ]+$/).optional(),
}).refine((v) => v.orderId || v.email || v.phone, "one identifier required"),
output: ShopifyOrder, // exhaustive type from the Shopify SDK
exec: async (input, ctx) => { ... },
});This schema is sent to the LLM as a JSON schema in the native tool-use API (Anthropic's tool_use, OpenAI's function_call). So the LLM doesn't get 'there is a tool that looks up orders, figure out what you need', but 'there is a tool with this exact signature, you MUST fill in one of these three fields'. Zod's runtime parser catches the rest.
Step 2: validation before execution
Between the moment the LLM says 'call this tool with these args' and the moment our code runs the tool, there is a validation layer. Three checks:
- Schema validation: does the input satisfy the Zod schema? If not → feedback to the LLM with the validation error, no execution.
- Authorization check: does this agent have permission for this tool? Per agent you can allow or block tools; write tools only get permission if the merchant has explicitly activated them.
- Rate budget check: how many tool calls has this session already made? Prevents looping where the agent keeps looking things up endlessly without engaging the customer.
If one of the three fails, the LLM gets a structured error back. For schema errors with the exact validation message ('expected integer, received string for field orderId'). The LLM corrects itself on its own in 95% of cases on the next turn. The remaining 5%, usually a hallucinated tool that doesn't exist, is blocked.
Step 3: output validation
When the tool itself returns data (a Shopify order, a HubSpot deal), we validate that output too. This looks like overkill, the Shopify SDK is typed after all, but it catches three problems:
- External APIs violate their own schema. We really once got a 'price' field as a string instead of a number from a 'stable' API.
- New fields that confuse the LLM. We strip fields that aren't in our output schema, so the agent gets a focused payload.
- Encoding issues (smart quotes, emoji-broken UTF-8). We normalise to UTF-8 NFC before it lands in the context.
Step 4: tool results as typed context
How the LLM gets the result back is typed as well. Not as a loose JSON string it has to parse, but as a structured tool_result block where the schema keys are guaranteed. This prevents the LLM from hallucinating fields in later turns that aren't in the result.
What this delivers
We measured this in our internal benchmarks (3,000 real support conversations from four merchants):
- Hallucinated tool calls: from 4.2% of turns to 0.1%. The remaining 0.1% are edge cases (the LLM invents a tool name that happens to resemble an existing one).
- Blocked calls: ~12% of all tool calls are stopped by validation. Of those, the LLM corrects 95% on the next turn.
- Time to first tool call: 180ms on average (measured in apps/web/src/lib/integrations/integration-tools.ts). Schema validation itself is sub-millisecond.
- Customer-impact incidents from a wrong tool call: 0 in 6 months of production.
What this does NOT solve
Typed tool calls prevent the agent from carrying out invented actions. They don't prevent it from giving invented ANSWERS based on correctly retrieved data. We have a separate layer for that, output grounding, which I'll cover in a follow-up post.
Keep reading
Automating customer service: where do you start?
Automating customer service starts with your own inbox, not with the tool. Which questions to tackle first, how to measure success, and which beginner mistakes cost you time you did not need to lose.
ReadPlaybookChatbot or AI agent? The difference in plain language
A chatbot follows a decision tree. An AI agent reads along live and looks up the answer. Here is the difference in plain language, plus when you actually need which.
ReadPlaybook5 mistakes when deploying an AI chatbot (and how to avoid them)
Most AI chatbots don't fail because of the technology. They fail because of five choices around it. From giving the bot no access to your data to trying to automate everything at once, these are the traps and how to avoid them.
ReadReady to build this yourself?
Put your own AI agent live on your site, over email, WhatsApp and phone. Start today, no hassle.