Skip to content

Approvals & questions ​

An agent can stop and wait for a person. Three things make it stop:

  • A tool approval. A tool call waits until someone allows or denies it. Useful for tools that change something: a shell command, a write, a call to an external system.
  • A question. The agent asks the user to choose between options (or type an answer) when it needs a decision it cannot make alone.
  • A tool that runs in the page. When a chat is embedded in a host page, the agent can use tools the page runs — see Tools the page runs.

The turn is paused, not lost. When the request is answered, the same turn carries on from where it stopped: the tool runs (or the model reads the refusal), and the reply arrives as a new assistant message. An agent that does not use these features behaves exactly as before.

Turn it on ​

Open Build → Agents → your agent → Build and scroll to Approvals & questions.

SettingDefaultWhat it does
Let the agent ask the user questionsoffAdds the ask_user_question tool. The agent can pause with a structured question and options.
Allow free-text answersonBesides the offered options, the person can type their own answer. Off: every question must offer options.
Ask before tools runoff (on in agents created in Studio)A tool call waits for approval.
Tools not listed belowAlways askWhat happens to a tool with no setting of its own.
Per-tool settingsnoneOverride the default for one tool: Default, Always ask, Never ask. The name is the one the model sees. A badge says whether the tool asks or does not ask with the current choice.
Wait for an answer (minutes)1440How long a request stays open. 0 = never expires. Up to one year.
When time runs outDeny and continueDeny and continue: the tool is refused (or the question skipped) and the agent carries on in the background. Cancel the request: the request is dropped and the session goes back to idle.
Who can answerThe session owner and project adminsOnly the person who started the session; the owner and project admins; or anyone with access to this agent.
A new message while waitingReject itReject: the message is refused with 409 pending_action until the request is answered. Cancel the request and continue: the pending request is cancelled and the message is processed normally.
When nobody can answerDeny the tool callSee unattended runs.

The timing, approver, new-message and unattended settings appear once either questions or tool approval is on.

An agent created in Studio starts with tool approval on (every tool asks) and questions off. An agent created through the API starts with neither, because its callers never expected a run to stop and wait. Turn them on in Build, or send config.humanInTheLoop when you create the agent (see the config reference).

Publish for it to apply to API and portal traffic

These settings are saved with the draft. A published version keeps the settings it was published with, so publish again for them to apply to POST /agents/responses and portals. A paused turn resumes under the settings of the version it ran with.

What is not asked about ​

Tools that only read are never asked about, unless you set them to Always ask: knowledge_*, memory_search, sandbox_read_file, sandbox_list_files, sandbox_preview_link, web_search, session_files_list, session_files_read, session_files_view_image and the question tool itself. Everything else Console builds follows the default: bound, MCP and custom tools, memory writes, sandbox_exec, sandbox_run_code, sandbox_write_file.

The agent-sdk's own tools (plan, context, delegation, skills) are never gated. Approvals cover the agent's own tools; a call made by a sub-agent is not gated, and Studio warns about that when both are set.

In Studio ​

