← Back to Blog

What You'll Build

If you've ever wanted to build a multi-agent restaurant order system with Claude, you're in the right place. Most restaurant operations still rely on staff to manually relay orders, check stock, and notify the kitchen — which means errors, delays, and a lot of unnecessary chaos.

In this tutorial, you'll build a 3-agent pipeline in Python that takes a customer order as JSON input, validates it, checks inventory availability, and fires a kitchen notification — all automatically using the Claude API.

By the end, you'll have a fully working local system you can extend into a real restaurant automation tool. No prior AI experience required.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic Python knowledge — if you've written a function before, you're good
  • anthropic Python SDK installed (pip install anthropic)
  • A text editor or IDE (VS Code works great)
📦 Full Source Code Note: The complete, working code for this project is built step-by-step in the sections below. Every snippet connects to the next one. By Step 6, you'll have the entire system running. Copy each block as we go and you'll have a complete file at the end.

Full Source Code Overview

The project lives in a single file called restaurant_agents.py. It contains a main orchestrator class, three agent functions, tool definitions, and a run loop that handles Claude's tool call responses.

Here's the file structure we're building toward before we dive into each piece:

project structure
restaurant-agent-system/
├── restaurant_agents.py   # Main file — everything lives here
├── .env                   # Stores your ANTHROPIC_API_KEY
└── requirements.txt       # Just one dependency: anthropic

Create a folder called restaurant-agent-system, then open a terminal inside it and run:

terminal
pip install anthropic python-dotenv

Create a .env file in that folder with your key:

.env
ANTHROPIC_API_KEY=sk-ant-your-key-here

Step 1: Set Up Claude API and Project Structure

Let's start by wiring up the Anthropic client and building the skeleton of our orchestrator class. This class will hold all three agents and coordinate how they talk to each other.

Open restaurant_agents.py and add this at the top:

restaurant_agents.py
import os
import json
from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()

client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

# Simulated inventory database — replace with a real DB in production
INVENTORY = {
    "margherita_pizza": 8,
    "caesar_salad": 5,
    "spaghetti_carbonara": 3,
    "tiramisu": 10,
    "sparkling_water": 20,
}

# Tracks notifications sent to the kitchen during this session
KITCHEN_LOG = []


class RestaurantAgentSystem:
    """Orchestrates the three-agent order processing pipeline."""

    def __init__(self):
        self.model = "claude-sonnet-4-6"
        self.order_agent_system = (
            "You are an order processing agent for an Italian restaurant. "
            "Your job is to validate customer orders, extract structured data, "
            "and use tools to check inventory and notify the kitchen. "
            "Always check inventory before confirming an order."
        )

    def run(self, raw_order: dict) -> dict:
        """Entry point — takes a raw order dict and returns a final result."""
        print(f"\n[Orchestrator] New order received: {json.dumps(raw_order, indent=2)}")
        result = self._run_order_agent(raw_order)
        return result

The INVENTORY dict acts as our mock database for now. In a real system you'd swap this out for a Postgres query or a POS API call. The KITCHEN_LOG list collects every notification the kitchen agent sends so you can inspect the full pipeline output at the end.

Step 2: Define the Order Processing Agent

The order processing agent is the brain of the system. It reads the raw customer order, parses it into a clean structure, and decides which tools to call next.

Add this method inside the RestaurantAgentSystem class:

restaurant_agents.py — inside RestaurantAgentSystem
    def _run_order_agent(self, raw_order: dict) -> dict:
        """
        Order Processing Agent.
        Sends the raw order to Claude with tool access.
        Handles the tool-use loop until Claude returns a final text response.
        """
        tools = self._get_tool_definitions()
        messages = [
            {
                "role": "user",
                "content": (
                    f"Process this restaurant order and confirm it with the kitchen:\n"
                    f"{json.dumps(raw_order, indent=2)}\n\n"
                    f"Steps: 1) Check inventory for each item. "
                    f"2) If all items are available, notify the kitchen. "
                    f"3) Return a confirmation summary."
                ),
            }
        ]

        print("[Order Agent] Sending order to Claude for processing...")

        # Run the agentic loop — keeps going until Claude stops calling tools
        return self._agent_run_loop(messages, tools)

