AI AgentsTutorialCLIWhatsApp

How to Build an AI Agent Without Writing the Boilerplate

You install two things, then you describe the agent you want. Your coding agent writes it, deploys it and tests it. Your job is understanding the concepts well enough to ask for the right thing.

Written by: Victor VillalobosReviewed by: Jennifer VillalobosAugust 11, 202611 min read
View as Markdown

Most guides on building an AI agent hand you a hundred lines to type. A loop, a tool dispatcher, a webhook handler, a conversation store. You type them, they work on your laptop, and the tutorial declares victory.

That was the right shape of guide two years ago. It is not any more, because you already have an agent that writes code sitting in your editor, and the boilerplate is exactly what it is good at.

So this guide is a different shape. You install two things. Then you describe what you want in plain language, and your coding agent scaffolds it, deploys it, tests it and reads the logs. What you actually need to bring is the part it cannot: knowing what an agent is made of, well enough to ask for the right one and to spot a wrong answer.

What makes something an agent

Worth being precise about, because the word has been stretched until it means nothing, and because a vague mental model produces a vague prompt.

A prompt is one call. Text in, text out.

An agent is a loop. The model gets a goal and a set of tools. It decides which tool to call, sees the result, and decides again. It keeps going until the goal is met or it gives up.

while not done:
    decision = model(conversation, tools)
    if decision.is_final_answer:
        done = True
    else:
        result = run_tool(decision.tool, decision.args)
        conversation.append(result)

That loop is the entire idea. Frameworks add memory, retries, tracing and multi-agent handoff, but strip them down and you find this.

You will not write that loop. The runtime owns it. You need to understand it because every failure you will debug is a failure of that loop: it ran too many times, it called the wrong tool, it never called one at all.

The practical consequence: an agent is only as useful as its tools. A model with no tools can talk about your refund policy. A model with a lookup_order tool can tell a customer where their package is. Almost all the value lives in the tools, which means almost all of your prompt should be about them.

The five decisions to make before you ask for anything

These are the ones a coding agent cannot make for you, because they are business decisions wearing technical clothes. Ten minutes here saves a week.

1. What is the agent allowed to do? Write the list of tools first. lookup_order, book_slot, transfer_to_human. If the list is empty, you want a chatbot, not an agent, and you should read the comparison before building either.

2. What happens when it is unsure? Every agent hits a question it cannot answer. The two honest options are: say so and stop, or hand off to a human. Pick one now. The failure you are avoiding is the agent that confidently invents an answer, and that is a design decision, not a model problem.

3. Which channel? This decides your architecture, and it is the question skipped most often. See below.

4. What must it never do? Promise a delivery date. Approve a refund over an amount. Give medical advice. These become lines in the prompt, and if you do not say them out loud now they will not be there.

5. How will you know it broke? If your answer is "a customer will tell us", you do not have an observability plan. You have a complaint queue.

Written down, those five answers are most of your prompt. That is the point of writing them down.

Choosing the channel

Your customers already have a place where they read messages. The question is which one you meet them in.

ChannelGood forThe catch
WhatsAppSupport, bookings, anything conversational in Brazil, LATAM, India, Southeast Asia, Southern EuropeBusiness verification, and you can only message freely for 24 hours after they write
SMSReaching anyone with a phone, alerts, verificationIn the US you need A2P 10DLC registration before carriers deliver
EmailLong context, attachments, threads, B2BDeliverability is its own discipline: DKIM, SPF, and a domain reputation you can burn
VoiceCallers who will not type, older demographics, hands-busy momentsLatency budget is unforgiving. Two seconds of silence reads as a dropped call
Telegram, Instagram, MessengerCommunities and consumer brands where the audience already lives thereEach has its own identity model and rate limits

The web chat widget is missing from that table on purpose. A widget only reaches people already on your site. Every channel above reaches people where they already are, which is why an agent on WhatsApp gets used and the same agent behind a widget does not.

The setup: two commands

terminal
npx skills add zavudev/zavu-skills npx zavudev@latest login

These do two different jobs, and it is worth knowing which is which.

Skills give your coding agent knowledge. Ask any coding agent to "add WhatsApp to my app" without them and you get code that looks right. Looking right is the problem: it invents endpoint names, ignores the 24 hour window so the first production send fails and dev never did, and writes a webhook handler with no signature verification. That is not the model being weak. It has simply never seen this API and will not tell you so. The skills are plain markdown that loads itself when the task matches, and the installer asks which coding agents to install to. Forty-plus are supported, including Claude Code, Cursor, Copilot, Codex, Cline, Gemini CLI, Amp and Warp.

The CLI gives it hands. Scaffold, set secrets, deploy, test, tail logs. login opens a browser, you sign in, you pick the project, you click Authorize. The key lands in ~/.zavu/credentials.json and your agent uses it from then on. On a machine with no browser, set ZAVUDEV_API_KEY instead.

That is everything you type. From here you talk.

The prompt

Now use the five answers. A good prompt for this is not clever, it is specific:

> Build me a support agent for my online store on WhatsApp.>> It should answer questions about orders. Give it a tool that looks up an order by ID against https://api.example.com/orders/{id}, using STORE_API_KEY from secrets. Give it a second tool that hands the conversation to a human, which posts to our on-call webhook.>> Rules: answer in two sentences or fewer, never invent a delivery date, and if you cannot find an order say so and offer a person instead. For refunds over $200 always use the handoff tool rather than deciding.>> Deploy it and test it with "where is order 4471?"

Your coding agent will scaffold the function, write the agent and both tools, set the secrets, deploy, and run the test. Notice what made that prompt work: it was decisions 1, 2 and 4 from the list above, said out loud. Nothing in it is syntax.

