Run

Asking people

Let an agent ask a person a question in the middle of a job, and wait durably for the answer.

For: people running and reviewing agents.

Give an agent oz.ask_user when its work needs a person's answer, such as which customer an order belongs to, whether to refund it or which branch to release. The built-in tool asks the question and waits for a reply.

agents/acme-support.yaml
name: acme-support
runtime: pydantic-ai
model:
  primary: anthropic/claude-sonnet-5-5
tools: [acme_crm.find_order, acme_crm.refund, oz.ask_user]
ask_user:
  timeout: 4h            # how long a question waits; 24h when unset
  on_timeout: continue   # continue (the default) or fail

The model sees a tool oz__ask_user with two arguments: the question, written so that a person who has not seen the conversation can answer it, and optional choices, the answers to pick from. A call that does not fit, such as one with an argument it does not take or choices that is not a list of strings, goes back to the model with the reason, and it tries again; it counts against the tool's retries like any other refused call.

Typed questions

To ask for a date, a number or a pick from a list rather than words, the model passes fields instead of choices: at most 20 fields in the standard field set, each with a name (lower case letters, digits and _), a kind (text, number, choice, multi-choice, date, toggle), and optionally a label, a description, required, options for a choice, min, max and integer for a number, max_length and multiline for text, and date_time for a date that holds a time.

The fields take at most 16 KiB in all, as JSON; the model is told why when it asks with more:

{"question": "When should I send the report, and how many copies?",
 "fields": [{"name": "on", "kind": "date", "label": "Send on", "required": true},
            {"name": "copies", "kind": "number", "integer": true, "min": 1, "max": 9}]}

The answer is one JSON object of the fields' values, such as {"on": "2026-10-20", "copies": 2}, which the agent reads as The answer: {...}. The job checks it before it takes it: a missing required field, a number out of its bounds (or not a finite number), a choice that is not one of the options or a date that does not read is refused with the field's words, and the question keeps waiting.

The console draws the fields as a form; Slack and Teams draw them in their own forms; Discord offers Open in OZ, and the post says to answer with the form where the chat offers one, or in OZ. A question takes choices or fields, not both.

What happens

  1. The agent calls oz.ask_user. The job records the question and waits. The question shows up on the console's Approvals page, on the job's page, in oz0 approval list, and in the job's live stream.
  2. Someone answers it, or declines to.
  3. Answered: the agent reads The answer: ... as the tool's result and goes on. Declined: the agent reads who declined and why, for example user:kevin declined to answer: ask Ana.
  4. Nobody answers in time: with on_timeout: continue, the agent reads Nobody answered within 4:00:00. and decides itself whether to go on or stop. With on_timeout: fail, the job fails with QuestionTimedOut.

A question is a durable wait in the job's Temporal workflow, like an approval. It costs nothing while it waits and survives restarts of servers and nodes. The question, the answer and who gave it are in the job's history, and the job's trace shows the wait as a step.

The escalation ladder

A question climbs the same escalation ladder as an approval, counted from its own timeout: a reminder to the people who may answer at a quarter, the agent's owner at half and the tenant's admins at three quarters, each by email, webhook or chat when the tenant has a notification rule for question.waiting or approval.escalated. The agent's approvals: {escalation: ...} reshapes it or turns it off (with escalation_reason). At the timeout the question ends as before: on_timeout decides, and nobody's silence is ever read as an answer. Questions cannot be edited; answer, or decline with a comment.

Answer from the console

Approvals lists questions next to approvals, with the kind question. Open one to see the question. When it offers choices, each is a button; when it asks fields, fill in the form (it says what is missing before it sends); otherwise type the answer and press Answer. Decline tells the agent you will not answer; write why in the field next to it first if you want the agent to know.

Operators, admins and owners of the tenant answer. Readers see that a question waits, but not the question, unless they may see the tenant's content. An operator without the content permission does not see the question either, so they cannot answer it; they can only decline.

Answer from the CLI

Terminal
oz0 approval list
oz0 approval answer job-0a9dc2d1-... a-75eaf712473f blue
oz0 approval deny job-0a9dc2d1-... a-75eaf712473f --comment "ask Ana"

When the question has choices, the answer must be one of them; a refusal does not list them, because they are the job's content. A typed question takes one JSON object: oz0 approval answer <job> <id> '{"on": "2026-10-20", "copies": 2}' (approval list shows its fields as {on: date, copies: number}). oz0 run and job watch show the question as it is asked, with the command that answers it:

asker → oz.ask_user {"choices":["red","blue"],"question":"Which colour should the report use?"}
asker ? asks a-75eaf712473f: Which colour should the report use? [red / blue]
  oz0 approval answer job-0a9dc2d1-... a-75eaf712473f "..."   (or deny)
asker ▶ operator:admin answered: blue
asker ← oz.ask_user The answer: blue

From your own app

ListApprovals in the runtime API returns questions with kind: "question", the question in details, the answers to pick from in choices, and a typed question's fields (FormField; the feed's FeedQuestion and the chat model's ChatRequest carry them too, all only with the content permission). Answer with DecideApproval, approve: true and the answer; approve: false with an optional comment declines.

Only a question takes an answer: an answer sent to any other kind of approval, or together with approve: false, is refused (FailedPrecondition), so approval answer can never approve a tool call by mistake, and a decline never shows as an answer.

The server checks this against the job's pending approvals before the answer reaches the job, so it also holds for jobs still running on an older runtime version. An answer from a caller without the content permission is refused with PermissionDenied: they cannot see what they would answer.

Harness agents

A harness agent asks the same way. List oz.ask_user in its tools:, and the Claude harness gives the session the SDK's AskUserQuestion tool: each of its questions waits in the inbox, and the answers go back to the session.

An ACP agent asks with ACP's form elicitation: a form of one field is one question (with its choices), and a form of two to 20 fields is one typed question, each property in the field set where it maps and as text where it does not (an object property is answered as JSON). Only a harness that asks before each tool call (permissions: true) can ask people.

A client that started the job over MCP sees a question as the task's input_required, and can answer it there.

A session's question waits no longer than the session may run: its agent's limits.timeout, or two hours when that is unset, counted from when the session started. The inbox shows that deadline. When the session ends, its open questions leave the inbox, and an answer after that has no one to go to.

oz.ask_human, the name lint accepted before the tool was built, still works and is the same tool; lint suggests the new name.

answer from the web ui: Read the section.

Copyright © 2026