Skip to content
Get API Key

Concepts

Approvals

An approval is a worker asking before it takes an action, and a person or a key with permission answering Allow or Don't Allow.

Updated
View as Markdown

Some actions are too big to take without asking: sending an email, paying, posting in public, deleting, or signing in with a saved password for the first time. When a worker wants to take one, it stops and asks. The ask is an approval. The task waits until someone answers, then the worker goes on.

A person answers in the app. A program can answer too, but only with a key made for it, and never for the steps that are a person's alone. This page explains which actions ask, who may answer, and how the answer reaches the worker.

Which actions ask

Three things decide whether a step asks first.

  • The worker's policy. Each tool group of a worker, and each tool of an app, has a permission_policy. always_allow runs the step. always_ask waits for an Allow. auto lets a step that only reads run, and sends every other step to a safety check. The check runs what it allows, refuses what it blocks, and asks when it is unsure, slow (3 seconds) or unavailable. A refused step fails with the tool result Permission to use {tool_name} has been denied.
  • The app's levels. A person sets Read and Act for each app. A worker's policy can narrow them, never widen them. See Apps.
  • Fixed rules. A step that sends, pays, posts, deletes, changes settings, or uses a saved password for the first time always asks, whatever its policy says. always_allow and auto cannot turn this off. A password saved with permission always is already allowed, so its first use does not ask. See Password sets.

A step that runs on a person's Mac asks as the Mac's own settings say.

A custom tool of your own is different. Little Worker never runs it and asks no one. The task stops with requires_action and waits for your user.custom_tool_result.

Each worker.tool_use and worker.mcp_tool_use event says how the step was decided, once something has decided it:

evaluation.type What decided it
always_allow or always_ask The step's setting.
auto The safety check. Its evaluated_permission is allow, deny with reason_code high_risk, or ask with indeterminate when the check was unsure, failed or ran out of time.
fixed_rule A fixed rule. Its reason_code is send, pay, post, delete, change_settings or first_password_use.

evaluated_permission is allow, ask or deny. A step refused before any setting applied has deny and no evaluation. Treat an evaluation.type or reason_code you do not know as a plain ask.

How a task waits

When a worker takes a step that asks, the task stops as idle with stop_reason requires_action. Its state.status is needs_you, and state.needs is allow when it waits on an Allow. stop_reason lists what it waits on:

JSON
{
  "type": "requires_action",
  "event_ids": ["event_9a4e7c1b3d5f4a28b6c0e2d8f1a73b95"],
  "blocking": [
    {
      "type": "approval",
      "event_id": "event_9a4e7c1b3d5f4a28b6c0e2d8f1a73b95",
      "person_only": false
    }
  ]
}
  • event_ids holds the id of each worker.tool_use or worker.custom_tool_use that asks. An approval carries the same id in tool_use_id, and it never changes, even when the approval is asked again.
  • blocking lists every step the task waits on, in the order they were asked. type says what kind of step it is, event_id is the tool use when there is one, and person_only is true when only a person can answer.

A step is one of these kinds:

blocking.type What it waits for
approval An Allow for one tool use.
auto_review Permission to run something the safety check stopped.
virtual_card A purchase.
cookie Use of the person's existing logins.
email_draft and slack_draft A message the person sends or discards.
question An answer to a question.
form Fields to fill into a page. Secret fields never reach the worker.
password_request A value the person pastes. The worker never sees it.
handoff A step on the computer that only the person can do.
hosted_decision A decision in the hosted plane. It does not stop the work. If the work ends while it is open, the stop is requires_action instead of end_turn.
custom_tool_use Your user.custom_tool_result.

Only approval and custom_tool_use have an event_id. Every other kind is a card in the app, and a task that waits only on cards stops with requires_action and no event_ids.

When some steps are answered and others still wait, the task stops again with requires_action and the steps that are left. No webhook is sent for a stop left after a partial answer.

Who may answer