A paused turn shows a card on the assistant message.

  • Tool approval — the tool name and its arguments. Edit arguments lets you change them (the JSON is checked against the tool's schema before the tool runs). Add a reason when you deny — the agent sees it. Don't ask again for this tool in this session allows the tool for the rest of the session.
  • Question — each question with its options (radio buttons, or checkboxes for a multi-select), a free-text box when allowed, and Skip. Required questions must be answered before Send is enabled.

Answering continues the turn live, like a normal message. Sessions waiting for you show a Waiting for you badge in the session list (and a count on the Sessions tab), and the list has a Waiting for you status filter. While a request is open the composer follows the A new message while waiting setting: it is disabled with a short explanation, or it warns that sending a message cancels the request.

If the agent is set to Only the person who started the session or The session owner and project admins, a person outside that group sees a notice instead of working buttons: a response from anyone else is refused.

If a request was answered, cancelled or expired while you were looking at it, Studio says so and reloads the session instead of showing a stale card. Cards survive a page reload: a session still waiting shows its cards again.

Over the API ​

A paused response has status: "requires_action" and the requests in required_action:

json
{
  "id": "resp_6f1c0a2b9d3e4f5a6b7c8d9e",
  "object": "response",
  "model": "ops-agent",
  "status": "requires_action",
  "stop_reason": "requires_action",
  "required_action": {
    "type": "agent_actions",
    "actions": [
      {
        "id": "act_0123456789abcdef01234567",
        "kind": "tool_approval",
        "tool_name": "sandbox_exec",
        "args": { "command": "rm -rf build" },
        "status": "pending",
        "expires_at": "2026-10-02T12:00:00.000Z",
        "requested_at": "2026-10-01T12:00:00.000Z"
      }
    ]
  }
}

A question looks the same with kind: "user_question", questions: [{ question, header?, multi_select?, options: [{ label, value?, description? }] }] and allow_free_text, and no tool_name or args — those belong to a tool approval only. A page tool is kind: "client_tool" with tool_name, args and timeout_seconds.

expires_at is absent when the agent waits forever. {conversationId} below is the id inside resp_{conversationId}.

Answer a request ​

http
POST /api/client/v1/agents/sessions/{conversationId}/actions/{actionId}
Authorization: Bearer <token>
Content-Type: application/json

{ "decision": "allow" }
To…Send
Approve{ "decision": "allow" }
Approve with edited arguments{ "decision": "allow", "approved_args": { "command": "rm -rf build/tmp" } }
Deny{ "decision": "deny", "comment": "not on prod" }
Approve and stop asking{ "decision": "allow", "remember_for_session": true }
Answer a question{ "answers": { "Which environment?": "staging" } } — a list of values for a multi-select
Skip a question{ "cancelled": true }
Answer a page tool{ "output": { "text": "…" } } (any JSON, at most 64 KB), or { "error": "nothing is selected" } when the page could not do it

Rules the request is checked against (400 invalid_resolution, and the request stays open):

  • decision must be "allow" or "deny"; comment a string; remember_for_session a boolean.
  • approved_args must be a JSON object that satisfies the tool's schema, and only applies to an approval.
  • answers is an object keyed by the question text; each value a string, or a list of strings. A value that names one of the question's options is a selection, anything else is the person's own words, which is accepted only if the question allows free text.
  • Tool guardrails still apply when the tool runs.
bash
# 1. Start a turn that needs approval.
RESP=$(curl -s -X POST "$CONSOLE_URL/api/client/v1/agents/responses" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "model": "ops-agent", "input": "Clean up the staging build folder" }')

SESSION=$(echo "$RESP" | jq -r '.id | sub("^resp_"; "")')
ACTION=$(echo "$RESP" | jq -r '.required_action.actions[0].id')

# 2. Allow it. The turn carries on and this call returns the finished response.
curl -X POST "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION/actions/$ACTION" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "decision": "allow", "approved_args": { "command": "rm -rf build/tmp" } }'
typescript
const res = await client.agents.responses.create({
  model: 'ops-agent',
  input: 'Clean up the staging build folder',
});

if (res.status === 'requires_action') {
  const sessionId = res.id.replace(/^resp_/, '');
  const [action] = res.required_action!.actions;

  // Inline: resolves with the continuation in the Responses shape.
  const next = await client.agents.sessions.actions.resolve(sessionId, action.id, {
    decision: 'allow',
    approved_args: { command: 'rm -rf build/tmp' },
  });
  console.log(next.status);
}

What the answer returns ​

The turn continues inline and the answer is a normal response (completed, or requires_action again if it paused again). With several requests in one pause, each answer returns requires_action with the ones still open until the last is answered — then the turn continues.

Add "background": true (or the X-Cognipeer-Background: true header) to continue as a background run and get 202 with the run. In the SDK that is resolve(sessionId, actionId, body, { background: true }); follow the run with client.agents.runs.wait(run.id).

Errors ​

StatusCodeMeaning
400invalid_resolutionThe body is not a valid answer; the request stays open.
400agent_background_disabledbackground: true on an agent that does not allow background runs.
403action_forbiddenThe caller may not answer under the agent's Who can answer setting.
404action_not_found, session_not_foundNo such request or session in this project.
409action_not_pendingSomeone answered first. error.action_status says how it ended.
409action_cancelledThe request was cancelled meanwhile — a new message, a cancelled run, or the timeout policy.
409agent_run_conflictAnother run is active for the conversation.
410action_expiredThe request ran out of time.

The three "no longer open" cases are told apart on purpose: a client that shows the request can say Answered, Cancelled or Expired instead of guessing. Sending a new message to a session that is waiting is a separate refusal, 409 pending_action, with error.pending_action_ids listing what to answer — unless the agent cancels the pending request on a new message.

You can read what a session is waiting on at any time with GET /agents/sessions/{conversationId} (session events), and an action.required webhook tells you without polling.

Who can answer ​

The agent's Who can answer setting decides:

SettingAllowed to answer
Only the person who started the sessionThe session's owner (or whoever the request was created for).
The session owner and project admins (default)The owner, plus tenant owners/admins and project admins.
Anyone with access to this agentAnyone who may see the agent in this project.

