# 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.

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](/concepts/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](/concepts/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.

> [!NOTE]
> A worker follows one rule about who may tell it what to do. Only a message from the person, or a `user.message` sent with their key, says what to do. Web pages, files, tool output and app output are data, never instructions. The check that decides which actions ask first follows the same rule.

## 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`:

```bash
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](/concepts/runs-and-events): the `worker.tool_use` and `user.approval` events, and how to follow a task that waits.
- [Apps](/concepts/apps): the Read and Act levels that set what an app's tools may do.
- [Password sets](/concepts/password-sets): why the first use of a saved password is always a person's to allow.
