--- title: How to Build an AI Agent Without Writing the Boilerplate description: 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. date: 2026-08-11 author: Victor Villalobos locale: en source: https://www.zavu.dev/en/blog/how-to-build-an-ai-agent tags: AI Agents, Tutorial, CLI, WhatsApp --- # How to Build an AI Agent Without Writing the Boilerplate 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](/en/blog/ai-agent-vs-chatbot) 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. | Channel | Good for | The catch | |---|---|---| | WhatsApp | Support, bookings, anything conversational in Brazil, LATAM, India, Southeast Asia, Southern Europe | Business verification, and you can only message freely for 24 hours after they write | | SMS | Reaching anyone with a phone, alerts, verification | In the US you need A2P 10DLC registration before carriers deliver | | Email | Long context, attachments, threads, B2B | Deliverability is its own discipline: DKIM, SPF, and a domain reputation you can burn | | Voice | Callers who will not type, older demographics, hands-busy moments | Latency budget is unforgiving. Two seconds of silence reads as a dropped call | | Telegram, Instagram, Messenger | Communities and consumer brands where the audience already lives there | Each 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 ```bash 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: ```ts 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 ```bash npx zavudev agents test --agent --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: ```bash npx zavudev agents catalog npx zavudev agents pull fermi --sender ``` `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 - [AI agent vs chatbot](/en/blog/ai-agent-vs-chatbot): what actually changes when you add tools, and when a scripted flow is still right. - [AI agent frameworks compared](/en/blog/ai-agent-frameworks): LangGraph, CrewAI and the rest, and which layer each one owns. - [Build AI agents with your coding agent](/en/blog/build-ai-agents-with-your-coding-agent): the same workflow in more depth, including who does what. - [WhatsApp AI agents with Next.js](/en/blog/whatsapp-ai-agents-nextjs), [FastAPI](/en/blog/whatsapp-ai-agents-fastapi) or [Django](/en/blog/whatsapp-ai-agents-django): when you are wiring an existing app instead. - [Replace your IVR with a voice agent](/en/blog/replace-your-ivr-with-a-voice-agent): the same idea on the phone.