Question Stepper
Batch the decisions an agent needs into one card, answered one question at a time.
≈ 1,100 tokens per use (tool 586 · guidance 270 · typical payload 244)How token costs are estimated
Example: Question Stepper
Find companies redesigning their product or marketing site right now.
Narrowing the search. Question 1 of 3
Press 1 to 9 to choose an option and Enter to continue.
When to use
- You need 1–5 decisions from the user before you can do the work well.
- The answer would materially change the result, and you can't infer it.
- The choices are discrete enough that you can propose good options.
When not to use
- You can find the answer yourself in data, files or earlier messages. Look it up.
- It's a single yes/no question. Ask in one sentence, or use follow-up chips.
- You'd need more than 5 questions. Proceed with sensible defaults and ask afterwards.
- You're collecting record data, such as an address or a new contact. Use a generated form.
Interaction
| Input | Action |
|---|---|
| 1–9 | Select an option. The number after the last option focuses Other. The shortcuts aren't shown on screen; screen readers announce them |
| Enter | Next question, or submit on the last one |
| Space | Toggle the focused option |
| Skip | Only shown when the question isn't required |
| Let agent decide | Hands the question back to the agent, which should say what it chose |
| × | Dismisses the whole card. The agent should ask in text what the user wants instead |
While the tool call is streaming, the user can already answer the questions that have arrived. Submitting waits until the payload is complete.
The card shows one question at a time: a segmented progress bar, the question, and its options. title and each question's description are read to screen readers but not shown, so put anything the user must see in the question or the options. header labels each answer in the submitted summary. A required question is marked with an asterisk.
Installation
npx shadcn@latest add https://your-site.com/r/question-stepper.json
Usage
import { tool } from "ai"
import { questionStepperTool } from "@/components/genui/question-stepper.schema"
const tools = {
// No execute(): the call is completed by the user's answers in the UI.
[questionStepperTool.name]: tool({
description: questionStepperTool.description,
inputSchema: questionStepperTool.inputSchema,
}),
}import { QuestionStepper } from "@/components/genui/question-stepper"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-ask_questions") return null
return (
<QuestionStepper
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "ask_questions",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{
action: "submit",
answers: [
{ id: "source", status: "answered", selected: ["sheets"] },
{ id: "metrics", status: "answered", selected: ["score_trend"], text: "Churn by cohort" },
{ id: "audience", status: "delegated" },
{ id: "region", status: "skipped" },
],
}
{ action: "dismiss" }text holds the Other input for option questions, and the answer for text questions.
With LangGraph
Push it from a graph node with ui.push({ name: "ask_questions", 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. The component renders its finished state (the answer summary) and never calls onResult again, so a decision can't be made twice. With the AI SDK, that's the tool call's output:
<QuestionStepper
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string? | Context for screen readers; not shown |
questions | Question[] | 1–6 questions |
allowDelegate | boolean | Default true. Shows Let agent decide |
submitLabel | string? | Label on the last step, e.g. "Build dashboard" |
Question
| Field | Type | Notes |
|---|---|---|
id | string | Returned in the result |
header | string? | Short topic label: "Data source". Labels the answer in the summary |
prompt | string | The question |
description | string? | Why you're asking. Screen readers only |
type | single | multi | text | Default single |
options | { value, label, description?, recommended? }[] | Up to 8. Data-bound when they refer to real entities |
allowOther | boolean | Default true |
required | boolean | Default false |
placeholder | string? | For Other or the text input |
Host props: status (streaming, ready, submitted, dismissed), onResult, result and className.