DEV Community

Necati Özmen
Necati Özmen

Posted on

[Tutorial] WhatsApp Order AI Agent

Build a WhatsApp chatbot that takes food orders through natural conversation, manages menu items from a database, and processes orders with conversation context.


WhatsApp Order Agent

Full example: github.com/VoltAgent/voltagent/tree/main/examples/with-whatsapp

What We're Building

A WhatsApp AI agent that:

  • Shows menu items from Supabase
  • Takes orders through natural conversation
  • Collects delivery addresses
  • Tracks order status
  • Maintains conversation context with working memory

Setup

Clone the example:

npm create voltagent-app@latest -- --example with-whatsapp
cd with-whatsapp
Enter fullscreen mode Exit fullscreen mode

Create .env:

OPENAI_API_KEY=your_openai_api_key
SUPABASE_URL=your_supabase_project_url
SUPABASE_ANON_KEY=your_supabase_anon_key
WHATSAPP_WEBHOOK_TOKEN=your_webhook_verification_token
WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token
WHATSAPP_PHONE_NUMBER_ID=your_whatsapp_phone_number_id
Enter fullscreen mode Exit fullscreen mode

Database Schema

Run this SQL in your Supabase dashboard:

Supabase Setup

-- Menu items table
CREATE TABLE public.menu_items (
  id SERIAL PRIMARY KEY,
  category VARCHAR(50) NOT NULL,
  name VARCHAR(100) NOT NULL,
  description TEXT,
  price NUMERIC(10, 2) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Orders table
CREATE TABLE public.orders (
  id SERIAL PRIMARY KEY,
  customer_phone VARCHAR(20) NOT NULL,
  customer_address TEXT NOT NULL,
  total_amount NUMERIC(10, 2) NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'preparing',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Order items table
CREATE TABLE public.order_items (
  id SERIAL PRIMARY KEY,
  order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
  menu_item_id INTEGER REFERENCES menu_items(id) ON DELETE CASCADE,
  quantity INTEGER NOT NULL DEFAULT 1,
  price NUMERIC(10, 2) NOT NULL
);

-- Sample data
INSERT INTO menu_items (category, name, description, price) VALUES
  ('Pizza', 'Margherita', 'Fresh tomatoes, mozzarella, basil', 12.99),
  ('Pizza', 'Pepperoni', 'Pepperoni, mozzarella, tomato sauce', 14.99),
  ('Burger', 'Classic Burger', 'Beef patty, lettuce, tomato, onion', 10.99),
  ('Drinks', 'Coke', 'Coca-Cola 330ml', 2.99);
Enter fullscreen mode Exit fullscreen mode

Core Components

Working Memory Schema

Define conversation state with Zod:

const workingMemorySchema = z.object({
  orders: z.array(z.object({
    menuItemId: z.number(),
    itemName: z.string(),
    quantity: z.number(),
    price: z.number(),
  })).default([]),
  deliveryAddress: z.string().default(""),
  orderStatus: z.enum(["selecting", "address_needed", "completed"]).default("selecting"),
});
Enter fullscreen mode Exit fullscreen mode

Memory Configuration

const memory = new Memory({
  storage: new LibSQLMemoryAdapter({
    url: "file:./.voltagent/memory.db",
  }),
  workingMemory: {
    enabled: true,
    scope: "conversation",
    schema: workingMemorySchema,
  },
});
Enter fullscreen mode Exit fullscreen mode

Working memory maintains cart state across messages without asking customers to repeat information.

Memory Management

Agent Setup

const agent = new Agent({
  name: "whatsapp-order-agent",
  instructions: `You are a WhatsApp ordering agent.

Order Flow:
1. Show menu if orders array is empty
2. When customer orders, keep orderStatus as "selecting"
3. When done ordering, change to "address_needed"
4. After receiving address, change to "completed" and execute createOrder tool

Always be friendly and helpful.`,
  model: openai("gpt-4o-mini"),
  tools: [listMenuItemsTool, createOrderTool, checkOrderStatusTool],
  memory,
});
Enter fullscreen mode Exit fullscreen mode

Tools

List Menu Items:

export const listMenuItemsTool = createTool({
  name: "listMenuItems",
  description: "Lists all menu items from the database",
  parameters: z.object({
    limit: z.number().optional().default(100),
  }),
  execute: async ({ limit }) => {
    const { data, error } = await supabase
      .from("menu_items")
      .select("*")
      .limit(limit);

    return {
      success: !error,
      data: data || [],
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

Create Order:

export const createOrderTool = createTool({
  name: "createOrder",
  description: "Creates a new order",
  parameters: z.object({
    items: z.array(z.object({
      menuItemId: z.number(),
      quantity: z.number(),
      price: z.number(),
    })),
    deliveryAddress: z.string(),
  }),
  execute: async ({ items, deliveryAddress }, context) => {
    const totalAmount = items.reduce((sum, item) =>
      sum + item.price * item.quantity, 0);

    const { data: orderData } = await supabase
      .from("orders")
      .insert({
        customer_phone: context?.userId,
        customer_address: deliveryAddress,
        total_amount: totalAmount,
        status: "preparing",
      })
      .select()
      .single();

    const orderItems = items.map(item => ({
      order_id: orderData.id,
      menu_item_id: item.menuItemId,
      quantity: item.quantity,
      price: item.price,
    }));

    await supabase.from("order_items").insert(orderItems);

    return {
      success: true,
      orderId: orderData.id,
      totalAmount,
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

WhatsApp Webhook

new VoltAgent({
  agents: { agent },
  server: honoServer({
    configureApp: (app) => {
      // Verification (GET)
      app.get("/webhook/whatsapp", async (c) => {
        const mode = c.req.query("hub.mode");
        const token = c.req.query("hub.verify_token");
        const challenge = c.req.query("hub.challenge");

        if (mode === "subscribe" && token === process.env.WHATSAPP_WEBHOOK_TOKEN) {
          return c.text(challenge || "", 200);
        }
        return c.text("Forbidden", 403);
      });

      // Message handler (POST)
      app.post("/webhook/whatsapp", async (c) => {
        const body = await c.req.json();
        const message = body.entry?.[0]?.changes?.[0]?.value?.messages?.[0];

        if (message?.type === "text") {
          const response = await agent.generateText(message.text.body, {
            userId: message.from,
            conversationId: `whatsapp_${message.from}`,
          });

          await sendWhatsAppMessage(message.from, response.text);
        }

        return c.json({ status: "processed" }, 200);
      });
    },
  }),
  logger,
});
Enter fullscreen mode Exit fullscreen mode

Run the Agent

npm run dev
Enter fullscreen mode Exit fullscreen mode

Visit VoltOps Console to test and debug.

VoltOps Console

Example Conversation

Customer: Hi
Agent: Welcome! Would you like to see our menu?

Customer: Yes
Agent: Here's our menu:
**Pizza**
" Margherita - $12.99
" Pepperoni - $14.99
**Burgers**
" Classic Burger - $10.99

What would you like to order?

Customer: 2 pepperoni pizzas and a coke
Agent: Added to cart:
- 2x Pepperoni Pizza ($29.98)
- 1x Coke ($2.99)
Total: $32.97

Anything else?

Customer: No thanks
Agent: Please provide your delivery address.

Customer: 123 Main St
Agent: Order #1057 confirmed!
=� 123 Main St
=� $32.97
� 30-45 minutes
Enter fullscreen mode Exit fullscreen mode

Agent in Action

Key Features

  • TypeScript Tools: Zod-typed tools with automatic inference
  • Working Memory: Maintains cart state across messages
  • WhatsApp Integration: Webhook handlers for Meta API
  • Observability: VoltOps platform traces every interaction
  • Database Integration: Supabase for menu and orders

Top comments (0)