What it wrote, and what you check

You do not type this. You read it, because reviewing the diff is the part that stays yours:

TypeScript
import { defineAgent, defineTool } from "@zavudev/functions" defineAgent({ senderId: process.env.SENDER_ID!, name: "Nora", provider: "zavu", model: "openai/gpt-4o-mini", prompt: You are Nora, support for an online store. Answer in two sentences or fewer. If you cannot find an order, say so and offer to pass the customer to a person. Never invent a delivery date., }) defineTool({ name: "lookup_order", description: "Get the current status of a customer order. Use when the customer mentions an order number or asks where their package is.", parameters: { type: "object", properties: { orderId: { type: "string" } }, required: ["orderId"], }, handler: async ({ orderId }) => { const res = await fetch(https://api.example.com/orders/${orderId}, { headers: { Authorization: Bearer ${process.env.STORE_API_KEY} }, }) return res.json() }, })

Three things to look at, in this order:

The tool descriptions. "Use when the customer mentions an order number or asks where their package is" is what the model reads when deciding whether to call it. This is the single highest-leverage line in the file, and it is the one a coding agent most often writes lazily. If it says only "looks up an order", make it say when, and when not to.

Your rules, actually present. Every "never" you asked for should appear in the prompt string. If one is missing, it will not be enforced by good intentions.

The escalation rule in two places. Policy goes in the prompt so the model knows it. The trigger goes in the tool description so the model recognises the moment. An agent that "knows" it should escalate but never does is almost always missing the second one.

What you are not reviewing: the loop, the turn limit, per-contact conversation history, webhook signature verification, the WhatsApp 24 hour window. The runtime owns all of it, which is the reason there is no boilerplate in this file.

Test before a customer does

terminal
npx zavudev agents test --agent <agentId> --message "where is order 4471?"

This runs the real agent with the real prompt and the real knowledge base, returns what it would say, delivers nothing to anyone and charges nothing. Your coding agent can run it in a loop while it iterates. Add --json to assert on it in CI.

Two details make it more honest than most preview features. It returns warnings for things that are true of your agent but that a dry run cannot prove: the agent being disabled, or tools that exist but were not offered to the model in this run. And by default it does not execute tools, because a rehearsal that charges a customer's card is not a rehearsal. When you want the full loop, pass executeTools and check executedToolCalls to see what actually ran.

A green test is not proof the agent works live. It is proof the prompt does what you think, which is the thing you were unsure about.

Iterating is more prompting

You do not go back to the editor. You say what was wrong:

> It answered "your order is on the way" without calling the tool. Tighten the tool description so it fires whenever an order is mentioned, redeploy and test again with the same message.

The reason this works is the deploy summary is machine-readable and the test is one command, so your coding agent can close the loop by itself: change, deploy, test, read, change again.

One thing to know about that summary. + means created, ~ means it existed and was rewritten, = (unchanged) means nothing differed. The markers describe what the deploy wrote, not what the agent now says. To verify a change reached the model, put a distinctive word in the prompt you edited and check it comes back from agents test. And read the lines above the ✓: warnings print before the success line, and they cover the cases where a green deploy did not do what it looks like.

Starting from something that already works

Often faster than describing an agent from nothing:

terminal
npx zavudev agents catalog npx zavudev agents pull fermi --sender <senderId>

catalog lists ready-made agents for support, lead capture and booking, with their tool count and whether they answer phone calls. pull scaffolds one into your repo as real, editable code you own, with the prompt and every tool already written. Then you point your coding agent at it: "change this to work for a dental clinic and use our booking API".

npx zavudev agents init runs the whole thing as one guided command, including creating the sender.

What goes wrong in production

Five failures, in the order you will meet them. Each one is also a good prompt to hand your coding agent, because it can read the same records you can.

It answers confidently and wrong. Almost always a prompt that never gave it permission to fail. Add the sentence explicitly: "If you do not know, say you do not know." Then check knowledgeChunksUsed on the execution record. Zero, on an agent with documents attached, means the answer was not grounded in your content.

It says it will look something up and never does. The reply reads like a tool call happened. Nothing reached your endpoint. Check toolCalls on the execution: zero on an agent with tools configured means the model answered without calling any. Usually the tool description does not match the words customers actually use.

It replies twice. Two messages arrive within a second, two loops run, both answer. Handle the case where a conversation is already being processed.

It is too slow. Every tool call is a round trip, and the model waits. On WhatsApp you have a few seconds before silence reads as broken, so mark the message read and show a typing indicator while you work. On voice you have about one second, which is why a voice agent's tools have to be fast or fake fast.

It works and nobody can tell you why. npx zavudev agents executions gives you the tool calls, the arguments, the results and the errors for a deployed agent. Hand that output to your coding agent and ask it what changed.

Honest limits

Some things an agent should not be the answer to.

If the task is deterministic, ask for a function instead. An agent that always calls the same tool in the same order is a workflow with a language model tax on it.

If a wrong answer is expensive, an agent needs a human in the approval path, not better prompting. Refunds, medical guidance and anything legally binding are in this category.

If you have no tools, you have a search interface over your documents. That can be genuinely useful. It is not an agent, and calling it one sets expectations you cannot meet.

And the limit on this workflow specifically: your coding agent will do everything in the right column, but you still create the account, authorize the login, approve buying a phone number, connect WhatsApp Business through Meta's signup, and read the diff before it ships. Those are the steps that legally, financially or physically need a person.

Where to go next

Need help? Contact us or join our Discord community for support.

Get started

Ready to get started?

Start building for free, or schedule a call to discuss your specific use case.

How to Build an AI Agent (2026 Guide) | Zavu Blog