An API token answers as its owner. Every answer is written to the audit log (who, what, the decision, and whether the arguments were edited) and appears in the session events as action.resolved with a channel of dashboard, api, portal, timeout or system.

Timeouts ​

Open requests are checked about every 30 seconds. Past the time you set, the agent's When time runs out setting applies, and the answer is recorded as given by system:timeout:

  • Deny and continue — a tool approval is answered as a denial with the comment timed out, a question is skipped, and once nothing else is open the agent continues as a background job — nobody is waiting on it. The event reads action.resolved with expired: true, channel: "timeout".
  • Cancel the request — the request becomes expired, the paused state is dropped and the session goes back to idle. Nothing else runs.

A tool that runs in the page is the exception: a page that does not answer is always answered with the error The host page did not answer. and the turn goes on, whatever When time runs out says.

Unattended runs ​

Some runs have nobody to answer: schedules, evaluations and red-team cases, and the OpenAI-shaped and Assistants endpoints, which have no way to send an answer. The When nobody can answer setting decides what a gated tool does there:

SettingEffect
Deny the tool call (default)The tool does not run, the model is told This tool needs human approval and no approver is available in this run. and the run goes on. Questions are not asked.
Pause and wait for approval in StudioBehaves like an attended run; the request waits in the session until someone answers it. API bridges that cannot show it return the paused message.
Run without approvalThe tool runs. An explicit opt-in for agents whose owner accepts that; Studio warns when it is combined with approvals.

A stateless call with no session has nowhere to keep a paused turn, so pause cannot be honoured there and the same rule degrades to deny — never to run.

Background runs and A2A ​

A background run that pauses has the non-terminal status requires_action; its result carries the same required_action, and the agent_run.requires_action callback and webhook fire. Answering queues the same run again; it finishes as usual. Cancelling a paused run ends it and drops its open requests. Clients that poll until a terminal status must stop on requires_action too, or they wait for a person. (client.agents.runs.wait() in the SDK already does.)

Over A2A, a paused task reports the state input-required with the requests in metadata.pendingActions. Unless the agent cancels the pending request on a new message, a message/send to a conversation that is still waiting is refused, with the ids to answer in the error's data.pending_action_ids. tasks/cancel is not supported (-32004): a turn runs inside the message/send call, so there is nothing to stop. A task that waits on a person is ended by answering its requests through the actions endpoint above (deny an approval, or answer a question with cancelled: true); the error of tasks/cancel on such a task lists the ids.

Tools the page runs (client_tool) ​

A chat that is embedded in a host page — a portal's widget, an intranet, a product — can let the agent use tools that run in the page: read what is selected, open a record, without any server-side connector. Two things have to agree before an agent gets one:

  • The portal lets this agent take them. In the portal's Chat features tab, each agent has a switch, May use the page's tools, that is off by default (it shows while embedding is on). An agent that is not switched on in that portal never receives a tool from a page, whatever the embedding declares. The rule belongs to the portal, per agent; the agent itself has no setting for it.
  • The embedding's admin declares the tools — name (letters, digits and _, up to 64), description, JSON-Schema input, a timeout (default 60 s, at most 600 s) and what to do about approval. At most 20 tools. Only those are offered, and only on a run that says it is embedded. Nothing declares them from a client request: they are never offered over the plain API.

What a page returns is user input

The person controls their browser, so a tool's result is whatever they — or a script in their page — send. A tool such as get_user_role or get_order_total lets the person steer an agent that believes the value. Do not let a client tool's result carry an authorization decision. Take identity, roles and permissions from the signed identity of the session and from server-side tools, never from the page.

Console helps but cannot make the value true: the description the model sees says the result is untrusted, every result reaches the model behind a one-line notice saying so, the arguments are checked against the declared schema and the result against the 64 KB limit, and the agent's guardrails screen it like any tool result.

A call pauses the turn like an approval, as a pending action of kind client_tool:

json
{
  "id": "act_0123456789abcdef01234567",
  "kind": "client_tool",
  "tool_name": "get_selection",
  "args": { "max": 100 },
  "timeout_seconds": 30,
  "status": "pending",
  "expires_at": "2026-10-11T12:00:45.000Z"
}

