Ranked Recommendations

A shortlist ranked for the user's situation, with reasons, trade-offs and the constraints each option meets or misses.

Pattern

≈ 1,974 tokens per use (tool 1,166 · guidance 293 · typical payload 515)How token costs are estimated

Example: Ranked Recommendations

Which neighbourhood should we stay in for a week in Lisbon?

Where to stay in Lisbon

Ranked for a quiet, walkable base for two adults for a week.

Must have: Walkable, Quiet at night. Nice to have: Near the metro.

  1. A leafy Lisbon street beside a garden square

    Rank 1: Top pick: Príncipe Real

    Leafy and residential, above Bairro Alto. Gardens, Independent shops, Hilly

    Score 8.8 out of 10
    • Calm at night, a short walk to Chiado and the river.
    • Garden squares and cafés to come back to.
    • The walk back up from the river is steep.
    • Meets must-have: Walkable
    • Meets must-have: Quiet at night
    • Partly meets nice-to-have: Near the metro
  2. A quiet flat street with a yellow tram

    Rank 2: Campo de Ourique

    Local neighbourhood with a food market. Flat streets, Market, Tram 28

    Score 8.1 out of 10
    • One of the flattest central areas, easy to walk.
    • Far from the metro; tram or bus instead.
    • Meets must-have: Walkable
    • Meets must-have: Quiet at night
    • Partly meets nice-to-have: Near the metro
  3. A busy pedestrian street in central Lisbon

    Rank 3: Chiado and Baixa

    The centre, between the hills. Central, Metro on the doorstep

    Score 6.6 out of 10

    Misses a must-have

    • The most central, with a metro station.
    • Busy until late.
    • Meets must-have: Walkable
    • Misses must-have: Quiet at night
    • Meets nice-to-have: Near the metro

When to use

  • The user asks which option is best for them: where to stay, which tool to buy, who to interview first, what to cook.
  • The agent compared a few real options against the user's situation and can say why each one fits or doesn't.

When not to use

  • There's one clear answer. Say it in a sentence.
  • The user wants to compare many attributes side by side without a verdict. Use a Data Table.
  • The agent only wants to show items to browse or buy. Use the Entity Card, which describes items; this component explains why to choose them.
  • The agent hasn't looked anything up. Never rank options from memory.

How the agent should use it

  • Write basis as one sentence naming what the ranking optimizes for, in the user's terms: "Ranked for a 2-person team under $20/month that needs a mobile app". It's read to screen readers; the card doesn't repeat it visually, and neither does it show a must/nice legend (each item's checks carry the constraint names).
  • Rank 1 is the top pick. Its name is the card's largest text and it shows all its reasons and trade-offs; the other items show their first reason and first trade-off, so put the strongest one first. Give each item 1–3 reasons specific to this user, not generic praise. subtitle and tags are read to screen readers only.
  • Be honest. Add up to two trade-offs per item, and mark a missed must-have as false rather than leaving it out. The item then shows "Misses a must-have".
  • Fill meets for every constraint, using "partial" when it's met only in part. Constraints without a value aren't shown for that item.
  • Cite with [n] in reasons and trade-offs, where n is the 1-based position in sources, the same way as in Citations. Only cite pages the agent retrieved.
  • Only make it selectable when the pick changes what happens next, such as starting a trial or searching for stays.
  • Lifecycle: set fetchedAt and staleAfterMs on the call when the ranking depends on live data such as prices or availability, so the Renderer flags it as stale. Use supersede when the agent re-ranks after a more result. expiresAt isn't needed unless the items are held offers.

Interaction

ControlSendsNotes
A radio or checkbox per item—Shown when selectable is single or multiple
Submit (submitLabel)selectDisabled until something is chosen. Defaults to "Choose this" or "Choose these"
Show other optionsmoreOpens an optional "what should be different?" box first. Shown in every mode, display-only included
Not nowdismissOnly when the list is selectable

Not now is the ✕ icon button. After a decision the list locks, the footer shows a record such as "Chose Tasklane", and focus moves to it. The footer announces it politely.

Installation

npx shadcn@latest add https://your-site.com/r/ranked-recommendations.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { rankedRecommendationsTool } from "@/components/genui/ranked-recommendations.schema"

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

The agent receives one of:

{ action: "select", itemIds: ["tasklane"] }
{ action: "more", reason: "Something with built-in time tracking" }
{ action: "more" }
{ action: "dismiss" }

On select, act on the returned ids only. On more, search again using the reason, and show a new ranking. On dismiss, stop.

The helpers in ranked-recommendations-utils.ts (sortByRank, summarizeConstraints, formatScore, formatPrice, toggleSelection, splitMarkers) are pure, so the server can reuse them, for example to check which items miss a must-have before ranking.

With LangGraph

Push it from a graph node with ui.push({ name: "recommend", 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 user's choice (or the "other options" request and its reason) and never calls onResult again.

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

Schema

PropTypeNotes
titlestring?"Where to stay in Lisbon"
basisstring?One sentence: what the ranking optimizes for. Screen readers only
constraints{ id, label, kind }[]?Up to 6. kind: must or nice. Shown per item as checks; the legend is read to screen readers
itemsRecommendationItem[]Data-bound. 2–6 options, shown in rank order
sources{ id, title, url }[]?Data-bound. Up to 10, cited as [n] in reasons
selectable"none" | "single" | "multiple"Default none
submitLabelstring?Verb for the submit button, e.g. "Start a trial"

RecommendationItem

FieldTypeNotes
idstringReturned in itemIds
ranknumber1 is the top pick
namestringData-bound
subtitlestring?One line of context. Screen readers only
image{ src, alt }?http(s) or data:image
score{ value, outOf }?Shown as "8.6 / 10". outOf defaults to 10
whystring[]1–3 plain sentences; may include [n] markers
tradeoffsstring[]?Up to 2 honest downsides
meetsRecord<constraintId, true | false | "partial">?Data-bound. Check, cross or half icon per constraint, with spoken text
price{ amount, currency, period? }?Data-bound. "$10 / user/month"; 0 shows "Free"
urlstring?http(s) only; opens in a new tab
tagsstring[]?Up to 3 short facts. Screen readers only

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

Source

On this page