> ## 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

> Complete walkthrough — menu, availability, reservations over WhatsApp.

## 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.

<Note>
  The `restaurant-booking` template ships with the CLI:

  ```sh theme={null}
  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.
</Note>

## 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

```sh theme={null}
zavu fn init --template restaurant-booking --slug bella -y
cd bella
```

`index.ts` is the entire integration:

```ts index.ts theme={null}
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

```sh theme={null}
zavu senders list                                      # find your WhatsApp sender ID
zavu fn secrets set SENDER_ID jn76vnxet8g5nq661by3v06y1581bmmn
```

## 3. Deploy

```sh theme={null}
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.

```sh theme={null}
# 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
(cold starts wipe it).

### Step 1: persist reservations

Add a real database. We'll use Postgres via the `postgres` npm package:

```sh theme={null}
zavu fn secrets set DATABASE_URL "postgresql://user:pass@host/db?sslmode=require"
```

```json package.json theme={null}
{
  "dependencies": {
    "postgres": "^3.4.0"
  }
}
```

```ts theme={null}
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:

```ts theme={null}
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:

```ts theme={null}
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

```ts theme={null}
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:

```ts theme={null}
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

```sh theme={null}
# 1. Edit index.ts.
# 2. Push code to the dashboard without redeploying the runtime:
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:

| Item                                          | Per conversation               | Per month  |
| --------------------------------------------- | ------------------------------ | ---------- |
| LLM (gpt-4o-mini via Zavu gateway, \~3 turns) | \~\$0.0006                     | \~\$0.90   |
| 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.

## Next

<CardGroup cols={2}>
  <Card title="Customer support example" icon="headset" href="/guides/functions/examples/customer-support">
    Knowledge base lookup + ticket creation.
  </Card>

  <Card title="Ecommerce example" icon="bag-shopping" href="/guides/functions/examples/ecommerce">
    Order tracking + smart recommendations.
  </Card>
</CardGroup>
