Skip to main content

defineAgent

defineAgent declares the AI agent that will run on a specific WhatsApp / SMS sender. Calling it inside your function source binds the agent to that sender on every npx zavudev deploy.
After npx zavudev deploy:
  • The agent is created (or updated) under that sender.
  • Tagged as managed by this function — the dashboard disables manual edits to prevent drift.
  • Marked enabled: true automatically.

Required fields

Optional fields

Voice

Add a voice block to make the agent answer calls. The LLM runs co-located in the voice network for the lowest latency, independent of the model used for text. npx zavudev deploy provisions everything; removing the block turns the agent back into text-only.
Voice requires the Voice Agents feature enabled for your team and a phone number assigned to the sender. The fastest way to start is to pull a ready-made voice agent: see Factory agents.

Knowledge base

Documents the agent answers from, declared with the agent. Retrieval runs before every reply.
The declaration is the source of truth, like everything else here. On deploy:
  • a document you added is created and embedded
  • a document whose content changed is re-embedded
  • a document you removed is deleted
  • a document that did not change is left alone, so a redeploy costs nothing
The deploy tells you which happened:

Titles are identity

Documents are matched by title. Renaming one replaces it: the old document is deleted and a new one embedded. That is usually what you want, but it is a re-embed rather than an edit.

Writing documents that retrieve

Retrieval is similarity over chunks, not keyword search.
  • Headings and short paragraphs chunk cleanly. A wall of text produces chunks that each hold half an answer.
  • Use the customer’s words. A document that says “return merchandise authorization” does not retrieve for “can I send it back”.
  • State facts. “14 days” retrieves and answers. “within a reasonable period” retrieves and says nothing.
  • One base per subject. Mixing the refund policy with the API reference retrieves from both and dilutes the context.

Check that it is actually being used

The reply comes with knowledge chunk(s) used. Zero on an agent that has documents means the answer was not grounded in them, which is otherwise indistinguishable from a correct answer.

The size limit is your source file

Documents live inline, and the deploy caps source at about 900KB. A knowledge base larger than that belongs on the API:
Both paths write to the same place. Declaring in code is better when the documents belong with the agent and change with it; the API is better when they are large, or maintained by someone who does not deploy.

Flows

defineFlow declares a deterministic conversation: the steps you wrote, in the order you wrote them. Reach for it where the model must not improvise, and leave the rest of the conversation to the agent.
The step shapes, the validation types and what each trigger does are in the flows guide. Three things are specific to declaring one in code:

A new flow arrives disabled

A flow intercepts real conversations, so writing one and turning it on are separate decisions. Pass enabled: true to have the first deploy enable it, or turn it on afterwards:
Later deploys never change enabled. Pausing a flow from the dashboard during an incident has to survive a redeploy.

The tool a step calls must exist on the agent

Declaring it with defineTool in the same file is enough, because tools reconcile before flows. A step naming a tool the agent does not have is reported and that flow is skipped; the rest of the deploy still lands.

The declaration is the source of truth, with two exceptions

A flow you stop declaring is deleted on the next deploy. Two things are left alone:
  • A flow this function did not create. One built in the dashboard with the same name is not adopted and not overwritten. The deploy says so.
  • A flow someone is standing in. If a contact is mid-conversation inside it, it is kept and removed on a later deploy, rather than stranding them.

Providers

No API key needed. LLM costs are billed directly from your Zavu balance at pass-through rates.
Available models on the gateway:

Bring your own key (BYOK)

Pass provider matching the vendor and apiKey (or pre-create a secret in the dashboard and rely on it being already stored).
Models follow each vendor’s own naming (gpt-4o-mini, claude-3-5-sonnet-20241022, gemini-1.5-flash, etc) — no provider/ prefix when using BYOK.
On first deploy, if you pass apiKey, we create a row in apiSecrets encrypted with AES-256-GCM and reference it from the agent. Subsequent deploys without apiKey reuse the same stored secret. To rotate, pass a new apiKey and redeploy.

Prompts

The prompt field is the system message every conversation starts with. It’s where you set the persona, rules, and guardrails.

Patterns that work

Anti-patterns

Don’t put prices, menu items, or any data that changes in the prompt. Put them in tool handlers so they stay current without redeploys.Don’t make the prompt longer than ~2,000 characters. LLMs lose focus on the rules with verbose prompts; tools are how you scope behavior.

Triggers (which messages reach the agent)

By default the agent fires on every inbound message to its sender. Restrict with channels and messageTypes:
If you need fine-grained event triggers (e.g. only when a specific sender fires message.inbound), use explicit triggers in addition.

Several files

Split the agent across as many files as it needs. Imports between them are resolved when you deploy:
npx zavudev deploy starts at your entrypoint (index.ts by default) and uploads every file it reaches through relative imports. Files nothing imports stay on your machine, so tests and scratch work are not deployed. npm packages are not part of that tree: declare them under dependencies in package.json and they are installed and bundled.
A few paths are refused: anything above the project root (../shared/x.ts), node_modules/, and names starting with __zavu. A project can hold up to 200 files totalling 900,000 bytes. If you import something from outside the project directory, either move it inside or publish it as an npm package. The dashboard editor works the same way — add files in the sidebar and import between them. To deploy straight from a repository instead, link it:
Every push to that branch redeploys.

One agent per file

defineAgent is allowed multiple times only when each call has a different (senderId, name) pair, in which case each tool must explicitly bind to one:
For most use cases you want one agent per function file. Multiple senders sharing identical logic? Use the same code in multiple functions, each with its own SENDER_ID secret.

Updates and ownership

Every npx zavudev deploy reconciles the live agent to match what’s in the code. When you remove defineAgent from your code and redeploy, the managed agent is deleted. Manual agents are never touched.
Deleting the agent takes everything attached to it, and that is wider than the rest of this table: its managed tools and flows, and every knowledge base on that agent, including one you built in the dashboard, with its documents.Not an oversight. A knowledge base belongs to one agent, and every route resolves the agent before it. Once the agent is gone nobody can list, read or delete what was attached to it, so leaving it behind hides customer content instead of preserving it.If a knowledge base has to outlive the agent, move it to another one before you stop declaring the agent. The same applies to deleting the whole function.

Disabling without deleting

To pause an agent without removing the code:
Redeploy. The agent stays in the database; messages to its sender will be processed by webhooks instead.

Common patterns

Don’t hard-code a language. Tell the prompt to match the user:
The LLM handles language detection per turn — no need for routing logic.
Add a tool that signals escalation (e.g., creates a ticket in your CRM or notifies an operator on Slack). Tell the prompt when to use it:
Same code, different SENDER_ID secret in each function. The prompt can even include process.env.BRAND_NAME to customize per tenant:
With includeContactMetadata: true (default), the LLM sees:
  • Contact’s display name (if known).
  • Custom metadata fields on the contact.
  • Channel they wrote from.
Set metadata via client.contacts.update(contactId, { metadata: {...} }). Useful for “Hello $name” style personalization without a tool call.

Next

defineTool

Give the agent actions to execute.

Secrets

Store SENDER_ID, API keys, and other env vars.