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.
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_allowruns the step.always_askwaits for an Allow.autolets 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 resultPermission 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_allowandautocannot turn this off. A password saved withpermissionalwaysis 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:
{
"type": "requires_action",
"event_ids": ["event_9a4e7c1b3d5f4a28b6c0e2d8f1a73b95"],
"blocking": [
{
"type": "approval",
"event_id": "event_9a4e7c1b3d5f4a28b6c0e2d8f1a73b95",
"person_only": false
}
]
}event_idsholds the id of eachworker.tool_useorworker.custom_tool_usethat asks. An approval carries the same id intool_use_id, and it never changes, even when the approval is asked again.blockinglists every step the task waits on, in the order they were asked.typesays what kind of step it is,event_idis the tool use when there is one, andperson_onlyistruewhen 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
403witherror_codeperson_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
403witherror_codeperson_required. Such a step showsperson_only: trueinblocking. - 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
403witherror_codenot_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:
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."
}
]
}'{
"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_messageis 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_useanduser.approvalevents, 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.