The page runs the tool and answers with output or error (table above). The turn then continues; the model reads output as the tool's result (after the agent's tool.post guardrails) and error as the tool failing.

  • A page that does not answer within the tool's timeout — plus 15 seconds for the round trip — is answered by Console with the error The host page did not answer., and the turn goes on as a background job.
  • Approval follows the agent's policy. Unless the admin decided for the tool, a call is treated like a call to any other tool of the agent: with Approvals & questions on and the default ask, a person approves it first (an ordinary tool_approval), and only then the page is asked; the read-only exemption of the agent's own lookup tools does not apply. The admin can decide per tool: needsApproval: true always asks, needsApproval: false exempts the tool from the agent's policy (for a harmless one, such as reading the page title), and leaving it out follows the agent.
  • tool.pre guardrails see the arguments before the page does. Tool-access guardrails see the tool as agent.client.<name>. The audit trail (agent.client_tool.resolve) records the call and its result as digests and sizes, never as content; the session log has action.required / action.resolved and an agent.tool_result with source: "client".
  • Only the person the chat belongs to can supply the page's output — it goes into the model's context as a tool result. An administrator or another project member whom Who can answer lets approve a tool can only give the call up with an error.
  • Studio shows the open call as Waiting for the page, with a Give up on it button that answers with an error.

Config reference ​

The same settings as JSON, for agents you create or update through the API (POST /api/client/v1/agents, PATCH /api/client/v1/agents/{key}):

json
{
  "name": "Ops agent",
  "config": {
    "modelKey": "gpt-4o",
    "systemPrompt": "You operate the staging environment.",
    "humanInTheLoop": {
      "toolApproval": {
        "enabled": true,
        "defaultPolicy": "always",
        "tools": { "web_search": "never", "sandbox_exec": "always" }
      },
      "askUser": { "enabled": true, "allowFreeText": true },
      "timeoutMinutes": 1440,
      "onTimeout": "deny",
      "approvers": "owner_or_admin",
      "onUserMessageWhilePending": "reject",
      "unattended": "deny"
    }
  }
}
KeyValuesDefault
toolApproval.enabledtrue / falsefalse
toolApproval.defaultPolicyalways, neveralways
toolApproval.tools.<name>always, never—
askUser.enabled, askUser.allowFreeTexttrue / falsefalse, true
timeoutMinuteswhole number, 0 – 525600 (0 = never expires)1440
onTimeoutdeny, canceldeny
approversowner, owner_or_admin, project_membersowner_or_admin
onUserMessageWhilePendingreject, cancel_pendingreject
unattendeddeny, pause, allowdeny

A value outside these sets, or a non-integer timeoutMinutes, is a validation error and the config is not saved.

PATCH replaces config

PATCH /agents/{key} replaces the stored config wholesale. Send the complete config, not only the section you changed. The create, update and publish responses return the full config (with secrets masked); the single-agent GET returns a summary only.

Security notes ​

  • Deny is the default when nobody can answer. An unattended run never executes a gated tool unless its owner explicitly chose Run without approval.
  • Edited arguments are validated against the tool's schema before the tool runs, and the agent's tool guardrails still apply to the call.
  • Every answer is audited — who answered, which action, the decision and whether the arguments were edited — and recorded in the session events with the channel it came from.
  • A request is answered once. A second answer gets 409 (or 410 after the timeout), so two approvers cannot both act on one request.
  • Approvers are scoped. Who can answer limits the people who may approve, and an API token answers as its owner, within its own scope.
  • Page tools are untrusted input. Their results are marked as such for the model, size- and schema-checked, screened by guardrails, and audited as digests rather than content. Never base an authorization decision on one.
  • The paused state is internal. The session resource and the event log expose the request (tool_name, args, questions) — not the stored state the turn resumes from.

Troubleshooting ​

My agent never asks. Work down the list: Ask before tools run is off; the tool is read-only and set to Default; the tool is set to Never ask; the call was made by a sub-agent; the run is unattended and When nobody can answer is Deny or Run without approval; or you are calling the published version and have not published since you turned approvals on. The per-tool table in Build shows what each tool will do.

409 pending_action when I send a message. The session is waiting for an answer and the agent rejects new messages meanwhile. Answer the request (error.pending_action_ids), or set A new message while waiting to Cancel the request and continue.

403 action_forbidden. The caller is not an allowed approver under Who can answer. An API token answers as its owner: use a token owned by an eligible person, or loosen the setting.

410 action_expired for a request that is a few minutes old. Wait for an answer is shorter than you think, or the request was created on a version with a different timeout. A paused turn uses the settings of the version it ran with.

A message says "The run stopped for a human but its state is too large to keep, so it cannot be continued." The paused turn's state did not fit in what Console stores for a session, so no request was opened and nothing can be answered. Reduce what the turn carries — very large tool results are the usual cause — or split the work across turns.

