How agents work
From TypeScript on your machine to an agent answering a phone call
An agent on Zavu is one object that answers on every channel. The same prompt, model, knowledge base and skills serve a WhatsApp message and a phone call.
There are two ways to get one, and they produce the same thing:
- In code — declare it with
defineAgentin a Zavu Function and deploy. The code is the source of truth. - In the dashboard or over the API — create and configure it directly.
This page follows the code-first path end to end, because it is the one with moving parts you cannot see.
The pieces
flowchart LR
subgraph you["Your machine"]
src["index.ts<br/>defineAgent + defineTool"]
end
subgraph cloud["Zavu Cloud"]
bw["Build Worker<br/>bundles your code"]
fn["Your Function<br/>an isolated runtime"]
rec["Reconciler<br/>reads what you declared"]
agent[("Agent<br/>prompt · model · tools")]
end
src -->|npx zavudev deploy| bw
bw -->|publishes| fn
rec -->|asks it what it declares| fn
rec -->|creates / updates| agent
Two things live in Zavu Cloud that are easy to conflate:
- Your Function is your TypeScript, running in its own isolated runtime. It holds your tool handlers — the code that talks to your systems.
- The Agent is the configuration a conversation runs against: the prompt, the model, which channels trigger it, its knowledge bases, and pointers to your tools.
npx zavudev deploy publishes the first and derives the second from it.
What a deploy actually does
sequenceDiagram
participant Dev as You
participant CLI as zavu CLI
participant BW as Build Worker
participant Fn as Your Function
participant Rec as Reconciler
Dev->>CLI: npx zavudev deploy
CLI->>BW: source + dependencies
BW->>BW: bundle (your runtime stays external)
BW->>Fn: publish
Rec->>Fn: "what do you declare?"
Fn-->>Rec: manifest — agents + tools
Rec->>Rec: create, update, delete to match
Rec-->>CLI: summary + warnings
CLI-->>Dev: ✓ Deployed
The step worth understanding is the manifest. After publishing, Zavu asks your function what it declared, and reconciles reality to match it. That is what makes the code the source of truth:
- A
defineAgentyou added becomes a new agent. - A field you changed is updated.
- A
defineAgentyou deleted deletes the agent.
A message arrives
sequenceDiagram
participant User as Customer
participant Ch as WhatsApp / SMS / …
participant Zavu as Zavu
participant Agent as Your agent
participant KB as Knowledge base
User->>Ch: "where is order ORD-1?"
Ch->>Zavu: inbound message
Zavu->>Zavu: does an agent answer on this sender?
Zavu->>KB: retrieve relevant chunks
KB-->>Zavu: context
Zavu->>Agent: prompt + history + context
Agent-->>Zavu: reply
Zavu->>Ch: send
Ch-->>User: reply
A call arrives
Voice adds speech in and out around the same agent, and one important difference: on a call the model is offered your tools.
sequenceDiagram
participant Caller
participant Tel as Telephony
participant VP as Voice pipeline
participant Agent as Your agent
participant Fn as Your Function
Caller->>Tel: dials your number
Tel->>VP: call connected
VP-->>Caller: greeting
Caller->>VP: speaks
VP->>VP: speech → text
VP->>Agent: transcript
Agent->>Fn: check_availability(…)
Fn-->>Agent: { ok: true, slots: [...] }
Agent-->>VP: reply
VP->>VP: text → speech
VP-->>Caller: spoken answer
The caller can interrupt while the agent is speaking (barge-in) and the agent stops to listen. It can also transfer to a human, or end the call.
Where tools run
Everywhere.
| Channel | Are tools offered to the model? |
|---|---|
| Voice | Yes |
A flow's tool step | Yes |
| Text (WhatsApp, SMS, Telegram, email) | Yes |
A text agent asked to look something up calls the tool and answers with what it returned. The model may chain up to five tool rounds in a single reply, and you are billed for the tokens the whole loop uses.
Flows are still worth reaching for, but for a different reason than they used to be: a flow runs the steps you wrote, in the order you wrote them. Use one when the sequence has to be deterministic rather than decided by the model.
Two things this does not cover, both of which npx zavudev agents test will
tell you about:
- By default a dry run does not execute a tool, because doing so fires real
webhooks. It reports the tool and arguments the agent chose, so an answer that
looks like a lookup is never mistaken for one.
--execute-toolsruns them. - An agent is only reachable on the channels its sender actually has. An agent whose sender has none, or none of the ones the agent triggers on, answers every dry run and no real message.
What you can verify, and when
Most of the loop is free and instant. A dry run costs what a reply costs, and the last step is the real thing.
flowchart TD
A["Write index.ts"] --> B["tsc — types resolve locally"]
B --> C["npx zavudev fn invoke --tool<br/>runs one handler, no deploy"]
C --> D["npx zavudev deploy"]
D --> E["npx zavudev agents list<br/>confirm what landed"]
E --> F["npx zavudev agents test<br/>the agent's reply, nothing sent"]
F --> G["A real message, or a real call"]
G -.->|costs money| G
| Step | Cost | What it proves |
|---|---|---|
tsc | free | Your code is valid against the runtime's types |
fn invoke --tool | free | A handler does what you think, in isolation |
agents list | free | The deploy created what you meant |
agents test | a reply's tokens | The prompt, model and retrieval produce the right answer; with --execute-tools, the tools too |
| A real message or call | billed | Everything else, including tool calling on voice |
Agents and senders
A sender is the identity a conversation happens on: a phone number, a WhatsApp account, an email address.
flowchart LR
A["Agent"] --- S1["Sender<br/>+1 415 555 0100"]
A --- S2["Sender<br/>support@acme.com"]
A --- S3["Sender<br/>WhatsApp"]
An agent can answer on several senders. A sender answers with at most one agent — connecting one that is already in use is rejected, naming the agent that holds it.
terminalnpx zavudev agents list npx zavudev agents senders connect --agent <agentId> --sender <senderId>
An agent can also exist with no sender at all while you build it. It will not receive anything until you connect one and enable it.
Addressing an agent
Everything that hangs off an agent (its tools, flows and knowledge bases) is reachable two ways:
Code/v1/senders/{senderId}/agent/tools through the sender that answers with it /v1/agents/{agentId}/tools through the agent itself
They are the same resource. Use the second when the agent has no sender yet,
which is the state POST /v1/agents creates and the one you assemble an agent
in. The first is convenient when you are already holding a sender id.
terminal# Build an agent before it answers anywhere AGENT=$(curl -s -X POST https://api.zavu.dev/v1/agents \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -d '{"name":"Ada","provider":"zavu","model":"openai/gpt-5.6-luna","systemPrompt":"..."}' \ | jq -r .agent.id) curl -X POST https://api.zavu.dev/v1/agents/$AGENT/tools -d '{...}' curl -X POST https://api.zavu.dev/v1/agents/$AGENT/knowledge-bases -d '{...}' # Then connect a sender and enable it curl -X POST https://api.zavu.dev/v1/agents/$AGENT/senders -d "{\"senderId\":\"$SENDER_ID\"}" curl -X PATCH https://api.zavu.dev/v1/agents/$AGENT -d '{"enabled":true}'