Ranked Recommendations
A shortlist ranked for the user's situation, with reasons, trade-offs and the constraints each option meets or misses.
≈ 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.
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
basisas 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.
subtitleandtagsare read to screen readers only. - Be honest. Add up to two trade-offs per item, and mark a missed must-have as
falserather than leaving it out. The item then shows "Misses a must-have". - Fill
meetsfor 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, wherenis the 1-based position insources, 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
fetchedAtandstaleAfterMson 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 amoreresult.expiresAtisn't needed unless the items are held offers.
Interaction
| Control | Sends | Notes |
|---|---|---|
| A radio or checkbox per item | — | Shown when selectable is single or multiple |
Submit (submitLabel) | select | Disabled until something is chosen. Defaults to "Choose this" or "Choose these" |
| Show other options | more | Opens an optional "what should be different?" box first. Shown in every mode, display-only included |
| Not now | dismiss | Only 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
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,
}),
}<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
| Prop | Type | Notes |
|---|---|---|
title | string? | "Where to stay in Lisbon" |
basis | string? | 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 |
items | RecommendationItem[] | 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 |
submitLabel | string? | Verb for the submit button, e.g. "Start a trial" |
RecommendationItem
| Field | Type | Notes |
|---|---|---|
id | string | Returned in itemIds |
rank | number | 1 is the top pick |
name | string | Data-bound |
subtitle | string? | 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 |
why | string[] | 1–3 plain sentences; may include [n] markers |
tradeoffs | string[]? | Up to 2 honest downsides |
meets | Record<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" |
url | string? | http(s) only; opens in a new tab |
tags | string[]? | Up to 3 short facts. Screen readers only |
Host props: status (streaming, ready, selected, more, dismissed), onResult, result and className.


