Skip to main content

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.

How the agent decides

The agent’s LLM picks tools based on three signals:
  1. description — primary signal. Write it as the answer to “when should the model call this?”. Be specific about preconditions.
  2. parameters — JSON Schema. Each property’s description helps the LLM fill in the right value.
  3. name — secondary signal. Use snake_case action verbs: check_availability, create_reservation, cancel_reservation.
Bad description:
Good description:

Required fields

Optional fields

Parameters schema

Use JSON Schema. The agent’s LLM is trained to fill in any standard schema — keep it simple.

Primitive types

Enums

Nested objects

Free-form metadata

When you want to accept arbitrary key/value pairs:
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.

Handler signature

ctx properties

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.
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.

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.
Prefer returning a structured error over throwing when the user can recover:
The LLM sees the alternatives and offers them naturally.

Calling Zavu’s own API from a tool

Every function has a ZAVUDEV_API_KEY env var injected automatically. Use it to call your Zavu account:
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).

Adding npm dependencies

Edit package.json:
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.
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.
If a dependency only ships x86 prebuilds (some image / crypto libs), opt into x86_64 — package.json:
x86 invocations cost 1.25× units vs arm64 (the default). See Runtime → CPU architecture for full pricing impact.

Testing locally

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

Don’t return huge arrays — the LLM has limited context. Filter and trim server-side:
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:
Tools can be called multiple times (LLM retries, user repeats request). Use the tool’s natural keys to dedupe:
For destructive operations, return a confirmRequired payload first:
The LLM will summarize the cancellation and wait for the user to confirm before calling again with confirm: true.

Next

Secrets

Inject DB credentials and API keys.

Restaurant example

Full booking agent walked through.