Guides
Human input
Approvals, questions and forms that suspend a run until a person answers
A run can stop and wait for a person: the model asks a question, a tool needs
approval before it runs, or a tool asks for a form or a setup step. The run
suspends. It ends with status: "input_required" and the inputs it waits
on, and the agent sleeps (nothing is billed while it waits). Once the last input
is answered, minutes or days later and from any process, the turn resumes.
let run = await agent.run("Delete the staging database", { user: "alice" });
while (run.status === "input_required") {
const input = run.inputs[0];
console.log(input.kind, input.message); // "approval", "Allow db__drop to run?"
run = await input.answer(true, { from: "alice" }); // resolves with the resumed run
}
console.log(run.text);Where inputs come from
- Approvals. A tool with
needsApproval: true(@tool(needs_approval=True)) is approved before each call. A definition's MCP and OpenAPI sources take an approval policy:"approval": {"default": "never" | "always" | "destructive", "tools": {"delete_repo": "always"}}.destructivemeans MCP tools annotateddestructiveHint, and OpenAPI operations other than GET, HEAD and OPTIONS. The person sees the real call, and an approved call runs with exactly those arguments. - Questions. The
ask_userbuilt-in lets the model ask 1 to 4 multiple-choice questions, with free text where it allows. - The tool itself.
context.confirm(message),context.ask(message, schema)(a flat form) andcontext.requireUrl(url, message)(a step on an https page, such as connecting an account).
const deleteApp = tool({
description: "Delete an app",
input: schema.Object({ app: schema.String() }),
execute: async ({ app }, context) => {
if (!await context.confirm(`Delete ${app}? Its URL stops working.`)) return { cancelled: true };
return apps.delete(app, { idempotencyKey: context.idempotencyKey });
},
});An ask ends the call. Once the person answers, camelRun calls the tool again
with the same arguments and the ask returns the answer, so everything before
an ask runs again. Ask first and act after, and key side effects by
context.idempotencyKey, which is the same on both calls.
Answering
input.answer(value, { from }) takes a plain value by the input's kind:
| Kind | Value |
|---|---|
approval | true to approve, false to decline |
question | The chosen option's label (or labels, or free text where allowed). For several questions, { "<question>": answer } |
form | The fields, as an object |
url | true once the person has done what the page asks |
input.decline() declines any input. Answering the same way twice is safe; a
different answer to an input that already settled is a 409 that says how it
settled.
Inputs outlive your process, so you can answer later from anywhere:
agent.pendingInputs()(Pythonpending_inputs()) lists an agent's waiting inputs.agents.runtime.inbox("pending")lists inputs across all your agents, for an approvals queue.- Over REST:
GET /v1/agents/{id}/inputs?state=pending, thenPOST /v1/agents/{id}/inputs/{inputId}with{"action": "accept" | "decline" | "cancel", "content": ..., "from": ...}. onInputonupserthears each input as it's asked. Return an answer to give it right away, or nothing to answer later.- Events
input_requiredandinput_resolved, and the webhooksinput.requestedandinput.resolved.
Who may answer
By default, the person whose message started the turn, plus the definition's
humanInput.approvers. An answer must say who is answering (from) whenever
the input names who may. A from who may not answer gets a 403. The model has no
way to answer.
Waiting and giving up
- A new message to the agent supersedes its waiting inputs. Their calls close with "Not answered: the user sent a new message instead", then the message runs.
agent.abort()cancels them.- Inputs expire after
humanInput.expiresInSeconds: 7 days by default, at most 30.
{"name": "Ops", "builtins": ["ask_user"],
"humanInput": {"expiresInSeconds": 86400, "approvers": ["slack:U0123"]},
"mcpServers": [{"name": "github", "url": "...", "approval": {"tools": {"delete_repo": "always"}}}]}In Slack, Telegram or Discord, a suspended turn's reply ends with its first
input as text. The next message from someone allowed to answer, if it fits (an
option's number, approve or deny, done), is the answer. See
Channels.