A person can answer every approval in the app. A key can answer an approval only when all three of these are true:

  • The key was made with Can allow actions. Without it, the answer is 403 with error_code person_required, and you answer in the app. Make the key in Settings, API Keys, and turn that option on.
  • The step is not a person's alone. These are always a person's, in the app, whatever the key: a worker's first use of a saved password, an email it sends, a press on a risky control in its browser, a public post, a payment, and every card. A key that tries gets 403 with error_code person_required. Such a step shows person_only: true in blocking.
  • The key's person started the task. On a shared workspace, a key answers only approvals of tasks its own person started. Any other is 403 with error_code not_task_starter.

An approval answers once. A key cannot choose "remember always", so it can never loosen a setting.

Read approvals

GET /approvals lists the approvals that wait for you, newest first, across all your tasks. GET /approvals/{approval_id} reads one in any status.

Field What it is
id The approval's id, which starts with approval_.
task_id and worker_id The task and worker that asked.
tool_request_id and tool_use_id The step asked about. tool_use_id is the id of its worker.tool_use event, and it is what your answer names.
tool_name and input What the worker wants to do. A saved password's value is hidden wherever it would appear.
status waiting, allowed, denied, expired or used.
created_at, expires_at and decided_at When it was asked, when it runs out, and when it was answered (null until then).

An approval never shows a Mac action's command or environment.

status What it means
waiting Nobody has answered it, it has not run, and expires_at has not passed.
allowed A person or a key allowed it, and the action has not run yet.
denied A person or a key did not allow it. The action does not run.
expired Nobody answered before expires_at, or Little Worker took it back. A renewed step asks again under a new approval id.
used It was allowed and the action ran.

The list shows only waiting approvals, and leaves out the kinds that only the person answers on their cards in the app: the first use of a password (tool_name password.use), sending an email (email.send), and a press in the worker's hosted browser (remote.browser.activate). Read one of those by its id to see it. A refund is answered only on its card, and reading it is 404.

Answer an approval

Send a user.approval event to the approval's task. tool_use_id is an id from event_ids:

Shell
curl https://core.thelittleworker.com/api/v1/tasks/$TASK_ID/events \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19" \
  -H "content-type: application/json" \
  -d '{
    "events": [
      {
        "type": "user.approval",
        "tool_use_id": "event_9a4e7c1b3d5f4a28b6c0e2d8f1a73b95",
        "result": "deny",
        "deny_message": "Not before Friday."
      }
    ]
  }'
JSON
{
  "data": [
    {
      "type": "user.approval",
      "tool_use_id": "event_9a4e7c1b3d5f4a28b6c0e2d8f1a73b95",
      "status": "denied"
    }
  ]
}

result is allow or deny. deny_message is up to 10,000 characters and goes only with deny; with allow, the send is 400. The answer goes to the approval asked last for that tool use.

Every answer, yours or a person's in the app, is listed as a user.approval event. Its api_key_id names the key that answered, and is null when a person answered in the app.

How the answer reaches the worker

  • Allow. The step runs, and the worker gets its result as usual.
  • Deny. The step does not run. The worker gets the failure of that tool, and your deny_message is the reason it reads.

After the last waiting step is answered, the task runs again: task.status_running follows, and the worker goes on.

A step that waits on a person or an app never runs out for you. This covers an app action, a password's first use, an email and a browser press. If nobody answers, the approval can show expired, but a late answer by tool_use_id still runs the step, on an approval asked again inside the service.

A step that runs on a Mac has a deadline. When it passes unanswered, the step ends as a failed tool result with error.code approval_expired. The task is then idle again with requires_action, naming only the steps still waiting, or, when none are left, it goes on with task.status_running.

Limits

What Limit
Answers a key may give Allow or deny one tool use at a time. No "remember always".
deny_message 10,000 characters, with deny only
Automatic check 3 seconds, then it asks

Next steps

  • Runs and events: the worker.tool_use and user.approval events, and how to follow a task that waits.
  • Apps: the Read and Act levels that set what an app's tools may do.
  • Password sets: why the first use of a saved password is always a person's to allow.

Type a few words, like “give a task” or “errors”.