What's new ​

Approvals, questions, page tools, the requires_action Responses status, the …/actions/{actionId} endpoint, input-required over A2A, the Waiting for you badge and filter, and the unattended-run policy are new in the October 2026 agent runtime release.

Step by step: require approval before a tool runs ​

Some tools change something in the world: they send an email, write to a database, run a shell command. This guide puts a person in the loop for exactly those tools. The agent keeps working until it reaches one, pauses, and carries on from the same spot when someone approves or denies the call — in Studio, over the API, or from your own app.

Who this is for

Both halves of the team. Each step gives the Studio click-path first and the API call after it. Follow the clicks, or jump to Answer it from your own app.

What you end up with ​

user message ─► agent ─► wants to run `create_ticket`
                              │
                              ▼  turn pauses  (status: requires_action)
                 person: Approve · Edit arguments · Deny
                              │
                              ▼  the SAME turn carries on
                 tool runs (or the model reads the refusal) ─► final answer

Before you start ​

You needWhere
An agent with at least one tool that has a side effectBuild and publish an agent — step 3 binds tools
An API token, if you will answer from codeSettings → API Tokens → Create Token
Optional — a place that can receive webhooksReact to agent events with webhooks

1. Turn approvals on ​

Open Build → Agents → your agent → Build and scroll to Approvals & questions.

  1. Switch on Ask before tools run. An agent created in Studio already has it on; one created through the API does not.
  2. Leave Tools not listed below on Always ask if you want every tool gated, or switch it to Never ask and gate only the dangerous ones in the next step.
  3. In Per-tool settings, set the tools that matter: Always ask for the ones that write, Never ask for the harmless ones. The badge next to each says whether it asks or does not ask with the choice you made.

Tools that only read — knowledge search, memory_search, web_search, the sandbox's read and list tools, and the session-file tools — are never asked about unless you set them to Always ask. The agent's delegation to sub-agents is not gated: approvals cover the agent's own tools, and Studio warns when both are set.

2. Decide how requests behave ​

Once approvals are on, an Open requests block appears:

SettingA sensible start
Wait for an answer (minutes)1440 (a day). 0 means never expires.
When time runs outDeny and continue: the tool is refused and the agent carries on without it. Cancel the request drops it and returns the session to idle.
Who can answerThe session owner and project admins. Use Only the person who started the session for personal agents, Anyone with access to this agent for a shared queue.
A new message while waitingReject it keeps a conversation from racing past its own pending approval. Cancel the request and continue suits a chat where people change their mind.
When nobody can answerDeny the tool call. Schedules, evaluations, red-team cases and the OpenAI-shaped endpoints cannot show an approval card, so by default the gated tool simply does not run.

Click Save Configuration.

3. Try it in Studio ​

  1. On the agent, open Sessions → Start session and ask the agent to do something that needs the tool.
  2. The agent pauses. The assistant message shows an Approval needed card: the tool name and its Arguments. The session header shows Waiting for you, and the composer is disabled with The agent is waiting for your response above.
  3. Try each answer:
    • Approve — the tool runs with the arguments as proposed.
    • Edit arguments, change the JSON, Approve — the edited arguments are checked against the tool's schema before it runs. Invalid JSON, or arguments the schema refuses, keep the request open.
    • Deny, with a reason — the agent sees the reason and carries on without the tool.
    • Tick Don't ask again for this tool in this session before approving to stop being asked for the rest of this session.
  4. The turn continues live. Open the Activity tab to see the record: Approval requested for …, Approved, Called …, … returned, Run ended: finished.

The Sessions list marks the session with a Waiting for you badge while it waits, and its status filter has a Waiting for you option — a handy queue when several agents pause at once.

4. Publish ​

Approval settings are saved with the draft. API calls to POST /agents/responses and portals run the published version, so publish again for them to apply there, with the Publish button at the top of the agent page. A paused turn resumes under the settings of the version it ran with.

5. Answer it from your own app ​

Call the agent as usual. When it reaches a gated tool, the response is requires_action instead of a final answer:

bash
RESP=$(curl -s -X POST "$CONSOLE_URL/api/client/v1/agents/responses" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "model": "ops-agent", "input": "Open a ticket for the failed nightly build" }')

echo "$RESP" | jq '{status, actions: .required_action.actions}'
typescript
const res = await client.agents.responses.create({
  model: 'ops-agent',
  input: 'Open a ticket for the failed nightly build',
});

