Question Stepper

Batch the decisions an agent needs into one card, answered one question at a time.

Pattern

≈ 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.

Which redesign motion should I prioritize?

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

InputAction
1–9Select an option. The number after the last option focuses Other. The shortcuts aren't shown on screen; screen readers announce them
EnterNext question, or submit on the last one
SpaceToggle the focused option
SkipOnly shown when the question isn't required
Let agent decideHands 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

app/api/chat/route.ts
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,
  }),
}
components/message.tsx
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

PropTypeNotes
titlestring?Context for screen readers; not shown
questionsQuestion[]1–6 questions
allowDelegatebooleanDefault true. Shows Let agent decide
submitLabelstring?Label on the last step, e.g. "Build dashboard"

Question

FieldTypeNotes
idstringReturned in the result
headerstring?Short topic label: "Data source". Labels the answer in the summary
promptstringThe question
descriptionstring?Why you're asking. Screen readers only
typesingle | multi | textDefault single
options{ value, label, description?, recommended? }[]Up to 8. Data-bound when they refer to real entities
allowOtherbooleanDefault true
requiredbooleanDefault false
placeholderstring?For Other or the text input

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

Source

On this page