Skip to content
Get API Key

Guides

Answer approvals from your program

Notice a task that waits for an Allow, read what it asks, and allow or deny it from your program with a key made with Can allow actions.

Updated
View as Markdown

An approval is a worker asking before it acts. The task stops until someone answers. Approvals explains which actions ask and who may answer. A person can answer every approval in the app. This guide is for a program that answers some of them for you, by a rule you write, such as allowing a worker to save a note in one folder and refusing everything else.

Each step shows curl, TypeScript and Python. Pick one and the page keeps it picked. The TypeScript and Python steps build one small program, so add each step's code to the same file.

Before you start

  • A key made with Can allow actions turned on. In Settings, then API Keys, choose Create Key, name it, turn the switch on, and choose Create. A key without it gets 403 with person_required when it answers. API keys says what the switch opens.
  • The key in LITTLE_WORKER_API_KEY.
  • For curl: curl and jq.
  • For TypeScript: Node 22.18 or later, which runs a .ts file as it is. Save the program as approvals.ts and run node approvals.ts.
  • For Python: Python 3 and the requests library (pip install requests). Save the program as approvals.py and run python3 approvals.py.

Step 1: Make a worker that asks first

A worker asks before a step when its tool group is set to always_ask. This worker keeps notes, and asks before it writes one. Every other group that changes things asks too, unless you set it otherwise.

curl https://core.thelittleworker.com/api/v1/workers \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19" \
  -H "content-type: application/json" \
  -d '{
    "name": "Note keeper",
    "system": "You keep notes for a small shop. When the person tells you something to remember, save it as a note with your memory tools, at a path that starts with /shop/. Then say what you saved.",
    "tools": [
      {
        "type": "worker_toolset_20260401",
        "configs": [{ "name": "memories", "permission_policy": { "type": "always_ask" } }]
      }
    ]
  }'

The answer is the new worker, and its tools show the policy of every group. With curl, keep its id:

Shell
export WORKER_ID="worker_bf5ff997fca044b38dc1cc9ea5b60100"

Step 2: Give it a task

Ask for something that needs a note. The worker will try to write one, and the task will stop before it does.

curl https://core.thelittleworker.com/api/v1/tasks \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19" \
  -H "content-type: application/json" \
  -d '{
    "worker": "'"$WORKER_ID"'",
    "title": "Shop hours",
    "initial_events": [
      {
        "type": "user.message",
        "content": [{ "type": "text", "text": "Remember that the shop now opens at 7 am." }]
      }
    ]
  }'

With curl, keep the task's id:

Shell
export TASK_ID="task_01M5TTRJKWYAK3XHFX432K5MBV"

Step 3: Notice that it waits

A task that waits for an Allow stops as idle. Its state.needs is allow, and its stop_reason is requires_action. Read the task until it stops:

curl -s https://core.thelittleworker.com/api/v1/tasks/$TASK_ID \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19" \
  | jq '{status, state, stop_reason}'

Once the worker has reached the note, the answer looks like this:

JSON
{
  "status": "idle",
  "state": { "status": "needs_you", "needs": "allow", "waiting_reason": null },
  "stop_reason": {
    "type": "requires_action",
    "event_ids": ["event_3b7e1a9c5d2f4e8a9c0b1d2e3f4a5b6c"],
    "blocking": [
      {
        "type": "approval",
        "event_id": "event_3b7e1a9c5d2f4e8a9c0b1d2e3f4a5b6c",
        "person_only": false
      }
    ]
  }
}
  • event_ids holds the id of each worker.tool_use event that asks. That id is the tool_use_id you answer with, and it never changes, even when the same step is asked again.
  • blocking lists every step the task waits on, in the order they were asked. An approval is an Allow for a tool or an app action.
  • person_only is true for a step only a person can answer, in the app. A key that tries gets 403 with person_required. Leave those for the person.

blocking can list other kinds of step. A custom_tool_use is a call to one of the worker's own tools, which your code answers with user.custom_tool_result. The rest are cards in the app, such as a question or a form, and are always person_only.

You can also learn that a task waits without polling it. The stream sends a task.status_idle event with requires_action (Follow a task as it works), a webhook sends the same notice (Webhooks), and GET /tasks?status=needs_you lists every task that needs you, so you can look for state.needs allow.

Step 4: Read what it asks

The id in event_ids names a worker.tool_use event, or a worker.mcp_tool_use event for an app. The event holds the tool's name and its input, which is what you decide on. List the task's tool uses and keep the ones that wait:

curl -s -g "https://core.thelittleworker.com/api/v1/tasks/$TASK_ID/events?types[]=worker.tool_use&types[]=worker.mcp_tool_use&order=desc&limit=100" \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19" \
  | jq '.data[] | {id, name, input, evaluated_permission}'