if (res.status === 'requires_action') {
  for (const action of res.required_action!.actions) {
    console.log(action.kind, action.id, action.kind === 'tool_approval' ? action.tool_name : '');
  }
}
json
{
  "id": "resp_6f1c0a2b9d3e4f5a6b7c8d9e",
  "status": "requires_action",
  "stop_reason": "requires_action",
  "required_action": {
    "type": "agent_actions",
    "actions": [
      {
        "id": "act_0123456789abcdef01234567",
        "kind": "tool_approval",
        "tool_name": "create_ticket",
        "args": { "title": "Nightly build failed", "priority": "high" },
        "status": "pending",
        "expires_at": "2026-10-12T09:14:03.000Z"
      }
    ]
  }
}

Show the request to whoever decides, then answer. The session id is the part of resp_… after the prefix:

bash
SESSION=$(echo "$RESP" | jq -r '.id | sub("^resp_"; "")')
ACTION=$(echo "$RESP" | jq -r '.required_action.actions[0].id')

# Approve as proposed…
curl -X POST "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION/actions/$ACTION" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "decision": "allow" }'

# …or edit the arguments…
#   -d '{ "decision": "allow", "approved_args": { "title": "Nightly build failed", "priority": "low" } }'

# …or deny, telling the agent why.
#   -d '{ "decision": "deny", "comment": "A ticket already exists for this build" }'
typescript
const sessionId = res.id.replace(/^resp_/, '');
const [action] = res.required_action!.actions;

const next = await client.agents.sessions.actions.resolve(sessionId, action.id, { decision: 'allow' });
// …or { decision: 'allow', approved_args: { … } }, or { decision: 'deny', comment: '…' }

console.log(next.status, next.output?.[0]?.content?.[0]?.text);

The call returns when the turn does — a normal response (or requires_action again if the agent hit a second gated tool). For a turn that may run long after the approval, add "background": true and poll the run; see Background runs.

Find out without polling ​

Two options when nobody is waiting on the call:

To list what is waiting right now: GET /api/client/v1/agents/sessions?status=requires_action, then GET /api/client/v1/agents/sessions/{id} for a session's pending_actions.

6. Let the agent ask questions ​

Approvals answer "may I do this?". Questions answer "which one do you want?" — switch on Let the agent ask the user questions in the same section. The agent then has an ask_user_question tool: it pauses with a question and options, and the card shows radio buttons (or checkboxes for a multi-select), plus a free-text box unless you turn Allow free-text answers off.

Over the API a question is an action of kind user_question; answer it with the question text as the key:

bash
curl -X POST "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION/actions/$ACTION" \
  -H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
  -d '{ "answers": { "Which environment should I use?": "staging" } }'
# Skip it instead:  -d '{ "cancelled": true }'

What can go wrong ​

SymptomCause
The agent never pausesThe tool is read-only and on Default; the tool is on Never ask; the call came from a sub-agent; the run is unattended and set to deny; or you are calling the published version and have not published since turning approvals on.
409 pending_action on a new messageThe session is waiting. Answer it, or set A new message while waiting to Cancel the request and continue.
403 action_forbiddenThe caller is not an allowed approver. A token answers as its owner.
409 action_not_pending / action_cancelled, 410 action_expiredSomeone answered first; the request was cancelled (a new message, a cancelled run, the timeout policy); or it ran out of time.
400 invalid_resolutionapproved_args is not an object, or does not satisfy the tool's schema. The request stays open — fix the body and send again.

Where to go next ​

Who can do this ​

An agent can stop and wait for a person: a tool approval (allow or deny a tool call before it runs) or a question (the agent asks the user to choose or type an answer). This page is about who is allowed to answer. It explains the agent's Who can answer setting, the Approve tool calls permission that sits behind it, how to appoint more approvers (for example a compliance group), and why answering a question follows a different rule.

Requires a Console release with the IAM engine

The agent.approve action, the built-in managed:agent:* policies and the Who can answer behaviour described here exist only in releases that include the IAM engine. How approvals and questions are configured per agent (tools that ask, timeouts, unattended runs) is part of the agent's own settings; this page covers the access side.

The short version ​

  • A pending tool approval may be answered by the person who started the session, and by whoever the agent's Who can answer setting accepts. Nobody else, even with Write access to Agents.
  • Approving is appointed, not inherited: Agents · Approve tool calls (agent.approve) is deliberately left out of the generic Write permission. A workspace writer cannot approve somebody else's tool call unless the setting accepts them.
  • You can appoint more approvers with an allow policy (a compliance group, an on-call rota) and narrow with a deny policy.
  • A question is not an approval. The person who started the session can always answer it; anybody else needs the permissions to view and act on others' sessions.

