Restaurant booking agent
Complete walkthrough — menu, availability, reservations over WhatsApp.
Restaurant booking agent
A full agent that handles three things customers want from a restaurant on WhatsApp:
- Browse the menu (with filters: vegan, gluten-free).
- Check availability for a date and party size.
- Confirm a reservation with a code they can quote on arrival.
By the end you'll have ~200 lines of TypeScript, no infrastructure to manage, and the whole thing deploys with one command.
What you'll build
Code👤 hola, qué tienen vegano? 🤖 [view_menu(filter=vegan)] 🤖 Tenemos Vegan Buddha Bowl ($11) y ensaladas. ¿Te muestro alguna específica? 👤 sí, busco mañana viernes para 2 personas 🤖 [check_availability(date=friday, partySize=2)] 🤖 Tengo disponibilidad 19:30, 21:00, 22:30. ¿Cuál te conviene? 👤 21 🤖 ¿A qué nombre la reservo? 👤 Marco 🤖 [create_reservation(date=friday, time=21:00, partySize=2, customerName=Marco)] 🤖 ¡Listo Marco! Reserva RES-LXXY confirmada para 2 personas, viernes a las 21:00. Te esperamos. 🍕
1. Scaffold
terminalnpx zavudev fn init --template restaurant-booking --slug bella -y cd bella
index.ts is the entire integration:
index.tsimport { defineAgent, defineTool } from "@zavudev/functions" defineAgent({ senderId: process.env.SENDER_ID!, name: "Bella", provider: "zavu", model: "openai/gpt-5.6-luna", channels: ["whatsapp"], prompt: `Eres Bella, anfitriona de Bella Pizzeria en WhatsApp. Ayudas al cliente a ver el menú, consultar disponibilidad y hacer reservas. Reglas: - Respuestas cortas. Es WhatsApp. - Nunca inventes precios ni horarios — usa siempre las tools. - Confirma cada reserva con su código antes de cerrarla. - Habla en el idioma del cliente.`, }) // Demo data — replace with your POS / DB. const MENU = [ { name: "Pizza Margherita", price: 12, vegan: false }, { name: "Pizza Pepperoni", price: 14, vegan: false }, { name: "Vegan Buddha Bowl", price: 11, vegan: true }, { name: "Caesar Salad", price: 9, vegan: false }, { name: "Tiramisu", price: 7, vegan: false }, ] defineTool({ name: "view_menu", description: "Get the restaurant menu. Use when the customer asks what's available, prices, or vegan options.", parameters: { type: "object", properties: { filter: { type: "string", description: "all | vegan" }, }, required: [], }, handler: async ({ filter }) => { const items = filter === "vegan" ? MENU.filter((m) => m.vegan) : MENU return { menu: items.map((m) => ({ name: m.name, price: `$${m.price}`, vegan: m.vegan, })), count: items.length, } }, }) defineTool({ name: "check_availability", description: "Check available reservation slots for a date and party size. Call before create_reservation.", parameters: { type: "object", properties: { date: { type: "string", description: "YYYY-MM-DD or 'today', 'tomorrow', 'friday'" }, partySize: { type: "number" }, }, required: ["date", "partySize"], }, handler: async ({ date, partySize }) => { const weekend = /(fri|sat)/i.test(date) const slots = weekend ? ["18:00", "19:30", "21:00", "22:30"] : ["19:00", "21:00"] return { available: slots.length > 0, slots, date, partySize } }, }) const reservations: Record<string, any> = {} defineTool({ name: "create_reservation", description: "Book a confirmed reservation. Only call AFTER check_availability returns the requested slot.", parameters: { type: "object", properties: { date: { type: "string" }, time: { type: "string" }, partySize: { type: "number" }, customerName: { type: "string" }, }, required: ["date", "time", "partySize", "customerName"], }, handler: async ({ date, time, partySize, customerName }, ctx) => { const id = `RES-${Date.now().toString(36).toUpperCase()}` reservations[id] = { id, date, time, partySize: Number(partySize), customerName, phone: ctx?.contactPhone ?? null, status: "confirmed", createdAt: new Date().toISOString(), } return { confirmed: true, reservationId: id, summary: `${customerName}, party of ${partySize}, ${date} at ${time}`, cancellationCode: id.slice(-4), } }, }) defineTool({ name: "view_reservation", description: "Look up an existing reservation by its ID.", parameters: { type: "object", properties: { reservationId: { type: "string" } }, required: ["reservationId"], }, handler: async ({ reservationId }) => { const r = reservations[reservationId] if (!r) return { error: "not_found", reservationId } return r }, })
2. Configure
terminalnpx zavudev senders list # find your WhatsApp sender ID npx zavudev fn secrets set SENDER_ID jn76vnxet8g5nq661by3v06y1581bmmn
3. Deploy
terminalnpx zavudev deploy
Watch the summary:
Code✓ Deployed in 14s Agents synced: + Bella Tools synced: + view_menu + check_availability + create_reservation + view_reservation
4. Test
Send a WhatsApp to the sender's number:
hola, qué tienen vegano?
The agent answers, calls tools, and confirms a reservation.
terminal# In another terminal, watch what's happening npx zavudev fn logs --tail
Code2026-05-11T19:15:32.123Z tool call: view_menu { filter: 'vegan' } 2026-05-11T19:15:48.456Z tool call: check_availability { date: 'friday', partySize: 2 } 2026-05-11T19:16:12.789Z tool call: create_reservation { date: 'friday', time: '21:00', partySize: 2, customerName: 'Marco' }
Moving to production
The template uses in-memory state — fine for demo, useless in real life (cold starts wipe it).
Step 1: persist reservations
Add a real database. We'll use Postgres via the postgres npm package:
terminalnpx zavudev fn secrets set DATABASE_URL "postgresql://user:pass@host/db?sslmode=require"
package.json{ "dependencies": { "postgres": "^3.4.0" } }
TypeScriptimport postgres from "postgres" const sql = postgres(process.env.DATABASE_URL!, { ssl: "require" }) defineTool({ name: "create_reservation", description: "...", parameters: { /* same */ }, handler: async ({ date, time, partySize, customerName }, ctx) => { const [row] = await sql` INSERT INTO reservations (date, time, party_size, customer_name, phone, status) VALUES (${date}, ${time}, ${partySize}, ${customerName}, ${ctx?.contactPhone ?? null}, 'confirmed') RETURNING id ` return { confirmed: true, reservationId: `RES-${row.id}`, summary: `${customerName}, party of ${partySize}, ${date} at ${time}`, } }, })
Step 2: real availability
Replace the demo logic with a query against your reservations + capacity:
TypeScriptdefineTool({ name: "check_availability", description: "...", parameters: { /* same */ }, handler: async ({ date, partySize }) => { const dateIso = resolveDate(date) // "friday" → "2026-05-15" const taken = await sql<{ time: string }[]>` SELECT time FROM reservations WHERE date = ${dateIso} AND status = 'confirmed' ` const allSlots = [ "18:00", "18:30", "19:00", "19:30", "20:00", "20:30", "21:00", "21:30", "22:00", "22:30", ] const capacity = await sql`SELECT max_concurrent FROM restaurant_config LIMIT 1` const free = allSlots.filter((slot) => { const concurrent = taken.filter(t => t.time === slot).length return concurrent < capacity[0].max_concurrent }) return { available: free.length > 0, slots: free.slice(0, 4), date: dateIso, partySize } }, })
Step 3: send confirmation message
After creating a reservation, send a follow-up WhatsApp confirming the
booking. The auto-provisioned ZAVU_API_KEY already lets you do this:
TypeScriptimport Zavudev from "@zavudev/sdk" const zavu = new Zavudev({ apiKey: process.env.ZAVU_API_KEY!, baseURL: process.env.ZAVU_API_BASE_URL, }) // inside create_reservation handler, after the INSERT: await zavu.messages.send({ to: ctx.contactPhone!, channel: "whatsapp", "Zavu-Sender": process.env.SENDER_ID!, text: `✅ Confirmado: ${customerName}, ${partySize} personas, ${date} a las ${time}. ID: RES-${row.id} Si necesitas cancelar, escribime "cancelar RES-${row.id}".`, })
Step 4: cancellation tool
TypeScriptdefineTool({ name: "cancel_reservation", description: "Cancel a confirmed reservation. Customer must provide the reservation ID.", parameters: { type: "object", properties: { reservationId: { type: "string" } }, required: ["reservationId"], }, handler: async ({ reservationId }, ctx) => { const id = reservationId.replace(/^RES-/, "") const result = await sql` UPDATE reservations SET status = 'cancelled', cancelled_at = NOW() WHERE id = ${id} AND phone = ${ctx?.contactPhone ?? null} RETURNING customer_name ` if (result.length === 0) { return { error: "not_found_or_not_yours", message: "No encuentro esa reserva con tu número.", } } return { cancelled: true, summary: `Cancelada la reserva ${reservationId} de ${result[0].customer_name}.`, } }, })
Note the WHERE phone = ${ctx?.contactPhone} — customers can only cancel
their own reservations, even if they get the right ID.
Step 5: opening hours guard
Add reasoning the LLM can't accidentally bypass:
TypeScriptdefineTool({ name: "check_availability", description: "...", handler: async ({ date, partySize }) => { const dateIso = resolveDate(date) const day = new Date(dateIso).getDay() if (day === 1) { return { available: false, message: "Cerrado los lunes. Te puedo reservar para martes en adelante.", } } // ... }, })
The LLM reads the message field and incorporates it into a natural-sounding
reply. No need to add "and we're closed Mondays" to the prompt — the tool
itself enforces it.
Iterating fast
terminal# 1. Edit index.ts. # 2. Push code to the dashboard without redeploying the runtime: npx zavudev fn push # 3. The dashboard reflects the new code instantly (good for code review). # 4. When ready to test live: npx zavudev deploy
When you remove a tool from the code, npx zavudev deploy deletes it from the
agent automatically. The summary will show:
CodeTools synced: - cancel_reservation
Costs
For a busy restaurant doing ~50 customer conversations a day:
| Item | Per conversation | Per month |
|---|---|---|
| LLM (gpt-5.6-luna via Zavu gateway, ~3 turns) | ~$0.001 | ~$1.50 |
| Function invocations | ~$0.00000001 × 6 invocations | negligible |
| WhatsApp conversation fee | ~$0.005 (utility tier) | ~$7.50 |
| Total | ~$0.006 | ~$9 |
Add your DB hosting (PlanetScale free tier works) and you're under $10/mo for a fully automated booking agent.