Shortened, the event looks like this. evaluated_permission is ask and evaluation says why: always_ask is the setting you made.

JSON
{
  "type": "worker.tool_use",
  "id": "event_3b7e1a9c5d2f4e8a9c0b1d2e3f4a5b6c",
  "task_id": "task_01M5TTRJKWYAK3XHFX432K5MBV",
  "sequence": "6",
  "processed_at": "2026-10-25T22:07:41.318Z",
  "name": "memory.write",
  "input": { "path": "/shop/hours.md", "content": "The shop now opens at 7 am." },
  "evaluated_permission": "ask",
  "evaluation": { "type": "always_ask" }
}

A password the worker uses never shows in input: it is masked.

Step 5: Allow or deny it

Write the rule, then answer with a user.approval event (Send events). result is allow or deny. A denial may carry a deny_message, up to 10,000 characters, and the worker reads it as the reason the action failed. A message with allow is refused with 400.

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_3b7e1a9c5d2f4e8a9c0b1d2e3f4a5b6c",
        "result": "allow"
      }
    ]
  }'

The answer is 200, with one entry in data for the event you sent. status is allowed or denied:

JSON
{
  "data": [
    {
      "type": "user.approval",
      "tool_use_id": "event_3b7e1a9c5d2f4e8a9c0b1d2e3f4a5b6c",
      "status": "allowed"
    }
  ]
}

Every answer, yours or a person's in the app, is listed in the task's events as a user.approval. For yours, api_key_id is the key that answered. For a person's, it is null.

A key answers one action at a time. It cannot choose to remember an answer, so it can never loosen a setting.

Step 6: Go on until the task is done

After an answer the task runs again and may stop for another Allow, so keep looping. This loop reads the task, answers whatever waits, and ends when the task stops for any other reason. It skips an id it already answered, because the task can still show its old stop for a moment after you answer.

async function handle(taskId: string): Promise<Task> {
  const answered = new Set<string>()
  for (;;) {
    await new Promise((resolve) => setTimeout(resolve, 2000))
    const current = await call<Task>("GET", `/tasks/${taskId}`)
    if (current.status !== "idle") continue
    const stop = current.stop_reason
    if (stop?.type !== "requires_action") return current
    const open = (stop.blocking ?? []).filter(
      (step) => step.event_id === null || !answered.has(step.event_id),
    )
    if (open.some((step) => step.type !== "approval" || step.person_only)) {
      console.log("A step waits for a person, in the app.")
      return current
    }
    const ids = open.flatMap((step) => (step.event_id === null ? [] : [step.event_id]))
    for (const use of await asked(taskId, ids)) {
      await answer(taskId, use)
      answered.add(use.id)
    }
  }
}

const done = await handle(task.id)
console.log("Stopped:", done.stop_reason?.type, done.state.status)

After the lines the earlier steps print, the program prints each answer and then the stop:

Text
memory.write {"path":"/shop/hours.md","content":"The shop now opens at 7 am."} -> allowed
Stopped: end_turn done

If your rule denies a step, the worker reads the reason, and usually tries something else, which may ask again. A task keeps stopping for as long as the worker keeps asking, so a program that must end should count its answers and take the task back after a few (Stop work and keep history).

See every approval that waits

GET /approvals (List approvals) lists the approvals that wait for an answer across all your tasks, newest first. It lists app actions and steps on a Mac. It leaves out the steps only a person answers, and the steps a worker's tool group setting asks, such as this guide's note, which a task's requires_action stop always names.

Shell
curl https://core.thelittleworker.com/api/v1/approvals \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19"

Each approval has these fields. GET /approvals/{approval_id} reads one in any status, which the list does not show once it is answered:

Field What it holds
id The approval's id, starting with approval_.
task_id, worker_id The task that waits and its worker.
tool_use_id The id to answer with, the worker.tool_use event that asks.
tool_name, input What the worker wants to do. A Mac action's command and environment are never shown.
status waiting, allowed, denied, expired or used.
created_at, expires_at, decided_at When it was asked, when its time ends, and when it was answered.

For an app action or a tool group's ask, expires_at does not end the wait. Answer late, by tool_use_id, and the step still runs. A step on a Mac has a deadline: after it, the step fails with approval_expired and the task stops again for what still waits.

When an answer is refused

Status error_code When it happens
403 person_required The key was made without Can allow actions, or only a person may answer this step.
403 not_task_starter On a shared workspace, the task was started by someone else. A key answers only the approvals of tasks its own person started.
404 not_found No approval is asked for that tool_use_id in this task.
409 conflict The approval already has the other answer, or the server took it back.
409 approval_consumed The step was already allowed and has run.
409 approval_expired A step on a Mac passed its deadline.
400 validation_error A deny_message came with allow, or is over 10,000 characters.

Answering the same way twice before the step runs is safe: the second answer changes nothing.

Next steps

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