Flight Offers

A ranked list of bookable flights from a fare search, with local times, stops, fare and bags, the cheapest and fastest marked, and one to pick.

Experience · Shares patterns with Comparison Matrix, Flight Ticket

≈ 3,117 tokens per use (tool 1,631 · guidance 272 · typical payload 1,214)How token costs are estimated

Example: Flight Offers

Flights from Tel Aviv to Athens for the family in November

Tel Aviv to Athens

Thu, Nov 12 – Thu, Nov 19

Sort flights by
Choose a flight

When to use

  • The user searched for flights and should compare a few bookable options and pick one: "flights to Athens for the family in November", "cheapest nonstop to Lisbon next week".

When not to use

  • The user already booked. Use a Flight Ticket.
  • The user asks about a flight in progress. Use Flight Status.
  • The agent has no live fares. Search first; never invent prices or times.
  • More than 10 options. Narrow the search (nonstop, time of day) first.

How the agent should use it

  • Every price, time and fare rule comes from the fare search result. price is the total for all passengers in the search, taxes included.
  • Send times with the airport's offset and each airport's IANA timezone. Times show local to each airport, so a 21:30 departure from JFK reads 21:30 for every viewer, and an arrival on the next local day gets a "+1".
  • Order offers by what the user asked for (cheapest, fastest) and set sort to match.
  • Pass search.passengers so the footer says who the prices are for: "Prices for 3 adults, 1 child, 1 infant".
  • Act only on a select result, then confirm the fare before booking: prices change.
  • Lifecycle: fares are live, so set fetchedAt and staleAfterMs (about 10 minutes) on the call. When the provider holds the fares until a deadline, set expiresAt too. Use supersede when the agent re-runs the search with new dates or filters.

Interaction

ElementBehavior
OfferRank, airline mark (its code on a tile, no logos), airline, stops and total price. One row per direction (side by side on a wide card): departure and arrival times in each airport's zone, the airport codes, and the door-to-door duration on a dashed line. Connections add "1 stop · LCA 1h 05m". One meta line: fare, bags, trip type, "Refundable" when true, and "2 seats left" when 5 or fewer
Cheapest, FastestComputed from the offers (total price; total travel time across both directions). Ties mark every tied offer; nothing is marked when there's one offer or all are equal. Text badges, not color
SortPrice, Duration or Departure re-sorts the list locally, stable so ties keep the agent's order. Rank numbers follow the visible order. The active sort starts at sort; without it the agent's order stands and no sort is pressed
ChooseWith selectable (the default) each offer is a radio card: click anywhere on it, or use the arrow keys. The chosen offer gets a ring and a check in place of its rank. Select flight sends it
Show moreWith more than 2 offers, the first 2 show and the rest sit behind "Show N more". This is a documented exception to expanded-by-default: the top of a ranked list is the answer. A chosen offer stays visible when the list is collapsed. The list flows with the page; nothing scrolls inside the card

While streaming, offers appear one at a time in the agent's order. Sort and the badges wait for the whole list, so nothing jumps.

The helpers in flight-offers-utils.ts (sortOffers, cheapestIds, fastestIds, legMinutes, layoverText, legDayOffset, pricesForText) are pure, so the server can reuse them, for example to check that an "it's the cheapest" reply matches the data.

Installation

npx shadcn@latest add https://your-site.com/r/flight-offers.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { flightOffersTool } from "@/components/genui/flight-offers.schema"

const tools = {
  // No execute(): the call is completed by the user's choice.
  [flightOffersTool.name]: tool({
    description: flightOffersTool.description,
    inputSchema: flightOffersTool.inputSchema,
  }),
}
components/message.tsx
<FlightOffers
  {...part.input}
  status={part.state === "input-streaming" ? "streaming" : undefined}
  result={part.state === "output-available" ? part.output : undefined}
  onResult={(result) =>
    addToolResult({ tool: "compare_flights", toolCallId: part.toolCallId, output: result })
  }
/>

The agent receives:

{ action: "select", offerId: "mr-412" }

With LangGraph

Push it from a graph node with ui.push({ name: "compare_flights", props: { toolCallId, ...args } }, { message }), then interrupt() until the user's result arrives. Render the thread's UI messages with the Renderer.

Restoring from history

Pass the stored result back as result. The list renders read-only, with the chosen offer ringed and checked (shown even if it's past the first 5), and the footer shows "Selected Meridian Air · $1,486". Sorting still works. It never calls onResult again.

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

Schema

PropTypeNotes
searchSearch?Titles the card ("Tel Aviv to Athens", dates) and says who the prices are for
currencystringISO 4217, e.g. USD
offersOffer[]Data-bound. 1–10, best first
sort"price" | "duration" | "departure"?What offers is sorted by; marks that sort as active
selectablebooleanDefault true: offers are radio cards with a Select flight button
source{ label, url? }?Data-bound. Where the fares came from, a small link in the footer

Search

FieldTypeNotes
from, tostringAs the user said it: "Tel Aviv", "Athens"
departDatestringYYYY-MM-DD
returnDatestring?Omit for one way
passengers{ adults, children?, infants? }"Prices for 3 adults, 1 child, 1 infant"
cabineconomy | premium | business | first?Shown in the header unless economy

Offer

FieldTypeNotes
idstringReturned on select
airline{ name, code }The code is the airline's mark
pricenumberData-bound. Total for all passengers
farestring?"Basic", "Standard", "Flex"
bags{ carryOn, checked }?"No carry-on", "Carry-on + 1 checked bag"
legs{ segments: Segment[] }[]1 leg = one way, 2 = round trip. 1–4 segments per leg; layovers are computed
refundableboolean?"Refundable" in the meta line when true
seatsLeftnumber?Shown when 5 or fewer

Segment

FieldTypeNotes
flightNumberstringShown under the flight's times, and read in the leg summary
operatedBystring?The airline that flies it when it isn't the seller. Shown as "Operated by …"
from, to{ code, city?, timeZone }IATA code, an optional city shown under it, and the IANA timezone
departAt, arriveAtstringISO 8601 with the airport's offset. Without one, local time at the airport

Host props: status (streaming, ready, submitting, selected), onResult, result and className.

Source

On this page