Entity Picker
Let the user choose one or more real records the agent fetched, with search, groups and keyboard support.
≈ 844 tokens per use (tool 492 · guidance 220 · typical payload 132)How token costs are estimated
Example: Entity Picker
Pull last week's results for our Meta ads.
Meta ad account
When to use
- The user must choose which real records to work with: an ad account, campaigns, files, teammates.
- The agent fetched more than one candidate and can't tell which the user means.
When not to use
- Only one record matches. Use it and say which one.
- The choices are options or preferences, not records. Use the Question Stepper.
- The agent hasn't fetched the records yet. Fetch them first; never list records from memory.
How the agent should use it
- Every entity comes from a tool result, with its real
id. - Use
descriptionto tell similar records apart: an account id, an email, a path. - Use
groupwhen records belong to different parents, e.g. campaigns by ad account. - Set
multipleandmaxonly when the next step can take several records. - Act only on the ids returned with
select.
Interaction
| Input | Action |
|---|---|
| Typing in Search | Filters by label, description, meta and group. The result count is announced |
| Esc in Search | Clears the search |
| ↓ in Search | Moves into the list |
| ↑ ↓ Home End | Move through the options |
| Space or Enter | Select the option (toggle it when multiple) |
The list is an ARIA listbox (aria-multiselectable when multiple) that tracks the active option with aria-activedescendant; groups are labelled by their headings. Search appears automatically above 6 records, and lists longer than 8 rows scroll inside the card. Once max is reached, the remaining options are disabled until one is cleared.
Installation
npx shadcn@latest add https://your-site.com/r/entity-picker.json
Usage
import { tool } from "ai"
import { entityPickerTool } from "@/components/genui/entity-picker.schema"
const tools = {
// No execute(): the call is completed by the user's choice in the UI.
[entityPickerTool.name]: tool({
description: entityPickerTool.description,
inputSchema: entityPickerTool.inputSchema,
}),
}import { EntityPicker } from "@/components/genui/entity-picker"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-pick_entities") return null
return (
<EntityPicker
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "pick_entities",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{ action: "select", ids: ["120210", "330102"] }
{ action: "cancel" }With LangGraph
Push it from a graph node with ui.push({ name: "pick_entities", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
Pass the stored result back as result. The picker lists the chosen records (or the cancelled state) and never calls onResult again.
<EntityPicker
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string | What to pick: "Choose an ad account" |
description | string? | One short line under the title, only for a consequence the user should know (e.g. what access is granted) |
entities | { id, label, description?, meta?, group? }[] | Data-bound. 1–200 records, most relevant first |
multiple | boolean | Default false |
min | number? | Fewest picks. Default 1 |
max | number? | Most picks, when multiple |
searchable | boolean? | Default: on above 6 records |
submitLabel | string? | Verb for the confirm button. Default "Select" |
Host props: status (streaming, ready, submitting, selected, cancelled), onResult, result and className.