Skip to content

How agents work

From TypeScript on your machine to an agent answering a phone call

View .md

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 defineAgent in 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 defineAgent you added becomes a new agent.
  • A field you changed is updated.
  • A defineAgent you 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.

ChannelAre tools offered to the model?
VoiceYes
A flow's tool stepYes
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-tools runs 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
StepCostWhat it proves
tscfreeYour code is valid against the runtime's types
fn invoke --toolfreeA handler does what you think, in isolation
agents listfreeThe deploy created what you meant
agents testa reply's tokensThe prompt, model and retrieval produce the right answer; with --execute-tools, the tools too
A real message or callbilledEverything 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.

terminal
npx 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}'

Where to go next