Notice the prompt gives Claude explicit steps to follow. That's intentional — the more specific you are about the workflow, the fewer hallucinations or missed steps you'll see. Think of it like giving a new employee a checklist.

Step 3: Create the Inventory Management Agent

The inventory agent is implemented as a tool that Claude can call. When Claude decides to check stock for an item, it calls check_inventory and gets a real-time response back.

Add this method to the class:

restaurant_agents.py — inside RestaurantAgentSystem
    def _check_inventory(self, item_name: str, quantity_requested: int) -> dict:
        """
        Inventory Management Agent (tool implementation).
        Checks if the requested item and quantity are available.
        Returns availability status and current stock level.
        """
        # Normalize the item name to match our inventory keys
        normalized = item_name.lower().replace(" ", "_")
        current_stock = INVENTORY.get(normalized, 0)
        available = current_stock >= quantity_requested

        result = {
            "item": item_name,
            "requested": quantity_requested,
            "in_stock": current_stock,
            "available": available,
            "message": (
                f"{'✅ Available' if available else '❌ Insufficient stock'}: "
                f"{current_stock} units of '{item_name}' in stock, "
                f"{quantity_requested} requested."
            ),
        }

        print(f"[Inventory Agent] {result['message']}")

        # Deduct from inventory if available — simulates a real reservation
        if available:
            INVENTORY[normalized] -= quantity_requested

        return result

The inventory deduction at the bottom is a small detail that makes a big difference. It means if two orders come in simultaneously for the last item, the second one will correctly get an "out of stock" response. That's the kind of real-world edge case you want to handle from the start.

Step 4: Build the Kitchen Notification Agent

The kitchen notification agent is also a tool. Once inventory is confirmed, Claude calls this to fire off the kitchen ticket.

In a production system, this would hit a KDS (Kitchen Display System) API or send a webhook. Here, we log it and print it so you can see it working:

restaurant_agents.py — inside RestaurantAgentSystem
    def _notify_kitchen(self, table_number: int, items: list, special_notes: str = "") -> dict:
        """
        Kitchen Notification Agent (tool implementation).
        Sends a confirmed order ticket to the kitchen.
        Returns a ticket ID and confirmation status.
        """
        import time

        ticket_id = f"TKT-{int(time.time())}"

        ticket = {
            "ticket_id": ticket_id,
            "table": table_number,
            "items": items,
            "special_notes": special_notes,
            "status": "sent_to_kitchen",
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        }

        # Append to session log so the orchestrator can return it
        KITCHEN_LOG.append(ticket)

        print(f"\n[Kitchen Agent] 🎫 Ticket {ticket_id} sent to kitchen!")
        print(f"  Table: {table_number}")
        print(f"  Items: {', '.join(items)}")
        if special_notes:
            print(f"  Notes: {special_notes}")

        return {
            "success": True,
            "ticket_id": ticket_id,
            "message": f"Kitchen notified. Ticket {ticket_id} created for table {table_number}.",
        }
💡 Tip: Want to make this real? Replace the KITCHEN_LOG.append() line with an HTTP POST to your KDS or a message to a Slack channel using requests.post(). The agent architecture doesn't change at all — you're just swapping the implementation inside the tool.

Step 5: Connect Agents with Tool Calls

This is where it all comes together. We need to define the tool schemas that Claude uses to understand what parameters each tool expects, and then write the run loop that processes Claude's tool call responses.

Add the tool definitions method first:

restaurant_agents.py — inside RestaurantAgentSystem
    def _get_tool_definitions(self) -> list:
        """Returns the tool schemas Claude uses to call our agent functions."""
        return [
            {
                "name": "check_inventory",
                "description": (
                    "Check if a specific menu item is available in the required quantity. "
                    "Call this for every item in the order before notifying the kitchen."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "item_name": {
                            "type": "string",
                            "description": "The menu item name exactly as ordered (e.g., 'margherita pizza')",
                        },
                        "quantity_requested": {
                            "type": "integer",
                            "description": "How many units of this item the customer ordered",
                        },
                    },
                    "required": ["item_name", "quantity_requested"],
                },
            },
            {
                "name": "notify_kitchen",
                "description": (
                    "Send the confirmed order to the kitchen. Only call this after "
                    "all inventory checks pass. Do not call if any item is unavailable."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "table_number": {
                            "type": "integer",
                            "description": "The table number placing the order",
                        },
                        "items": {
                            "type": "array",
                            "items": {"type": "string"},
                            "description": "List of confirmed items to prepare (e.g., ['2x Margherita Pizza', '1x Caesar Salad'])",
                        },
                        "special_notes": {
                            "type": "string",
                            "description": "Any dietary restrictions or special requests from the customer",
                        },
                    },
                    "required": ["table_number", "items"],
                },
            },
        ]

Now add the agent run loop — this is the core engine that handles everything Claude sends back:

restaurant_agents.py — inside RestaurantAgentSystem
    def _agent_run_loop(self, messages: list, tools: list) -> dict:
        """
        Agentic loop — runs until Claude returns a final text response.
        Handles tool calls by dispatching to the correct agent method.
        """
        max_iterations = 10  # Safety cap to prevent infinite loops
        iteration = 0

        while iteration < max_iterations:
            iteration += 1
            print(f"\n[Orchestrator] Loop iteration {iteration}...")

            response = client.messages.create(
                model=self.model,
                max_tokens=1024,
                system=self.order_agent_system,
                tools=tools,
                messages=messages,
            )

            print(f"[Orchestrator] Claude stop reason: {response.stop_reason}")

            # If Claude is done, extract the final text and return
            if response.stop_reason == "end_turn":
                final_text = next(
                    (block.text for block in response.content if hasattr(block, "text")),
                    "Order processed successfully.",
                )
                return {
                    "status": "completed",
                    "summary": final_text,
                    "kitchen_log": KITCHEN_LOG,
                }

            # Claude wants to use tools — process each tool call
            if response.stop_reason == "tool_use":
                # Append Claude's response to message history
                messages.append({"role": "assistant", "content": response.content})

                tool_results = []
                for block in response.content:
                    if block.type != "tool_use":
                        continue

                    tool_name = block.name
                    tool_input = block.input
                    print(f"[Orchestrator] Tool call: {tool_name} with input: {tool_input}")

                    # Dispatch to the right agent method
                    if tool_name == "check_inventory":
                        result = self._check_inventory(
                            item_name=tool_input["item_name"],
                            quantity_requested=tool_input["quantity_requested"],
                        )
                    elif tool_name == "notify_kitchen":
                        result = self._notify_kitchen(
                            table_number=tool_input["table_number"],
                            items=tool_input["items"],
                            special_notes=tool_input.get("special_notes", ""),
                        )
                    else:
                        result = {"error": f"Unknown tool: {tool_name}"}

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result),
                    })

                # Send tool results back to Claude to continue the loop
                messages.append({"role": "user", "content": tool_results})

        return {"status": "error", "summary": "Max iterations reached.", "kitchen_log": KITCHEN_LOG}

Step 6: Test the Full Pipeline

Now let's add the entry point with a sample order and run the whole thing. Add this at the bottom of restaurant_agents.py, outside the class:

restaurant_agents.py — bottom of file
if __name__ == "__main__":
    # Example JSON input — simulates a customer order from a POS or web app
    sample_order = {
        "table_number": 7,
        "customer_name": "Maria Santos",
        "items": [
            {"name": "margherita pizza", "quantity": 2},
            {"name": "caesar salad", "quantity": 1},
            {"name": "sparkling water", "quantity": 2},
        ],
        "special_notes": "One pizza gluten-free if possible. No anchovies on the salad.",
    }

    system = RestaurantAgentSystem()
    result = system.run(sample_order)

    print("\n" + "=" * 50)
    print("FINAL PIPELINE RESULT")
    print("=" * 50)
    print(json.dumps(result, indent=2))

Run it from your terminal:

terminal
python restaurant_agents.py

Here's what the output looks like on a successful run:

sample output
[Orchestrator] New order received: {
  "table_number": 7,
  "customer_name": "Maria Santos",
  "items": [
    {"name": "margherita pizza", "quantity": 2},
    {"name": "caesar salad", "quantity": 1},
    {"name": "sparkling water", "quantity": 2}
  ],
  "special_notes": "One pizza gluten-free if possible. No anchovies on the salad."
}

[Order Agent] Sending order to Claude for processing...

[Orchestrator] Loop iteration 1...
[Orchestrator] Claude stop reason: tool_use
[Orchestrator] Tool call: check_inventory with input: {'item_name': 'margherita pizza', 'quantity_requested': 2}
[Inventory Agent] ✅ Available: 8 units of 'margherita pizza' in stock, 2 requested.
[Orchestrator] Tool call: check_inventory with input: {'item_name': 'caesar salad', 'quantity_requested': 1}
[Inventory Agent] ✅ Available: 5 units of 'caesar salad' in stock, 1 requested.
[Orchestrator] Tool call: check_inventory with input: {'item_name': 'sparkling water', 'quantity_requested': 2}
[Inventory Agent] ✅ Available: 20 units of 'sparkling water' in stock, 2 requested.

[Orchestrator] Loop iteration 2...
[Orchestrator] Claude stop reason: tool_use
[Orchestrator] Tool call: notify_kitchen with input: {'table_number': 7, 'items': ['2x Margherita Pizza (1 gluten-free)', '1x Caesar Salad (no anchovies)', '2x Sparkling Water'], 'special_notes': 'One pizza gluten-free if possible. No anchovies on the salad.'}

[Kitchen Agent] 🎫 Ticket TKT-1759350212 sent to kitchen!
  Table: 7
  Items: 2x Margherita Pizza (1 gluten-free), 1x Caesar Salad (no anchovies), 2x Sparkling Water
  Notes: One pizza gluten-free if possible. No anchovies on the salad.

[Orchestrator] Loop iteration 3...
[Orchestrator] Claude stop reason: end_turn

==================================================
FINAL PIPELINE RESULT
==================================================
{
  "status": "completed",
  "summary": "Order for table 7 (Maria Santos) has been successfully processed and sent to the kitchen.\n\n**Order Confirmation:**\n- 2x Margherita Pizza (1 gluten-free) ✅\n- 1x Caesar Salad (no anchovies) ✅\n- 2x Sparkling Water ✅\n\n**Kitchen Ticket:** TKT-1759350212\n**Special Notes:** One pizza gluten-free if possible. No anchovies on the salad.\n\nAll items were available in inventory. The kitchen has been notified and will begin preparation.",
  "kitchen_log": [
    {
      "ticket_id": "TKT-1759350212",
      "table": 7,
      "items": ["2x Margherita Pizza (1 gluten-free)", "1x Caesar Salad (no anchovies)", "2x Sparkling Water"],
      "special_notes": "One pizza gluten-free if possible. No anchovies on the salad.",
      "status": "sent_to_kitchen",
      "timestamp": "2026-10-01 14:23:31"
    }
  ]
}

How It Works

Here's the plain-English version of what's happening under the hood. You send Claude a customer order and a set of tools it's allowed to use. Claude reads the order, decides it needs to check inventory, and makes tool calls — one per item.

Your Python code intercepts those tool calls, runs the actual inventory check against your data, and sends the results back to Claude. Claude then decides what to do next — in this case, everything was available, so it calls the kitchen notification tool.

Once the kitchen ticket is sent, Claude has no more tools to call, so it returns a human-readable summary and the loop ends. The orchestrator packages everything into a final result dict that your app can use however it needs to — log it, send it to a database, display it to a manager.

Common Errors and Fixes

Error 1: AuthenticationError — Invalid API Key

anthropic.AuthenticationError: 401 {"type":"error","error