Who can answer: the agent's setting ​

In Studio, open the agent and go to Configure → Approvals & questions → Who can answer. The setting is stored as humanInTheLoop.approvers in the agent's configuration.

Option in StudioStored valueWho may answer a tool approval
Only the person who started the sessionownerThat person (and their API tokens).
The session owner and project admins (default)owner_or_adminThe above, workspace owners and admins, the admins of the agent's project (a project role given to a person or to a group assigned to the project), and members whose legacy workspace role is project_admin inside the projects they belong to.
Anyone with access to this agentproject_membersThe above, and every member of the agent's project.

Workspace owners and admins are matched on their own workspace role: an admin role that a person only receives through a group grant does not, by itself, make them an approver here (give them the project admin role, or appoint them with a policy).

Things that are true whatever you choose:

  • The person who started the session may always answer. The setting never takes that away.
  • An API token answers on behalf of its owner. Its own scope and the project's limits still apply on top.
  • Portals use the same rule; the portal's access list (and network restriction) is checked on top. See Portal access boundaries.
  • The setting is read as the agent version the request was raised under. Editing the agent later does not change who may answer a request that is already open, and a request of a deleted or disabled agent can still be answered under the same rule.
  • A session that no person started (a schedule, a public A2A call, an evaluation) has no owner. Under The session owner and project admins or Anyone with access to this agent the people the setting accepts can still answer; under Only the person who started the session nobody is accepted, so only a policy that appoints approvers applies.

The Approve permission ​

