Advanced patterns
You’ve shipped your first Function. Now what holds up at scale.Persistent state
Functions are stateless. Cold starts wipe in-process variables. Pick one:Convex tables (recommended for Zavu users)
Tightly integrated. The auto-provisionedZAVUDEV_API_KEY doesn’t grant
arbitrary table access — for that, create a project API key with broader
scopes and inject it as a secret. Or use Convex deployments outside our
managed scope.
Postgres (managed: PlanetScale, Neon, Supabase)
let sql = null pattern lets the same connection survive across warm
invocations — saves the connection handshake (~50ms) on subsequent calls.
Redis (Upstash for serverless-friendly)
For rate-limiting, deduplication, and short-lived state:Composing multiple functions
One project can have many functions. Use this for separation of concerns:
Functions don’t directly call each other today — they communicate via Zavu
events (triggers) or your own database / queue.
Observability
Structured logs
Use the framework’sctx.log so the dashboard’s logs panel can highlight
your output among the runtime’s ceremony lines:
Metrics → external sinks
Send important business events to a metrics service:fetch runs in parallel — don’t await it if you don’t care about delivery
guarantees. Use a fire-and-forget:
Error budgets and retries
The agent retries failed tool calls up to 2 times (LLM’s choice — it sees the error message and may try again). Beyond that, the LLM gives up and tells the customer. For tools that touch unreliable systems (3rd-party APIs), add your own retry-with-backoff:Testing
Local invoke
defineFunction fallback path. Doesn’t simulate LLM
tool calls — for that, deploy + use the real WhatsApp sender.
Unit tests for handlers
Tool handlers are plain functions. Extract them, test them:bun test or vitest locally — they don’t need to ship with the function.
Integration with the LLM
To test how the LLM ACTUALLY picks tools, you need a live agent. The fastest loop:Multi-agent on one sender
Not directly supported — one agent per sender. But you can simulate it with flows OR by having a “router” tool:mode from Redis at
each turn. (You’d inject mode into the agent via custom contact metadata,
which the agent reads automatically with includeContactMetadata: true.)
In practice: one focused agent > one mega-agent juggling modes. Multiple
senders / multiple functions is the canonical way.
Cost optimization
Per-conversation cost breaks down as:
LLM is rarely the bottleneck. What kills budgets:
- Long prompts. Every turn sends the full system prompt + last N messages. A 1000-token system prompt at 10 turns of history = 10k tokens per reply. Trim relentlessly.
- High
contextWindowMessages. Default 10 is overkill for transactional agents. Drop to 4-6 if your conversations are short. - Re-reading large tool returns. If a tool returns 500 items, the LLM re-reads them every turn. Trim server-side.
Migration paths
From dashboard-configured AI Agent → Function
You already have an agent and tools created from the dashboard. To move them under code-managed control:- Write
defineAgent({...})matching your existing config. - Write
defineTool({...})for each existing tool, including the samename,description,parameters. npx zavudev deploy. The reconciler sees existing rows with matching(senderId, name)and takes ownership — patches them to match your code AND marks them managed.
+ ToolName (took over manual) for each.
From that point on, dashboard edits are blocked. Code is source of truth.
From a custom webhook receiver → Function
You have a Vercel function listening for Zavu webhooks. To move:npx zavudev fn initand copy your handler intodefineFunction.- Set up triggers via CLI instead of webhook URLs on senders:
- Disable the webhook on the sender (or leave it — both work in parallel during migration).
Limits to know
For higher limits, contact support.
What a call costs
Functions are billed by memory and time, the same two things the infrastructure underneath charges for. One call is 128 MB running for one second. A function with more memory, or one that takes longer, uses several; anything under a second counts as one, so a fast function costs exactly what it always did.
300,000 calls a month are included on every plan, then $5 per million. Deploys,
logs, rollbacks and triggers are never charged.
Choosing a timeout
The ceiling is 180 seconds and the default is 30, but the number that matters depends on how the function is invoked:- Event and cron invocations are asynchronous. Nothing is waiting on the response, so the timeout only bounds what one invocation can cost you. A slow nightly job is fine here.
- A tool called during a conversation is synchronous. The agent’s reply waits for it, and so does the person who wrote the message. Keep these well under the ceiling: a tool that takes 60 seconds is a conversation that looks broken.
- HTTP-exposed functions are additionally bounded by the platform’s own response limit, which is lower than 180 seconds and is not something a function setting can raise.
Next
Restaurant example
Complete booking agent with persistence.
Runtime versions
Pinning, upgrades, security patches.
