Ask Question
Let an agent ask a human for missing input.
Use an ask_question tool when the model needs information from a person before it can continue. The tool is still a normal tool. The schema is portable: Studio provides a built-in UI for it, and your own app can render the same contract in a custom frontend.
Question Tool Contract
const questionChoiceSchema = z.object({
label: z.string(),
value: z.string(),
});
const askQuestion = createTool({
name: "ask_question",
description: "Ask the human operator one or more follow-up questions.",
input: z.object({
questions: z.array(
z.object({
id: z.string(),
question: z.string(),
choices: z.array(questionChoiceSchema).min(1),
}),
),
}),
output: z.object({
answers: z.array(
z.object({
questionId: z.string(),
answer: z.string(),
choice: z.string().optional(),
custom: z.boolean().optional(),
}),
),
}),
async execute({ questions }) {
return humanInput.ask({ questions });
},
});Studio intercepts ask_question before execute(...) runs, renders the questions, waits for answers, and sends the answers back to the model as the tool result. Outside Studio, the same execute(...) can call your own UI, queue, websocket, or backend workflow.
Multiple Questions
Ask related questions in one tool call instead of forcing several back-and-forth calls.
{
"questions": [
{
"id": "priority",
"question": "What is the priority level?",
"choices": [
{ "label": "Low", "value": "low" },
{ "label": "High", "value": "high" },
{ "label": "Critical", "value": "critical" }
]
},
{
"id": "channel",
"question": "Which support channel should we use?",
"choices": [
{ "label": "Email", "value": "email" },
{ "label": "Phone", "value": "phone" },
{ "label": "Other", "value": "other" }
]
}
]
}Studio shows each question as a compact control. Choices become buttons, and Studio always shows a custom input after the choices. Custom input is UI behavior, not a separate tool schema setting.
Agent Instructions
Tell the model when to ask and how to batch questions.
const agent = new AgentBuilder("support", model)
.instructions(
[
"Use ask_question when priority, channel, or operator context is missing.",
"Ask multiple questions in one ask_question call when you need multiple answers.",
"Use choices for bounded decisions.",
"Studio always shows a custom input after the choices.",
"After the human answers, continue the workflow with the confirmed values.",
].join("\n"),
)
.tool(askQuestion)
.build();Outside Studio
If you are not using Studio, keep the same schema and implement the waiting boundary yourself.
const humanInput = {
async ask(request: {
questions: Array<{
id: string;
question: string;
choices: Array<{ label: string; value: string }>;
}>;
}): Promise<{
answers: Array<{
questionId: string;
answer: string;
choice?: string;
custom?: boolean;
}>;
}> {
const pending = await db.humanQuestion.create({
data: {
questions: request.questions,
status: "pending",
},
});
await ui.notifyUser({ questionId: pending.id });
return waitForUserAnswers(pending.id);
},
};The run waits while humanInput.ask(...) awaits. Your frontend can render the same questions array as buttons, selects, radios, or custom text inputs. Add an app-owned timeout if the request must finish within a deadline.
Cookbook
Run the Studio question example:
pnpm cookbook:studio:05