Car Rental

Real rental car offers for a trip to filter and pick, with cover, extras, the young driver fee and the full cost, handed off to the host's booking flow. Also shows an existing rental.

Experience · Shares patterns with Entity Picker, Option Builder, Change Preview

≈ 3,412 tokens per use (tool 1,875 · guidance 398 · typical payload 1,139)How token costs are estimated

Example: Car Rental

Rent a car at Lisbon airport for next week

Lisbon Airport (LIS), Terminal 1

Mon, Oct 12, 10:00 – Mon, Oct 19, 10:00 (local times) · 7 days

Filter and sort cars

4 cars

Car
  • Lumen Car Hire · rated 7.6 out of 10, from 842 reviews · Free cancellation · Credit card only

    Cover
    Extras
    • Child seat · €7.00 / day
      0
    • Additional driver · €8.50 / day
      0
    • GPS · €35.00 per rental
      0
Total: €138.60

When to use

  • The user wants to rent a car for a trip and the agent has real offers from a rental tool: show them to compare and pick.
  • The user asks for a kind of car ("something automatic for 5 people with luggage"): preset defaultFilters from the request.
  • The user asks about a rental they already have (mode: "confirmation").

When not to use

  • The agent doesn't have offers and prices from a rental tool. Never invent offers, prices, cover or extras.
  • The user wants a taxi, transfer or ride. This is for self-drive rentals.
  • 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 offers, prices, cover, extras, deposits and deadlines exactly as the source returned them, in one currency. The component adds them up in integer minor units.
  • Quote prices for the given pickup, dropoff and driver age. Pass driver.age when the user gave it, so the young driver fee applies.
  • Put the included cover first in insurance, and mark add-ons paid at the counter with payAtDesk.
  • Preset defaultFilters only from what the user asked for, e.g. { transmission: "automatic", minSeats: 5 }.
  • On reserve, start the host's booking flow with exactly the returned offerId, insuranceId and extras. Never treat it as booked until the source confirms.
  • On change-trip, fetch offers for the returned trip and show the component again. If the location changed, resolve its time zone again.

Filters and offers

With 4 or more offers (or filters: true), one row of filters sits above the list: transmission ("Any transmission", Automatic, Manual), seats ("5+ seats") and sort (lowest price or best rated). More filters opens category chips, Unlimited mileage and Free cancellation; it starts open when defaultFilters presets one of them, and shows how many are on. Filtering runs locally over the given offers, and the match count ("4 of 5 cars") is announced to screen readers. Clear filters resets them, including the preset. defaultFilters only applies while the filters are shown.

Each offer is a radio row: category and model ("VW Golf or similar"), the total, and one line with the specs that drive the choice (seats, bags, transmission, plus the EV range or a mileage cap when they apply). The first visible offer is preselected.

Cover, extras and fees

For the selected offer:

  • One quiet line with the supplier, its rating, "Free cancellation" while it's still free, and "Credit card only" when the deposit needs one.
  • Cover is a radio group: "Basic · €1,200 excess", with the added cost or "Included".
  • Extras have quantity steppers up to maxQuantity (default 1), priced per day or per rental.
  • The young driver fee is added to the total automatically when driver.age is under underAge. The rule ("Drivers under 25 pay $27.00 a day") is in the price details either way.

Total and price details

The total for the selected offer and options is the largest figure on the card, with one line for the split when part is paid at the desk ("$268.40 now, $108.00 at the desk"). Price details opens the breakdown and the fine print in one place:

  • the base price (with the price per day), the chosen cover, each extra and the young driver fee;
  • fuel type, A/C and doors, "Guaranteed model" when orSimilar is false, mileage ("1,500 km included, then €0.25/km"), fuel policy and how to reach the desk;
  • the free cancellation deadline, the deposit ("€300 held on a credit card at pick-up") and the young driver fee rule;
  • where the prices came from (source).

The offer rows show each car's base price; the total adds the chosen cover, extras and the young driver fee. Rental days are counted in 24-hour blocks, and a block that runs more than 59 minutes over counts as another day. In one time zone, days are measured on the local clock, so 10:00 to 10:00 is whole days even across a DST change.

Add-ons follow payAtDesk; when it's omitted they're paid with the booking, unless the whole base price is paid at the desk. Items paid at the desk read "Due at desk: Child seat" when something is also paid now. The helpers live in car-rental-utils.ts (rentalDays, rentalTotals, filterOffers, sortOffers, mileageText, freeCancellationRelative, …), pure functions you can reuse on the server.

Changing the trip

Prices depend on places, times and the driver's age, so the component never reprices a different trip. Change trip in the header opens inputs for the pick-up location and time, the drop-off (or "Return to the same location"), and the driver's age. Prices change with the trip, so the agent checks them again. Check prices returns { action: "change-trip", pickup, dropoff, driverAge } with times in local time with an offset, and is only enabled when something changed and the drop-off is after the pick-up.

