Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.zavu.dev/llms.txt

Use this file to discover all available pages before exploring further.

Restaurant booking agent

A full agent that handles three things customers want from a restaurant on WhatsApp:
  1. Browse the menu (with filters: vegan, gluten-free).
  2. Check availability for a date and party size.
  3. 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.
The restaurant-booking template ships with the CLI:
zavu fn init --template restaurant-booking -y
This guide walks through what that template produces and how to evolve it into a production setup with real data.

What you’ll build

👤 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

zavu fn init --template restaurant-booking --slug bella -y
cd bella
index.ts is the entire integration:
index.ts
import { defineAgent, defineTool } from "@zavu/functions"

defineAgent({
  senderId: process.env.SENDER_ID!,
  name: "Bella",
  provider: "zavu",
  model: "openai/gpt-4o-mini",
  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

zavu senders list                                      # find your WhatsApp sender ID
zavu fn secrets set SENDER_ID jn76vnxet8g5nq661by3v06y1581bmmn

3. Deploy

zavu deploy
Watch the summary:
✓ 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.
# In another terminal, watch what's happening
zavu fn logs --tail
2026-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 (Lambda cold start wipes it).

Step 1: persist reservations

Add a real database. We’ll use Postgres via the postgres npm package:
zavu fn secrets set DATABASE_URL "postgresql://user:pass@host/db?sslmode=require"
package.json
{
  "dependencies": {
    "postgres": "^3.4.0"
  }
}
import 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:
defineTool({
  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:
import 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

defineTool({
  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:
defineTool({
  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

# 1. Edit index.ts.
# 2. Push code without redeploying Lambda:
zavu fn push

# 3. The dashboard reflects the new code instantly (good for code review).

# 4. When ready to test live:
zavu deploy
When you remove a tool from the code, zavu deploy deletes it from the agent automatically. The summary will show:
  Tools synced:
    - cancel_reservation

Costs

For a busy restaurant doing ~50 customer conversations a day:
ItemPer conversationPer month
LLM (gpt-4o-mini via Zavu gateway, ~3 turns)~$0.0006~$0.90
Lambda invocations~$0.00000001 × 6 invocationsnegligible
WhatsApp conversation fee (Meta)~$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.

Next

Customer support example

Knowledge base lookup + ticket creation.

Ecommerce example

Order tracking + smart recommendations.