Restaurant Reservation

Book a table from a restaurant's real availability, or show an existing reservation.

Experience · Shares patterns with Slot Picker, Generated Form

≈ 2,357 tokens per use (tool 1,708 · guidance 331 · typical payload 318)How token costs are estimated

Example: Restaurant Reservation

Book a table for 4 at Cal Pep on Friday

Cal Pep

Catalan seafood tapasEl Born

Party size
4 guests
Seating
Day
Times on Friday, October 16, Europe/Madrid (GMT+2)
Lunch
Dinner

Pick a time

When to use

  • The user wants a table at a specific restaurant: show its real open times for their party.
  • The user asks what's free at a time or on a day at one restaurant.
  • The user asks about an existing reservation (mode: "confirmation").

When not to use

  • The user is still choosing a restaurant. Show the options as Entity Cards first.
  • The agent hasn't fetched availability for the party size. Never invent times.
  • It's a generic appointment, not a restaurant. Use the Slot Picker.

How the agent should use it

  • Pass days, slots, areas and notes exactly as the source returned them, for the size in party.size. Include fully booked days with empty slots; they show faded, with a "Fully booked" tooltip.
  • Set defaultDate to the day the user asked for.
  • Only list occasions and dietary options the booking system accepts. Omit requests otherwise.
  • On reserve, start the host's booking flow with exactly the returned date, time, size and area. Never treat it as booked until the source confirms. The provider collects any deposit.
  • On change-party, fetch availability for the new size and show the component again.
  • On cancel-reservation, cancel through the booking tool and confirm in one line. On dismiss, do nothing.
  • Never ask for card details in chat.

Interaction

Select mode. The party size stepper and the seating filter share one row. The stepper starts at party.size; changing it never invents availability: the times are hidden and the card says "Availability is for 4 guests.", with a Check for 6 button that returns change-party. Days are a calendar strip (a radio group; arrow keys move between them): the month, then a serif date, the weekday and a tick per day, scrolling sideways with arrows when there are more. Days with no slots are disabled and faded, with a "Fully booked" tooltip. Times are grouped into Lunch (before 16:00) and Dinner. Times are compact buttons; each one's seating areas show in a tooltip while the filter is on Any and the restaurant has more than one area; slot notes from the source ("2-hour limit") are read to screen readers and shown once the time is picked. The picked time is the card's headline, with the day, party, seating and slot note under it, and the deposit for the whole party when there is one ("€80 deposit, refundable until 24 h before"). Choosing a time with several areas adds a small seating choice (with "No preference"). Occasion, dietary needs, the note, the policies (cancellation, deposit, dress code, table time) and the source sit behind one Requests and policies disclosure, collapsed by default. The restaurant's time zone is announced with the times for screen readers. Reserve stays disabled until a time is picked and never books or charges anything: it hands the choice to the host, and the provider collects any deposit.

Confirmation mode. The time is the headline, next to the booking state (a waitlist shows "On the waitlist"), with one line under it for the date, party, seating and how far away it is ("In 2 days", from the viewer's clock after mount or the now prop), the waitlist position when the notes give one, then the confirmation number with a copy button. One Reservation details disclosure holds the guest name, notes, policies, the address with Directions, Website and a tel: phone link, and the source. The footer has Modify, an Add to calendar button (hidden on the waitlist) and a quiet red Cancel reservation (or Leave waitlist) on the left, which asks for a second, explicit click before it returns cancel-reservation.

All times are wall-clock HH:mm in restaurant.timeZone and are formatted with Intl in the viewer's locale.

Installation

npx shadcn@latest add https://your-site.com/r/restaurant-reservation.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { restaurantReservationTool } from "@/components/genui/restaurant-reservation.schema"

const tools = {
  // No execute(): the call is completed by the user's choice in the UI.
  [restaurantReservationTool.name]: tool({
    description: restaurantReservationTool.description,
    inputSchema: restaurantReservationTool.inputSchema,
  }),
}
components/message.tsx
import { RestaurantReservation } from "@/components/genui/restaurant-reservation"

function ToolPart({ part, addToolResult }) {
  if (part.type !== "tool-reserve_table") return null

  return (
    <RestaurantReservation
      {...part.input}
      status={part.state === "input-streaming" ? "streaming" : undefined}
      result={part.state === "output-available" ? part.output : undefined}
      onResult={(result) =>
        addToolResult({
          tool: "reserve_table",
          toolCallId: part.toolCallId,
          output: result,
        })
      }
    />
  )
}

The agent receives one of:

{ action: "reserve", date: "2026-10-16", time: "20:00", size: 4, area: "outdoor", occasion: "Birthday", dietary: ["Vegetarian"], note: "A high chair, please" }
{ action: "change-party", size: 6 }
{ action: "dismiss" }
{ action: "modify" }              // confirmation mode
{ action: "add-to-calendar" }     // confirmation mode
{ action: "cancel-reservation" }  // confirmation mode, after the second click

area, occasion, dietary and note are left out when the user didn't choose them.

With LangGraph

Push it from a graph node with ui.push({ name: "reserve_table", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

Pass the stored result back as result (or an array of results, for several confirmation actions). Select mode shows what was sent ("Sent to booking", "Asked for tables for 6 guests", "Cancelled. Nothing was booked."); confirmation mode marks the used actions ("Cancellation requested"). It never calls onResult again.

<RestaurantReservation
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
mode"select" | "confirmation"
restaurant{ name, cuisine?, priceLevel?, address, neighborhood?, timeZone, rating?, phone?, url?, logo? }Data-bound. priceLevel 1–4; rating is { score, outOf, count? }; count is read to screen readers only. logo is { url, darkUrl?, alt? }, the restaurant's logo (http(s) or a same-origin path, never a photo), shown in a white 48px tile beside the name in place of the fork-and-knife icon; it falls back to the icon if it fails to load. Never invent a URL.
party{ size, min?, max? }Data-bound. The size the availability was fetched for. Stepper limits default to 1–20
days{ date, slots }[]Data-bound. Up to 7 days. Empty slots shows the day as full
defaultDatestring?YYYY-MM-DD, the day shown first
policies{ cancellation?, deposit?, dressCode?, maxDurationMinutes? }Data-bound. deposit is { amount, perPerson?, refundableUntil? }
currencystring?ISO 4217. Required with a deposit
requests{ occasions?, dietary?, allowNote? }Data-bound. What the booking system accepts. Note is up to 200 characters
confirmation{ number, bookingState, date, time, size, area?, guestName, notes? }Data-bound. Confirmation mode. bookingState: confirmed, pending, waitlist, cancelled
source{ label, url? }Data-bound. Where the availability or reservation came from

Slot

FieldTypeNotes
timestringData-bound. HH:mm, restaurant local time
areas("indoor" | "outdoor" | "bar" | "counter" | "private")[]?Data-bound. Seating open at this time
notestring?Data-bound. e.g. "Bar seating only", "2-hour limit"

Host props: status (streaming, ready, submitting, reserved, change-requested, dismissed, cancel-requested, done), onResult, result, now and className.

Source

On this page