Confirmation mode

Pass mode: "confirmation" with confirmation (number, bookingState, booked offerId, insuranceId, extras, driver name) and at least the booked offer in offers. The confirmation number is the focal point, with a copy button, and the booking state sits next to the title. Below: the car and supplier, the driver, pick-up and drop-off in local time with how to reach the desk ("Shuttle from Terminal 3, every 15 min") and a Directions link (an OpenStreetMap search for the pick-up location), then the total and the cancellation terms. Details opens the price paid and due, the cover and its excess, the deposit, relative cancellation text ("Free cancellation for 2 more days") and the source.

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

Cancel rental is irreversible, so it takes two clicks: the first opens "Cancel this rental? Free until Oct 10, 23:59." with Keep rental and Cancel rental; only the second returns { action: "cancel-rental" }. Manage (the primary action) and Add to calendar (an icon button) return their action once each.

Installation

npx shadcn@latest add https://your-site.com/r/car-rental.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { carRentalTool } from "@/components/genui/car-rental.schema"

const tools = {
  // No execute: the user's choice is the tool result.
  [carRentalTool.name]: tool({
    description: carRentalTool.description,
    inputSchema: carRentalTool.inputSchema,
  }),
}

Render the tool call and hand reserve to your booking flow:

components/message.tsx
import { CarRental } from "@/components/genui/car-rental"

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

  return (
    <CarRental
      {...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: "rent_car", toolCallId: part.toolCallId, output: result })
      }}
    />
  )
}

The agent receives one of:

{
  action: "reserve",
  offerId: "atlas-compact",
  insuranceId: "full",
  extras: [{ id: "child-seat", quantity: 1 }],
  total: { amount: 329, payNow: 280, payAtDesk: 49, currency: "EUR" },
}
{
  action: "change-trip",
  pickup: { location: "Lisbon Airport (LIS), Terminal 1", at: "2026-10-13T10:00:00+01:00", timeZone: "Europe/Lisbon" },
  dropoff: { location: "Porto Airport (OPO)", at: "2026-10-20T10:00:00+01:00", timeZone: "Europe/Lisbon" },
  driverAge: 30,
}
{ action: "dismiss" }
// Confirmation mode
{ action: "manage" }
{ action: "add-to-calendar" }
{ action: "cancel-rental" }

With LangGraph

Push it from a graph node with ui.push({ name: "rent_car", 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 ("Sent to booking"), change-trip shows the trip that was sent, and cancel-rental shows "Cancellation requested" with the actions locked. It never calls onResult again. With the AI SDK, that's the tool call's output:

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

Schema

PropTypeNotes
mode"select" | "confirmation"
pickup{ location, at, timeZone }at: ISO 8601, with an offset or in local time. timeZone: IANA. Data-bound
dropoff{ location, at, timeZone }A different location shows "One-way". Data-bound
driver{ age? }?The main driver's age, for the young driver fee
offers{ id, supplier, category, model, orSimilar?, seats, bags, doors?, transmission, fuel, rangeKm?, airCon?, mileage, fuelPolicy?, pickupType?, pickupNote?, rating?, price, deposit?, insurance, extras?, youngDriverFee?, freeCancelUntil? }[]1–12. bags: { large, small }. transmission: automatic, manual. fuel: petrol, diesel, hybrid, electric. mileage: { unlimited, includedKm?, extraPerKm? }. fuelPolicy: full-to-full, same-to-same, prepaid. pickupType: in-terminal, shuttle, meet-and-greet. rating: { score, outOf, count? }. Data-bound
offers[].price{ total, perDay?, payNow?, payAtDesk? }Base price for the quoted trip. payNow + payAtDesk = total; omit both when it's all paid now
offers[].insurance{ id, label, included, excess, pricePerDay?, payAtDesk? }[]1–4, the included one first
offers[].extras{ id, label, pricePerDay?, priceTotal?, maxQuantity?, payAtDesk? }[]?priceTotal is per unit for the rental and wins over pricePerDay
offers[].youngDriverFee{ underAge, perDay, payAtDesk? }?Applied when driver.age < underAge
offers[].deposit{ amount, card }?card: credit-only, credit-or-debit
currencystringISO 4217 code for every amount
filtersboolean?Default: on with 4 or more offers
defaultFilters{ transmission?, minSeats?, categories?, unlimitedMileage?, freeCancellation?, sort? }?Preset from the user's request
confirmation{ number, bookingState, offerId, insuranceId?, extras?, driverName }?Confirmation mode. bookingState: confirmed, pending, cancelled. Data-bound
source{ label, url? }Where the offers or rental came from. Data-bound

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

Source

On this page