Approving a tool call (allow it, deny it, edit its arguments, or Don't ask again for this tool in this session) is a permission of its own, shown as Agents · Approve tool calls (action agent.approve) in the IAM center. It is decided by the same engine as everything else, with two built-in policies that translate the agent's setting into an allow:

Built-in policyWhat it appoints
managed:agent:approveA person who started the session, or whom the agent's setting accepts.
managed:agent:approve-tokenAn API token, exactly when its owner may.

You see them under IAM center → Policies → Built-in → Agent approvals. The generic Write permits explicitly exclude agent.approve, which is why Write on Agents alone approves nothing.

Appoint more approvers ​

Open IAM center → Policies → Create policy and use Allow with the action Agents · Approve tool calls (under What → Specific actions; it is marked Checked on agent requests). The policy names people, not sessions; combine it with Where to limit it to some agents or projects.

Example: let the compliance group approve across projects ​

  1. Create a group (for example Compliance) under IAM center → Principals → Groups and add the reviewers.
  2. Create an allow policy:
json
{
  "name": "Compliance can approve tool calls",
  "enabled": true,
  "spec": {
    "effect": "permit",
    "principals": { "groups": ["<compliance group id>"] },
    "actions": ["agent.approve"],
    "resources": { "services": ["agents"] },
    "conditions": { "channels": ["dashboard", "api"] }
  }
}
  1. Make sure the group can reach the answer button at all. Answering is a POST, so the people also need the Write level on Agents. The policy editor says so when it applies; give the group that level in its service permissions, or add agents.write to the same rule.

Only an owner or admin can enable an allow rule

Appointing approvers grants access, so it is permission management: only workspace owners and admins can save this policy enabled. A delegated IAM administrator can save it disabled and ask an owner or admin to enable it.

The Write level appoints approvers too

An allow rule that names the Write level (instead of the specific action) also covers agent.approve, because the action sits in the Write group. The people the rule names would be able to approve tool calls of the agents it reaches. The editor warns about this; choose specific actions if you only mean to give write access.

Token scope and the project's own limits still win over any allow policy: an allow can never go beyond them.

Example: a deny to narrow ​

A deny policy removes a person, a group, an agent or a channel from the people the setting accepts. This one keeps contractors from approving anything, even where the setting would accept them:

json
{
  "name": "Contractors cannot approve tool calls",
  "enabled": true,
  "spec": {
    "effect": "forbid",
    "principals": { "groups": ["<contractors group id>"] },
    "actions": ["agent.approve"],
    "resources": { "any": true }
  }
}

To keep approvals for one sensitive agent to its session owner, use the agent's Who can answer = Only the person who started the session setting; to take approvals away from a group for one agent, deny agent.approve for the group with Where → Specific resources naming that agent.

What this does and does not cover ​

ActionGoverned by
Allow or deny a tool call, edit its arguments, remember for this sessionagent.approve
End somebody's open approval as a side effect: cancelling a run that is paused on it, or sending a new message to an agent that cancels the pending request (A new message while waiting)agent.approve: ending an approval is answering it with "no"
Answer a questionNot an approval: it follows the session. The person who started the session may always answer; anybody else needs Agents · Act on others' sessions (agent.session_manage_others) and Agents · View others' sessions (agent.session_view_others)
The timeout sweep and the continuation that follows itThe system's, outside the permission engine; recorded in the audit log with the actor type system (system:timeout)

Restricting approvals therefore never stops people from answering their own sessions' questions.

What people see ​

The session page and the Sessions list do not guess from the viewer's role. For each open request the dashboard receives canResolve and, when it is false, resolveBlockedReason:

resolveBlockedReasonMeaning
approvers_ownerThe agent accepts only the person who started the session.
approvers_adminThe agent accepts the session owner, workspace admins and the project's admins.
approvers_memberThe agent accepts every member of the project, and the viewer is not one.
session_ownerA question: only the session's owner, and people allowed to view and act on others' sessions, may answer.
policyA rule of the workspace stops the viewer.
unavailableThe engine could not be asked.

The Waiting for you badge (and the status filter of that name) is shown only to somebody who could answer; everybody else sees Waiting for an approver. For a request the viewer may not answer, the card says why and its controls (Approve, Deny, Edit arguments, Don't ask again, or Send and Skip for a question) are disabled. When the server could not check (unavailable) the notice is shown and the controls stay usable: that is not a "no". The server decides again when the answer is sent either way.

Over the API ​

Answer a pending request with an API token:

http
POST /api/client/v1/agents/sessions/{conversationId}/actions/{actionId}
Authorization: Bearer <token>
Content-Type: application/json

{ "decision": "allow" }

{conversationId} is the id inside resp_{conversationId}. Refusals:

Statuserror.codeMeaning
403action_forbiddenThe caller may not answer this request: a tool approval needs Approve tool calls (the setting, any allow policy that appoints them, and any deny policy); a question follows the session's ownership. Carries error.decision_id.
503authorization_unavailableThe engine could not decide who may answer. Nothing was recorded: retry. Carries error.decision_id. A 503 is never a "no".
404action_not_foundNo such session or request in this project.
409action_not_pending, action_cancelledSomeone answered first, or the request was cancelled meanwhile (a new message, a cancelled run, the timeout policy).
410action_expiredThe request ran out of time.
json
{
  "error": {
    "type": "permission_error",
    "message": "You are not allowed to answer this request.",
    "code": "action_forbidden",
    "decision_id": "7c0f6b2e-5d1a-4c1e-9a53-0d9b8a1f2e44"
  }
}

Over A2A, a caller who may not answer gets the JSON-RPC error -32010 (with a decision_id) from tasks/cancel on a task that waits on a person, and from a message/send that would cancel the request. The request bodies for answering, background runs and the event stream are described with the agent's own approval settings.

Check who may approve ​

Use IAM center → Access check: choose the person, the action Agents · Approve tool calls and the agent. The form then asks for the Session started by person and The agent accepts as approvers (the agent's setting; leave it empty for the default). The result says whether the person is allowed and which rule decides, in plain words. The same check over the API:

bash
curl -X POST https://console.example.com/api/iam/simulate \
  -H "Content-Type: application/json" --cookie "token=..." \
  -d '{
    "principal": { "type": "user", "id": "<person id>" },
    "action": "agent.approve",
    "resource": { "kind": "resource", "type": "agent", "id": "<agent id>" },
    "context": { "channel": "dashboard", "ownerId": "<user who started the session>", "approvers": "owner_or_admin" }
  }'

The response includes session: { ownerId, approvers }, the session the check assumed.

A refused answer carries a decision id: paste it into Access check → Look up a denied request to replay exactly that request. See Troubleshooting and FAQ.

Troubleshooting ​

A workspace admin cannot approve. Under Only the person who started the session nobody else may, not even an owner. Check the agent's Who can answer setting, then the Access check for the person.

The compliance group is appointed but the button is disabled. The group needs the Write level on Agents as well, and neither a deny policy nor the token scope or project limits may stop them. Replay the refusal with its decision id.

A request cannot be answered after I changed the setting. The setting that applies is the one the request was raised under (the agent version at that time), not the current one.

Answers from a portal are refused. A portal's access list and network restriction apply on top of the approval rule. See Portal access boundaries.

What's new ​

  • agent.approve is an appointed action: the agent's Who can answer setting is now the input of a built-in policy, and an allow policy can appoint more approvers.
  • Answering a question follows session ownership instead of the approval rule.
  • Refusals carry a decision id, in the dashboard and in the client API (error.decision_id), and a 503 authorization_unavailable is distinct from a 403.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub