Hotel Booking
One hotel's rooms and rates for the user's stay, with the full price and cancellation terms, handed off to the host's booking flow. Also shows an existing reservation.
Experience · Shares patterns with Slot Picker, Entity Picker, Change Preview
≈ 2,518 tokens per use (tool 1,574 · guidance 350 · typical payload 594)How token costs are estimated
Example: Hotel Booking
Book the Casa do Rio for my Lisbon trip

Casa do Rio
4-star hotelRated 8.9 out of 10, Excellent, from 1,204 reviews. Lisbon
When to use
- The user has one hotel in mind and wants to book it: show its real rooms, rates and nightly prices for their dates.
- The user asks which room at a specific hotel fits their party ("2 adults and a kid").
- The user asks about a reservation they already have (
mode: "confirmation").
When not to use
- The user is still comparing hotels. Show them as Entity Cards first, then this component for the one they pick.
- The agent doesn't have availability and nightly prices from a booking tool. Never invent prices, rooms or availability.
- The user wants to pay in chat. Reserve hands off to the host's booking flow; card details never go through the conversation.
How the agent should use it
- Pass rooms, rates, nightly prices, taxes, fees and deadlines exactly as the source returned them, in one currency. The component adds them up in integer minor units.
- Quote a price for every night of
stay. Extra nights on either side let the user move dates without a new quote. - Mark per-night taxes and fees with
perNight, and fees paid at the hotel (a city tax, a destination fee) withpayAtHotel. - Order rooms with the best fit first. The first room that fits the party is preselected, with its first rate.
- On
reserve, start the host's booking flow with exactly the returnedroomId,rateIdandstay. Never treat it as booked until the source confirms. - On
change-stay, fetch availability for the returned stay and show the component again.
Changing dates and guests
The user can move dates within availability (two native date inputs, limited to minDate and maxDate) and change the number of adults and children. The component never invents a price for the new stay:
| The user changes | Priced live when | Otherwise |
|---|---|---|
| Dates | Every night has a nightly price, and every tax and fee is perNight | "Not priced for these dates." |
| Adults or children | Never: prices were quoted for the original party | "Prices are for 2 adults." |
In those cases Reserve is disabled and a Check availability button returns { action: "change-stay", stay }. Nights listed in availability.unavailable, dates outside the window, and a check-out before check-in show a short message and can't be sent. Each room shows its bed and capacity ("1 queen · Fits 2"); rooms that can't fit the party are disabled and marked "Too small" (or "Sold out" when remaining is 0), and "2 left" shows when remaining is 3 or fewer. Room size and amenities are read to screen readers but not shown, to keep the list scannable.
Price breakdown
The total for the selected rate is the card's headline, with one quiet line under it: "Total for 4 nights · €561.80 now · €32.00 at the hotel" (or "· paid at the hotel" for a pay-at-hotel rate). That line is the card's one disclosure, collapsed by default: it opens each night's price (nights × the average past 7 nights), each tax and fee ("City tax, due at hotel"), the full cancellation terms in hotel time, and where the prices came from (source). Rate options show short terms ("Free cancellation until Oct 17"), breakfast and pay-at-hotel, with the rate's total.
The helpers live in hotel-booking-utils.ts (nightsBetween, stayCoverage, rateTotals, stayPricing, partyFits, freeCancellationRelative, guestSummary, …), pure functions you can reuse on the server.
Confirmation mode
Pass mode: "confirmation" with confirmation (number, bookingState, booked roomId and rateId, guest name) and at least the booked room and rate in rooms. The dates are the headline ("Oct 15 – Oct 18") next to the booking state, then the confirmation number with a copy button, the total, the room and rate, and the cancellation terms as relative text ("Free cancellation for 6 more days"). One Booking details disclosure holds the rest: guest name, check-in and check-out times in hotel time, the price breakdown with what was paid and what's due at the hotel, the full cancellation terms, the address with Directions (an OpenStreetMap search for the address), website and phone links, and the source. The footer has Manage, an Add to calendar icon button and Cancel booking.
Relative text is computed after mount, so server and client render the same markup. Pass now to compare with a fixed moment instead (tests, demos). Deadlines show in the hotel's timeZone.
Cancel booking is irreversible, so it takes two clicks: the first opens "Cancel this booking? Free until Oct 14." with Keep booking and Cancel booking; only the second returns { action: "cancel-booking" }. Manage and Add to calendar return their action once each.
Installation
npx shadcn@latest add https://your-site.com/r/hotel-booking.json
Usage
import { tool } from "ai"
import { hotelBookingTool } from "@/components/genui/hotel-booking.schema"
const tools = {
// No execute: the user's choice is the tool result.
[hotelBookingTool.name]: tool({
description: hotelBookingTool.description,
inputSchema: hotelBookingTool.inputSchema,
}),
}Render the tool call and hand reserve to your booking flow:
import { HotelBooking } from "@/components/genui/hotel-booking"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-book_hotel") return null
return (
<HotelBooking
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) => {
if (result.action === "reserve") openBookingFlow(result)
addToolResult({ tool: "book_hotel", toolCallId: part.toolCallId, output: result })
}}
/>
)
}The agent receives one of:
{
action: "reserve",
roomId: "deluxe-river",
rateId: "deluxe-flex",
stay: { checkIn: "2026-10-19", checkOut: "2026-10-23", adults: 2, children: 0, rooms: 1 },
total: { amount: 766.58, payNow: 734.58, payAtHotel: 32, currency: "EUR" },
}
{ action: "change-stay", stay: { checkIn: "2026-10-20", checkOut: "2026-10-24", adults: 2, children: 0, rooms: 1 } }
{ action: "dismiss" }
// Confirmation mode
{ action: "manage" }
{ action: "add-to-calendar" }
{ action: "cancel-booking" }With LangGraph
Push it from a graph node with ui.push({ name: "book_hotel", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
When a conversation is reloaded, pass the stored result back as result, or an array of results when several confirmation actions were taken. A reserve renders a read-only summary (room, total and stay, "Sent to booking"), change-stay shows the stay that was sent, and cancel-booking shows "Cancellation requested" with the actions locked. It never calls onResult again. With the AI SDK, that's the tool call's output:
<HotelBooking
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
mode | "select" | "confirmation" | |
hotel | { name, address, city, stars?, rating?, checkInFrom?, checkOutUntil?, timeZone, phone?, url?, image? } | rating: { score, outOf, label?, count? }. url and image.src http(s) only. Data-bound |
stay | { checkIn, checkOut, adults, children?, rooms? } | The stay the prices were quoted for. Data-bound |
availability | { minDate, maxDate, unavailable? }? | Select mode: where the user may move dates. Omit to lock the dates. Data-bound |
rooms | { id, name, bed, sizeM2?, maxGuests, amenities?, remaining?, rates }[] | 1–8, best fit first. Up to 6 amenities. Data-bound |
rooms[].rates | { id, label, nightly, taxes?, fees?, refundable, freeCancelUntil?, breakfast?, payment }[] | nightly: { date, amount }[] for all rooms. taxes: { label, amount, perNight? }[]. fees: add payAtHotel?. payment: "now" | "at-hotel". Data-bound |
currency | string | ISO 4217 code for every amount |
confirmation | { number, bookingState, roomId, rateId, guestName }? | Confirmation mode. bookingState: confirmed, pending, cancelled. Data-bound |
source | { label, url? } | Where the prices or booking came from. Data-bound |
Host props: status (streaming, ready, submitting, reserved, change-requested, dismissed, cancel-requested, done), now, onResult, result and className.