Flight Ticket
A booking or boarding pass the agent found, with local times, connections, seats and handoff actions.
Experience · Shares patterns with Timeline, QR Code
≈ 1,999 tokens per use (tool 1,492 · guidance 321 · typical payload 186)How token costs are estimated
Example: Flight Ticket
When is my flight to New York?
Fri, Oct 9Check-in opens in 3 h
Flight AF 1755Lisbon (LIS) to New York (JFK), direct, flight AF 1755.
- Seat
- 23A
- Class
- Economy
When to use
- The user asks about a flight they booked and the agent found the booking or boarding pass in their email or airline account: when it leaves, their seat, the gate, the booking reference.
- They want to see their boarding pass, check in, or act on the booking.
When not to use
- The agent doesn't have the booking from a tool result. Never invent times, seats, gates or references.
- The user asks whether a flight is on time or where it is now. Use Flight Status.
- The user is still searching for flights to buy.
How the agent should use it
- Pass every time with the airport's UTC offset and its IANA timezone, exactly as the source gives them. Never convert times to the user's timezone.
- Use one segment per flight, in order. The component works out each layover and flags connections under an hour.
- Use
passType: "boarding-pass"only when the source has an issued pass. Never generate barcode contents. - Every action is a handoff. On
check-in,add-to-wallet,choose-seat,manageorshare, open the airline's or your own flow. Never check in or change seats without confirming.
Times and timezones
Every time shows in its airport's own timezone, on a 24-hour clock as boarding passes print it: a flight from Lisbon at 07:40 reads 07:40 for a viewer anywhere. Screen readers hear the date, the city and its offset: "Departs Friday, October 9, 07:40 Lisbon time (GMT+1)". An arrival on a later local date gets a "+1" marker.
Times may carry an offset (2026-10-09T07:40:00+01:00, exact) or not (2026-10-09T07:40, read as wall-clock time at that airport, DST included). Formatting uses Intl only, no date library; the helpers are in flight-ticket-utils.ts (segmentMinutes, layovers, dayOffset, formatClock, defaultActions, …).
Relative text ("Check-in opens in 3 h", "Boarding in 40 min", "Departs in 1 day 3 h") is computed after mount, so server and client render the same markup, and refreshes every minute. Pass now to measure from a fixed moment instead (tests, demos, a server-rendered snapshot).
Layout
The card reads like a pass: one glance gives the time, the seat and the gate. It is drawn on a solid muted blue, an approved exception to GenUI's hollow cards for ticket documents; the action footer stays neutral. Pass the airline's light wordmark as airline.logoDark (and logo for reference) and it replaces the name in the header; without it the name shows as text.
- Header: the airline and the booking reference (with a copy button), then one quiet line with the departure date and the state that matters next. While check-in is still closed it is a live countdown (
13d 01:19:45, digits rolling, to the second); otherwise "Check-in is open", "Checked in" or, on a boarding pass, "Boarding in 40 min". On a single flight the flight number sits at the right of that line. - Route: each airport reads city, big airport code, then its local time and terminal. Between the codes a dashed line with a plane and the flight time. With connections the line becomes the journey bar: one pill per flight and a hatched segment per layover with the connection airport's code, widths following the minutes. The bar is decorative; the itinerary carries the same facts as text.
- Pass fields: gate, seat and class, plus boarding time and group on a boarding pass, set large.
- Itinerary for connections: a branch of dots joined by a line, one dot per flight ("AF 1325 06:00 LIS – 08:40 LHR") and a small one per layover, with a "Short connection" badge under an hour or "Change airports".
- Passengers with their seats when there's more than one.
Boarding passes
A boarding pass gets a ticket stub: a perforated divider (a dashed border with two half-circle notches drawn in CSS), the passenger, and a 96px neutral code placeholder with "Scan in airline app" beside it.
The component never draws a scannable code. Barcode contents are security-sensitive and must come from the airline. To show a real pass, render your own wallet pass or the airline's code next to it, or hand off with add-to-wallet.
Actions
Buttons come from a closed set: check-in, add-to-wallet, choose-seat, manage and share. When the agent passes none, the defaults follow the state:
| State | Default actions |
|---|---|
| Boarding pass | Add to wallet, Share |
| Booking, check-in open | Check in, Choose seat, Manage booking |
| Booking, checked in | Add to wallet, Manage booking |
| Booking, otherwise | Manage booking, Choose seat |
The first two show as buttons; any others go into a "More actions" menu. Check in is hidden once the passenger is checked in. Each action returns { action } once, locks its button, and the footer confirms it quietly ("Opening check-in…"); the other actions stay available. Otherwise the footer shows the source.
Installation
npx shadcn@latest add https://your-site.com/r/flight-ticket.json
Usage
import { tool } from "ai"
import { flightTicketTool } from "@/components/genui/flight-ticket.schema"
const tools = {
// No execute: the user's first action is the tool result.
[flightTicketTool.name]: tool({
description: flightTicketTool.description,
inputSchema: flightTicketTool.inputSchema,
}),
}Render the tool call and hand each action to your own flow:
import { FlightTicket } from "@/components/genui/flight-ticket"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-show_flight_ticket") return null
return (
<FlightTicket
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) => {
if (result.action === "add-to-wallet") openWalletPass(part.input.bookingRef)
if (result.action === "check-in") openCheckIn(part.input.bookingRef)
if (part.state !== "output-available") {
addToolResult({ tool: "show_flight_ticket", toolCallId: part.toolCallId, output: result })
} else {
sendMessage({ text: `Flight ticket action: ${JSON.stringify(result)}` })
}
}}
/>
)
}The agent receives one of:
{ action: "check-in" }
{ action: "add-to-wallet" }
{ action: "choose-seat" }
{ action: "manage" }
{ action: "share" }With LangGraph
Push it from a graph node with ui.push({ name: "show_flight_ticket", 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 actions were taken. Those actions show as done and stay locked; they never call onResult again.
<FlightTicket
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
bookingRef | string | The PNR, 5–8 letters or digits, e.g. "K7Q2LM". Copy button included. Data-bound |
airline | { name, code } | Data-bound |
passengers | { name, seat?, class? }[] | 1–9. class: economy, premium, business, first. Data-bound |
segments | Segment[] | 1–4 flights in order. Data-bound |
segments[].from / to | { code, city, terminal?, gate?, timeZone } | IATA code and IANA timezone; gate on departure only |
segments[].departAt / arriveAt | string | ISO 8601, with the airport's offset or as local time |
segments[].duration | number? | Minutes; computed when absent |
segments[].boardingAt, boardingGroup, operatedBy | string? | |
passType | "booking" | "boarding-pass" | |
checkInOpensAt | string? | ISO 8601. Data-bound |
checkedIn | boolean? | Data-bound |
barcode | { format: "qr" | "pdf417" | "aztec", label? }? | Placeholder only; never the payload |
source | { label, url? } | url http(s) only. Data-bound |
actions | FlightTicketAction[]? | Up to 5; the first is primary. Defaults by state |
Host props: status (streaming, ready, submitting, done), now, onResult, result and className.