defineTool
Give your agent real actions — database lookups, bookings, API calls.
defineTool
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 runtime
and returns the result.
TypeScriptimport { defineTool } from "@zavudev/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, } }, })
How the agent decides
The agent's LLM picks tools based on three signals:
description— primary signal. Write it as the answer to "when should the model call this?". Be specific about preconditions.parameters— JSON Schema. Each property'sdescriptionhelps the LLM fill in the right value.name— secondary signal. Usesnake_caseaction verbs:check_availability,create_reservation,cancel_reservation.
Bad description:
TypeScriptdescription: "Booking stuff"
Good description:
TypeScriptdescription: "Book a confirmed reservation. ONLY call AFTER check_availability has " + "returned the requested slot. Requires customer name."
Required fields
| Field | Type | Notes |
|---|---|---|
name | string | Unique within the agent. snake_case. |
description | string | Tells the LLM when to call this. ≤ 500 chars in practice. |
parameters | JSON Schema (object) | { type: "object", properties, required }. |
handler | async function | (args, ctx) => result. Return any JSON-serializable value. |
Optional fields
| Field | Default | Description |
|---|---|---|
agent | inferred | Only needed if your file defines multiple defineAgents. |
enabled | true | Set false to deploy the tool but keep it disabled. |
Parameters schema
Use JSON Schema. The agent's LLM is trained to fill in any standard schema — keep it simple.
Primitive types
TypeScriptparameters: { type: "object", properties: { orderId: { type: "string", description: "Order ID like ORD-12345." }, quantity: { type: "number", description: "How many units." }, expedited: { type: "boolean", description: "Pay extra for next-day shipping." }, tags: { type: "array", items: { type: "string" } }, }, required: ["orderId"], }
Enums
TypeScript{ type: "string", enum: ["pending", "shipped", "delivered", "cancelled"], description: "Filter by status.", }
Nested objects
TypeScript{ type: "object", properties: { address: { type: "object", properties: { street: { type: "string" }, city: { type: "string" }, zip: { type: "string" }, }, required: ["street", "city", "zip"], }, }, }
Free-form metadata
When you want to accept arbitrary key/value pairs:
TypeScript{ type: "object", description: "Extra metadata, arbitrary keys.", additionalProperties: { type: "string" }, }
Handler signature
TypeScripthandler: async (args, ctx) => { // args: typed by your schema (use `as Type` for safety) // ctx: invocation context — see below return { /* anything JSON-serializable */ } }
ctx properties
| Property | Type | When set |
|---|---|---|
projectId | string | Always. |
functionId | string | Always. |
slug | string | Always. |
messageId | string | When invoked by the agent (not on manual HTTP call). |
contactPhone | string | The phone number of the person writing (E.164), in a live conversation. Empty on a dry run (agents test), which has no contact. The model never sees this number, so read it here rather than asking the model for it. |
sessionId | string | null | If the agent is in a flow session. |
log | function | Equivalent to console.log but tagged for the dashboard logs panel. |
Return value
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.
TypeScriptreturn { 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."
Error handling
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.
TypeScriptdefineTool({ 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:
TypeScriptreturn { 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.
Calling Zavu's own API from a tool
Every function has a ZAVU_API_KEY env var injected automatically. Use it
to call your Zavu account:
TypeScriptimport Zavudev from "@zavudev/sdk" import { defineTool } from "@zavudev/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.
Calling external services
Standard fetch. Functions have unrestricted egress (today).
TypeScriptdefineTool({ name: "lookup_order", description: "Get the latest status of an order from our POS.", parameters: { type: "object", properties: { orderId: { type: "string" } }, required: ["orderId"] }, handler: async ({ orderId }) => { const res = await fetch(`https://pos.example.com/orders/${orderId}`, { headers: { authorization: `Bearer ${process.env.POS_API_KEY}` }, }) if (!res.ok) throw new Error(`POS returned ${res.status}`) return await res.json() }, })
Adding npm dependencies
Edit package.json:
JSON{ "dependencies": { "openai": "^4.20.0", "zod": "^3.22.0" } }
npx zavudev deploy installs them server-side during the bundle step (no local
npm install required). Your function ships as a self-contained bundle with the deps.
Testing locally
terminalnpx zavudev fn invoke --event message.inbound \ --data '{"from":"+14155551234","text":"menú vegano","channel":"whatsapp"}'
This runs your defineFunction handler with a synthetic message event, no
cloud round-trip. Tool calls invoked by the LLM aren't simulated in local
invoke — for that, use the deployed function and npx zavudev fn logs --tail.
Common patterns
Returning a list back to the LLM
Don't return huge arrays — the LLM has limited context. Filter and trim server-side:
TypeScripthandler: async ({ filter }) => { const all = await db.menu.findAll() const filtered = filter === "vegan" ? all.filter(m => m.vegan) : all return { items: filtered.slice(0, 20).map(m => ({ name: m.name, price: `$${m.price}`, vegan: m.vegan })), total: filtered.length, } }
Long-running tasks
Functions have a 30s budget by default (configurable up to 15min). For longer work, return immediately with a job ID and let the LLM follow up:
TypeScriptdefineTool({ name: "start_export", handler: async () => { const jobId = await queue.enqueue({ kind: "export" }) return { started: true, jobId, message: "Started. Use check_export(jobId) to see progress." } }, })
Idempotency
Tools can be called multiple times (LLM retries, user repeats request). Use the tool's natural keys to dedupe:
TypeScripthandler: async ({ orderId, action }) => { const result = await db.actions.upsert({ key: `${orderId}:${action}`, runOnce: () => doTheThing(orderId), }) return result }
Multi-step confirmations
For destructive operations, return a confirmRequired payload first:
TypeScriptdefineTool({ name: "cancel_reservation", handler: async ({ reservationId, confirm }) => { if (!confirm) { const r = await db.reservations.findById(reservationId) return { confirmRequired: true, summary: `Cancel reservation for ${r.customerName} on ${r.date}?`, } } await db.reservations.cancel(reservationId) return { cancelled: true } }, })
The LLM will summarize the cancellation and wait for the user to confirm
before calling again with confirm: true.