defineTool declares an action the agent can take. The LLM reads the
description to decide when to call it, and the parameters schema to
decide with what arguments. Your handler runs in the function’s Lambda
and returns the result.
import { defineTool } from "@zavu/functions"defineTool({ name: "check_availability", description: "Check available reservation slots for a date and party size. " + "Call before create_reservation.", parameters: { type: "object", properties: { date: { type: "string", description: "YYYY-MM-DD or 'today', 'tomorrow', 'friday'.", }, partySize: { type: "number", description: "Number of people.", }, }, required: ["date", "partySize"], }, handler: async ({ date, partySize }, ctx) => { // Look up your DB / POS / external API. return { available: true, slots: ["19:00", "21:00"], date, partySize, } },})
The LLM follows the schema strictly. If a field is required, the LLM will
ask the user follow-up questions until it has the value. Use this — it
removes a lot of validation from your handler.
The handler’s return value goes back to the LLM as the tool result. The LLM
includes it in the next message it composes for the customer.
return { confirmed: true, reservationId: "RES-LXXY", summary: "Marco, party of 2, friday at 21:00",}
Becomes (paraphrased) “Listo Marco, reserva RES-LXXY confirmada para 2
personas el viernes a las 21:00.”
The LLM reads field names. Return semantically named fields (confirmed,
summary, eta_minutes) rather than IDs and codes only. The natural-language
answer it generates is better when the structure is self-documenting.
Throwing from a handler returns success: false to the LLM with the message
as the error. The LLM usually translates this into “Sorry, that didn’t work
because…” for the user.
defineTool({ name: "cancel_reservation", ..., handler: async ({ reservationId }) => { const r = await db.reservations.findById(reservationId) if (!r) { throw new Error("Reservation not found. Double-check the ID.") } if (r.status === "completed") { throw new Error("That reservation already happened — nothing to cancel.") } await db.reservations.cancel(reservationId) return { cancelled: true } },})
Prefer returning a structured error over throwing when the user can
recover:
return { error: "slot_taken", message: "21:00 was just booked by someone else.", alternatives: ["19:30", "22:30"],}
The LLM sees the alternatives and offers them naturally.
Every function has a ZAVU_API_KEY env var injected automatically. Use it
to call your Zavu account:
import Zavudev from "@zavudev/sdk"import { defineTool } from "@zavu/functions"const zavu = new Zavudev({ apiKey: process.env.ZAVU_API_KEY!, baseURL: process.env.ZAVU_API_BASE_URL,})defineTool({ name: "send_thankyou", description: "Send a follow-up WhatsApp after a reservation is created.", parameters: { type: "object", properties: { phone: { type: "string" }, customerName: { type: "string" }, }, required: ["phone", "customerName"], }, handler: async ({ phone, customerName }) => { await zavu.messages.send({ to: phone, channel: "whatsapp", text: `Gracias por reservar con nosotros, ${customerName}! 🍕`, }) return { sent: true } },})
The auto-key has messages:send, messages:read, contacts:read scopes.
For other operations, create a project-scoped API key in the dashboard and
inject it as a secret.
zavu deploy installs them server-side during the bundle step (no local
npm install required). Lambda gets a self-contained zip with the deps.
Keep dependencies tight. Each unused package adds cold-start latency and zip
size. The runtime layer already ships @zavudev/sdk, hono, dayjs, zod,
and a few others — declaring them again is unnecessary.
This runs your defineFunction handler with a synthetic message event, no
AWS round-trip. Tool calls invoked by the LLM aren’t simulated in local
invoke — for that, use the deployed function and zavu fn logs --tail.