# Events

List a task's events, send it messages, answers and an accept or a send back, and follow what happens as it happens.

An event is one thing that happened in a task, in order: a message, a tool the worker used, a stop. A task's events tell its whole story, and you can read them at any time.

These routes list a task's events, send it events, and stream them live. Reading the task says where it stands now. Its events say how it got there. You send events to steer it: a message, a stop, an answer to an approval, or what you think of the result.

[Runs and events](/concepts/runs-and-events) explains the order events come in and how to read them without missing one. Work a worker hands to a helper runs in a thread of the task. Its steps are in these events too, with a `task_thread_id`. The routes for threads are not on this page.

> [!NOTE]
> A task's events are a session's events in Claude Managed Agents. `worker.*` events are its `agent.*` events, and `task.status_*` events are its `session.status_*` events. An approval is a tool confirmation, and `user.approval` is its `user.tool_confirmation`.

> [!TIP]
> To follow a task without missing an event or repeating one, list its events, then open the stream with `after` set to the `sequence` of the last event you listed. The [Quickstart](/get-started/quickstart#follow-the-work) follows a task this way.

## The event object

Every event has the same five fields: `type`, `id`, `task_id`, `sequence` and `processed_at`. The rest depend on the `type`, and each type is listed below with the fields it adds. Times are RFC 3339 in UTC, to the millisecond.
The types fall into a few groups:
- `user.*` and `system.message` are what was sent to the task.
- `worker.*` is what the worker did: its answers, the tools it used and what they answered, and its work with helpers.
- `span.*` marks the start and end of a model request or an outcome check round.
- `task.*` says where the task stands: running, stopped, failed, changed or deleted.
- `workflow_run.*` follows a workflow run, a plan of phases that helpers work through.
Little Worker may add event types, and fields to an event, without a new API version. Skip a `type` you do not know instead of failing on it.

- `type` (string): What kind of event it is.

- `id` (event id): The event's id: `event_` and 32 hex digits. A `worker.tool_use` event's id is also the tool use id that its answers name.

- `task_id` (task id): The task it belongs to.

- `sequence` (string): Its place in the task's chat, a whole number as a string, such as `"10"`. It orders lists, streams and every cursor, since two events can share a time. The sequence numbers of one task's events skip, because the chat holds more than the task's events.

- `processed_at` (time, or null): When Little Worker wrote the event. A `user.message` sent while the worker is busy has `null` until a run takes it, and stays listed where it was accepted.

- `user.message` (event): A message sent to the task, by a person in the app or by you with a key.

  - `message_id` (message id): The message's id: `message_` and 32 hex digits.

  - `content` (array of content blocks): What was sent, as it was sent: `text`, `image` and `document` blocks. A `redacted` block is content that is withheld.

- `system.message` (event): A system message sent right after a person's message. It is listed right after that message, with the same `sequence`.

  - `content` (array of text blocks): The text the model reads in its system role.

- `user.define_outcome` (event): An outcome set for the task: what its work is checked against. Listed where it was sent.

  - `outcome_id` (outcome id): The outcome, `outc_` and 32 hex digits.

  - `description` (string): What done looks like.

  - `rubric` (object): The check, as `{ "type": "text", "content" }` or `{ "type": "file", "file_id" }`.

  - `done_means` (array of strings, or null): A short list of checks graded one by one, sent in place of a rubric. `null` when the outcome has a rubric.

  - `max_iterations` (integer): How many check rounds before the outcome gives up.

- `user.approval` (event): An approval was answered, by a key or by the person in the app.

  - `tool_use_id` (event id): The tool use that asked.

  - `result` (string): The answer. One of `allow`, `deny`.

  - `deny_message` (string, or null): Why it was denied, as the worker reads it.

  - `api_key_id` (API key id, or null): The key that answered. `null` when the person answered in the app.

- `user.interrupt` (event): The work was stopped, by your `user.interrupt` or by the person's Stop.

  - `run_id` (run id): The run it stopped.

- `user.tool_result` (event): A tool's answer, sent back by a Mac that ran it. A Mac is the person's own computer, so its answers are listed as the person's.

  - `tool_use_id` (event id): The tool use it answers.

  - `status` (string): How the tool ended. `outcome_unknown` is a started call that was stopped, when nobody can prove what it did. One of `succeeded`, `failed`, `cancelled`, `outcome_unknown`.

  - `output` (JSON): What the tool answered. Left out when it answered nothing.

  - `error` (object): Why the tool failed. Left out when it did not.

    - `code` (string): The reason, such as `approval_expired`.

    - `message` (string): What went wrong.

  - `is_error` (boolean): `true` when the tool failed.

- `user.custom_tool_result` (event): Your answer to a custom tool use.

  - `custom_tool_use_id` (event id): The `worker.custom_tool_use` it answers.

  - `content` (array of content blocks): The answer: `text`, `image`, `document` and `search_result` blocks.

  - `is_error` (boolean): `true` when it is a failure the worker should read as one.

- `worker.message` (event): The worker's answer. It holds the whole text, and it is the record of what the worker said.

  - `message_id` (message id): The message's id.

  - `content` (array of content blocks): The answer, as `text` blocks. A `redacted` block is content that is withheld.

  - `sources` (array of objects): The web pages the answer cites, each once, in the order it cites them: a `url` and a `title`. Empty when it cites none.

- `worker.tool_use` (event): The worker used a tool of its own. Any password in the `input` is masked.

  - `id` (event id): The tool use id. The tool's result, an approval and a `requires_action` stop all name the step by it.

  - `name` (string): The tool's name.

  - `input` (JSON): What the worker sent it.

  - `evaluated_permission` (string): What decided the step: `allow`, `ask` or `deny`. Left out while nothing has decided it yet. One of `allow`, `ask`, `deny`.

  - `evaluation` (object): Why. Left out while nothing has decided the step, and on a step refused before any setting applied. Treat a `type` or a `reason_code` you do not know as a plain `ask`.

    - `type` (string): `always_allow` and `always_ask` are the tool's own setting. `auto` is the automatic check. `fixed_rule` is an action that always asks, whatever the setting says. One of `always_allow`, `always_ask`, `auto`, `fixed_rule`.

    - `evaluated_permission` (object): On `auto` and `fixed_rule` only: the decision. Its `type` is `allow`, `ask` or `deny`. A `reason_code` goes with `ask` and `deny`: on `auto`, `high_risk` for a block and `indeterminate` when the check was unsure, failed or ran out of time; on `fixed_rule`, one of `send`, `pay`, `post`, `delete`, `change_settings` or `first_password_use`.

  - `task_thread_id` (thread id): Set when the step belongs to a helper's thread and the task's events show it too.

- `worker.tool_result` (event): A tool's answer to the worker.

  - `tool_use_id` (event id): The `worker.tool_use` it answers.

  - `status` (string): How the tool ended. `outcome_unknown` is a started call that was stopped, when nobody can prove what it did. One of `succeeded`, `failed`, `cancelled`, `outcome_unknown`.

  - `output` (JSON): What the tool answered. Left out when it answered nothing.

  - `error` (object): Why the tool failed. Left out when it did not.

    - `code` (string): The reason, such as `approval_expired`.

    - `message` (string): What went wrong.

- `worker.mcp_tool_use` (event): The worker called a tool of one of your apps.

  - `id` (event id): The tool use id, as on a `worker.tool_use`.

  - `app_name` (string): The app's name.

  - `name` (string): The app's own name for the tool.

  - `input` (JSON): What the worker sent it.

  - `evaluated_permission` (string): What decided the step: `allow`, `ask` or `deny`. Left out while nothing has decided it yet. One of `allow`, `ask`, `deny`.

  - `evaluation` (object): Why. Left out while nothing has decided the step, and on a step refused before any setting applied. Treat a `type` or a `reason_code` you do not know as a plain `ask`.

    - `type` (string): `always_allow` and `always_ask` are the tool's own setting. `auto` is the automatic check. `fixed_rule` is an action that always asks, whatever the setting says. One of `always_allow`, `always_ask`, `auto`, `fixed_rule`.

    - `evaluated_permission` (object): On `auto` and `fixed_rule` only: the decision. Its `type` is `allow`, `ask` or `deny`. A `reason_code` goes with `ask` and `deny`: on `auto`, `high_risk` for a block and `indeterminate` when the check was unsure, failed or ran out of time; on `fixed_rule`, one of `send`, `pay`, `post`, `delete`, `change_settings` or `first_password_use`.

  - `task_thread_id` (thread id): Set when the step belongs to a helper's thread and the task's events show it too.

- `worker.mcp_tool_result` (event): The app's answer to the worker.

  - `mcp_tool_use_id` (event id): The `worker.mcp_tool_use` it answers.

  - `status` (string): How the tool ended. `outcome_unknown` is a started call that was stopped, when nobody can prove what it did. One of `succeeded`, `failed`, `cancelled`, `outcome_unknown`.

  - `output` (JSON): What the tool answered. Left out when it answered nothing.

  - `error` (object): Why the tool failed. Left out when it did not.

    - `code` (string): The reason, such as `approval_expired`.

    - `message` (string): What went wrong.

  - `is_error` (boolean): `true` when the call failed.

- `worker.custom_tool_use` (event): The worker called one of its custom tools. Little Worker never runs a custom tool: the task waits with `requires_action` until a `user.custom_tool_result` answers. A permission check never applies to it, so it has no `evaluated_permission`.

  - `id` (event id): The id your answer names in `custom_tool_use_id`.

  - `name` (string): The tool's name.

  - `input` (JSON): What the worker sent it.

  - `task_thread_id` (thread id): Set when the call belongs to a helper's thread and the task's events show it too.

- `worker.thread_context_compacted` (event): The worker's context grew too long and was squeezed into a summary. One event for each squeeze.

- `span.outcome_evaluation_start` (event): A check round of an outcome began.

  - `outcome_id` (outcome id): The outcome, `outc_` and 32 hex digits.

  - `iteration` (integer): The round, counted from `0`.

- `span.outcome_evaluation_ongoing` (event): A check round is still going. None is sent today; the type is listed so a client written for Claude's heartbeat reads this one.

  - `outcome_id` (outcome id): The outcome, `outc_` and 32 hex digits.

  - `iteration` (integer): The round.

- `span.outcome_evaluation_end` (event): A check round ended.

  - `outcome_id` (outcome id): The outcome, `outc_` and 32 hex digits.

  - `outcome_evaluation_start_id` (event id): The `span.outcome_evaluation_start` it closes.

  - `iteration` (integer): The round.

  - `result` (string): How the round came out. One of `satisfied`, `needs_revision`, `max_iterations_reached`, `failed`, `interrupted`.

  - `explanation` (string): The grader's reasons, in plain words.

  - `usage` (object): What the grader used. A check is one model call with no prompt cache, so the cache counts are `0`.

    - `input_tokens` (integer): Tokens read.

    - `output_tokens` (integer): Tokens written.

    - `cache_creation_input_tokens` (integer): Always `0`.

    - `cache_read_input_tokens` (integer): Always `0`.

- `span.model_request_start` (event): The worker sent one request to the model. Listed before what the answer wrote.

- `span.model_request_end` (event): The request ended. Listed after what its answer wrote, and before the stop that follows.

  - `model_request_start_id` (event id): The `span.model_request_start` it closes.

  - `is_error` (boolean, or null): `true` when the request failed, broke off or was stopped. Nothing is counted then.

  - `model_usage` (object): What the request used.

    - `input_tokens` (integer): Tokens read, without the prompt cache's tokens.

    - `output_tokens` (integer): Tokens written.

    - `cache_creation_input_tokens` (integer): Tokens written to the prompt cache.

    - `cache_read_input_tokens` (integer): Tokens read from the prompt cache.

    - `speed` (string): The speed it ran at. One of `standard`, `fast`.

    - `inference_geo` (string): The region that served it. One of `us`, `global`.

- `worker.thinking` (event): The model reasoned in that request. A progress signal with no content, listed right after the request's start.

- `task.error` (event): Something failed. An exhausted error comes right before the `task.status_idle` it caused.

  - `error` (object): What failed.

    - `type` (string): The kind of failure. One of `model_overloaded_error`, `model_rate_limited_error`, `model_request_failed_error`, `billing_error`, `app_connection_failed_error`, `app_authentication_failed_error`, `password_host_unreachable_error`, `repository_authentication_error`, `repository_forbidden_error`, `repository_not_found_error`, `repository_checkout_error`, `repository_clone_error`, `unknown_error`.

    - `message` (string): What went wrong.

    - `retry_status` (object): Whether Little Worker tries again, in its `type`: `retrying` (it goes on by itself), `exhausted` (the work stopped, and the task takes another message) or `terminal` (the task cannot go on).

    - `code` (string): Little Worker's own code for the reason, such as `password_host_blocked` or `repository_unavailable`.

    - `app_name` (string): On an app error: the app.

    - `password_id` (password id): On `password_host_unreachable_error`: the password whose site the worker's sites block. One comes for each such password, and the task goes on without it.

    - `password_set_id` (password set id): On `password_host_unreachable_error`: its set.

    - `repository_url` (string, or null): On a repository error: the repository the task could not use. The task goes on without it.

  - `cap` (string, or null): On a `billing_error` that is `retrying`: the plan, trial, balance or Limit cap that holds the task. Left out otherwise.

- `task.status_running` (event): The task began running: its work was accepted, or it went on after it had stopped. One event for each change of status, so a start that follows a queue adds none.

  - `run_id` (run id): The run, one stretch of the task's work.

- `task.usage` (event): What the task's work used, up to the stop that follows. One comes right before every `task.status_idle`.

  - `usage` (object): The fields of the task's `usage`, counted at that stop. After an interrupt, the model call that was cut off is charged a few seconds later, so this event leaves it out. Read the event again later, or the task, to count it.

  - `budget` (object): The task's budget.

- `task.status_idle` (event): The worker stopped. Read `stop_reason` to know why.

  - `run_id` (run id): The run, one stretch of the task's work.

  - `stop_reason` (object): Why the worker stopped.

    - `type` (string): `end_turn`: its turn is over, also after an interrupt. `requires_action`: it waits for you or a person. `retries_exhausted`: the work failed, and a `task.error` came just before. `budget_reached`: the task's own budget holds it. `refusal`: the model declined the turn. `turn_limit` and `time_limit`: a run guard stopped the run cleanly, and a new message or a send back starts a fresh run. One of `end_turn`, `requires_action`, `retries_exhausted`, `budget_reached`, `refusal`, `turn_limit`, `time_limit`.

    - `event_ids` (array of event ids): With `requires_action`: the `worker.tool_use`, `worker.mcp_tool_use` and `worker.custom_tool_use` events it waits on. Answer a tool use with a `user.approval`, and a custom tool use with a `user.custom_tool_result`.

    - `blocking` (array of objects): With `requires_action`: every step it waits on, in the order they were asked, cards in the app included.

      - `type` (string): The kind of step. One of `approval`, `auto_review`, `virtual_card`, `cookie`, `email_draft`, `slack_draft`, `question`, `form`, `password_request`, `handoff`, `hosted_decision`, `custom_tool_use`.

      - `event_id` (event id, or null): The `worker.tool_use` event of an `approval`, or the `worker.custom_tool_use` event of a `custom_tool_use`. `null` for every other kind, which is a card in the app.

      - `person_only` (boolean): `true` when only the person can answer it, in the app, and a key never can.

    - `limit` (integer): With `turn_limit`: the most model turns a run takes.

    - `limit_ms` (integer): With `time_limit`: the most active model time a run takes, in milliseconds.

  - `stop_details` (object, or null): Why the model declined the turn, after a `refusal`. `null` for every other stop.

    - `type` (string): Always `refusal`.

    - `category` (string, or null): The policy that declined it, such as `cyber`, `bio`, `frontier_llm`, `reasoning_extraction` or `general_harms`. More may come.

    - `explanation` (string, or null): A sentence for people. Its wording can change, so do not parse it.

- `task.status_rescheduled` (event): A run hit a passing fault, and Little Worker runs it again by itself.

  - `run_id` (run id): The run, one stretch of the task's work.

- `task.status_terminated` (event): The task was taken back with `POST /tasks/{task_id}/withdraw`. It takes nothing more.

- `task.updated` (event): The task was changed with `POST /tasks/{task_id}`. It shows what the change set.

  - `title` (string, or null): The new title. Left out when the change did not set it.

  - `metadata` (object): All of the task's labels, while any is left. Left out when the change did not set them.

  - `budget` (object): The new budget. Left out when the change did not set it.

- `task.deleted` (event): The task was deleted. An open stream reads it and ends. The task's other status events go with its history.

- `history.purged` (event): On a plan that keeps history for a set time, such as the free plan, older history was deleted. A list that starts at the beginning shows the newest one first.

  - `before` (time): The cutoff: history older than this is gone.

- `task.thread_created` (event): A helper's thread began. The worker handed it work.

  - `task_thread_id` (thread id): The thread of the helper this step is about.

  - `worker_name` (string): The helper's name.

  - `workflow_run_id` (workflow run id, or null): The workflow run the thread belongs to, if any.

- `task.thread_status_running` (event): The thread is at work.

  - `task_thread_id` (thread id): The thread of the helper this step is about.

  - `worker_name` (string): The helper's name.

- `task.thread_status_idle` (event): The thread stopped.

  - `task_thread_id` (thread id): The thread of the helper this step is about.

  - `worker_name` (string): The helper's name.

  - `stop_reason` (object): Why, in the same form as on `task.status_idle`. A thread can also stop with `requires_action`, which waits on the `event_ids` it lists.

  - `stop_details` (object, or null): As on `task.status_idle`.

- `task.thread_status_rescheduled` (event): A cap holds the thread. It goes on when there is room.

  - `task_thread_id` (thread id): The thread of the helper this step is about.

  - `worker_name` (string): The helper's name.

- `task.thread_status_terminated` (event): The thread ended.

  - `task_thread_id` (thread id): The thread of the helper this step is about.

  - `worker_name` (string): The helper's name.

- `worker.thread_message_sent` (event): The instructions the worker sent a helper.

  - `to_task_thread_id` (thread id): The helper's thread.

  - `to_worker_name` (string, or null): The helper's name.

  - `content` (array of content blocks): The instructions, as `text` blocks.

- `worker.thread_message_received` (event): The answer a helper sent back.

  - `from_task_thread_id` (thread id): The helper's thread.

  - `from_worker_name` (string, or null): The helper's name.

  - `content` (array of content blocks): The answer, as `text` blocks.

- `workflow_run.created` (event): A workflow run started: a plan of named phases, each a set of helpers that work side by side. No webhook hears a workflow run's events.

  - `workflow_run_id` (workflow run id): The workflow run, `wrun_` and 32 hex digits.

  - `name` (string): The run's name.

  - `description` (string, or null): What the run is for.

  - `phases` (array of objects): Its phases, in order: each has an `id` (`wrph_` and 32 hex digits), a `name`, and a `description` that may be `null`.

- `workflow_run.status_running` (event): The run is at work, again after a pause.

  - `workflow_run_id` (workflow run id): The workflow run, `wrun_` and 32 hex digits.

- `workflow_run.status_idle` (event): The run paused: a helper is held at the task's budget, or an interrupt paused it.

  - `workflow_run_id` (workflow run id): The workflow run, `wrun_` and 32 hex digits.

- `workflow_run.status_ended` (event): The run ended. It is always the run's last event.

  - `workflow_run_id` (workflow run id): The workflow run, `wrun_` and 32 hex digits.

  - `result` (object): How it ended, in its `type`: `completed`, `stopped`, or `error` with the `error`.

- `workflow_run.error` (event): The run's error, right before its end, or a start that made no run.

  - `workflow_run_id` (workflow run id, or null): `null` when the error kept a run from being made.

  - `error` (object): What went wrong. It never holds what the run wrote.

    - `type` (string): The kind of error. One of `timeout_error`, `program_error`, `unknown_error`, `thread_limit_error`, `max_workflow_runs_error`.

    - `message` (string): A sentence that is safe to log.

- `workflow_run.phase_started` (event): A phase began.

  - `workflow_run_id` (workflow run id): The workflow run, `wrun_` and 32 hex digits.

  - `workflow_run_phase_id` (workflow run phase id): The phase.

- `workflow_run.phase_ended` (event): A phase ended, or the run's end closed it.

  - `workflow_run_id` (workflow run id): The workflow run, `wrun_` and 32 hex digits.

  - `workflow_run_phase_id` (workflow run phase id): The phase.

  - `phase_started_id` (event id): The `workflow_run.phase_started` it closes.

```json
{
  "type": "worker.message",
  "id": "event_3c8e1f5a7b2d4c9e8a0b6d4f2e1c9a7b",
  "task_id": "task_01JA8Z6Q4M3T9V2K7X5R1B0C8D",
  "sequence": "10",
  "processed_at": "2026-10-25T16:04:30.771Z",
  "message_id": "message_a41d7e9c2b5f4083b6e1d9c7a5f3b2e0",
  "content": [
    {
      "type": "text",
      "text": "1. Open at 7 am, starting Monday\n2. Up early: open at 7 am from Monday\n3. Coffee is on at 7 am"
    }
  ],
  "sources": []
}
```

## List events

`GET /tasks/{task_id}/events`

List what happened in a task, oldest first. Each entry is an event, as the event object shows it. Keep only some types, or only some times, or start after an event you already have.

The time filters compare each event's `processed_at`, to the millisecond. A message still waiting (`processed_at` is `null`) matches no time filter. A `page` works only with the `order` and the filters that made it.

On a plan that keeps history for a set time, such as the free plan, older history is deleted. The chat then gets a `history.purged` event, whose `before` is the cutoff. A list from the start begins with the newest one, then the events that are kept.

### Path parameters

- `task_id` (task id, required): The task, such as `task_01JA8Z6Q4M3T9V2K7X5R1B0C8D`.

### Query parameters

- `types[]` (string): Only events of this type. Repeat it to name more than one. One of `user.message`, `system.message`, `user.define_outcome`, `user.approval`, `user.interrupt`, `user.tool_result`, `user.custom_tool_result`, `worker.message`, `worker.tool_use`, `worker.tool_result`, `worker.mcp_tool_use`, `worker.mcp_tool_result`, `worker.custom_tool_use`, `worker.thread_context_compacted`, `span.outcome_evaluation_start`, `span.outcome_evaluation_ongoing`, `span.outcome_evaluation_end`, `span.model_request_start`, `span.model_request_end`, `worker.thinking`, `task.error`, `task.status_running`, `task.usage`, `task.status_idle`, `task.status_rescheduled`, `task.status_terminated`, `task.updated`, `task.deleted`, `history.purged`, `task.thread_created`, `task.thread_status_running`, `task.thread_status_idle`, `task.thread_status_rescheduled`, `task.thread_status_terminated`, `worker.thread_message_sent`, `worker.thread_message_received`, `workflow_run.created`, `workflow_run.status_running`, `workflow_run.status_idle`, `workflow_run.status_ended`, `workflow_run.error`, `workflow_run.phase_started`, `workflow_run.phase_ended`.
- `after` (string): Only events after this sequence. A whole number as a string, such as `"42"`. `"0"` is the start.
- `limit` (integer): How many to answer, 1 to 100. The default is 20.
- `page` (string): The `next_page` of the answer before, to read the next page.
- `order` (string): `asc`, oldest first (the default), or `desc`, newest first. One of `asc`, `desc`.
- `created_at[gt]` (time): Only events written after this time. `created_at[gte]`, `created_at[lt]` and `created_at[lte]` work the same way.

### Returns

A list: `data` holds the events, and `next_page` is the `page` to send for the next ones (`null` on the last page). Unlike the task list, there is no `prev_page`.

### Example

```bash
curl "https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events?types%5B%5D=worker.message&order=desc&limit=1" \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19"
```

```json
{
  "data": [
    {
      "type": "worker.message",
      "id": "event_3c8e1f5a7b2d4c9e8a0b6d4f2e1c9a7b",
      "task_id": "task_01JA8Z6Q4M3T9V2K7X5R1B0C8D",
      "sequence": "10",
      "processed_at": "2026-10-25T16:04:30.771Z",
      "message_id": "message_a41d7e9c2b5f4083b6e1d9c7a5f3b2e0",
      "content": [
        {
          "type": "text",
          "text": "1. Open at 7 am, starting Monday\n2. Up early: open at 7 am from Monday\n3. Coffee is on at 7 am"
        }
      ],
      "sources": []
    }
  ],
  "next_page": null
}
```

### Errors

- 400 `invalid_request_error`: `task_id` is not a task id. `error_code` is `validation_error`.
- 400 `invalid_request_error`: A filter is not valid: a `limit` outside 1 to 100, a `types[]` value that is not an event type, or a time that is not RFC 3339. `error_code` is `validation_error`.
- 400 `invalid_request_error`: The cursor is refused. `error_code` is `invalid_cursor` when `page` or `after` cannot be read, or `page` was made with another `order` or filter; `cursor_ahead` when it is past the newest event; and `cursor_expired` when it is older than the history kept, or from before a `history.purged` that deleted events it had not read. For the last two, list the task's events from the start.
- 401 `authentication_error`: The key is missing, removed, or not a key. Send it in `x-api-key`.
- 404 `not_found_error`: No task in the key's workspace has this id, or it was taken back or deleted (`Task not found.`).
- 429 `rate_limit_error`: The workspace made too many requests this minute. Wait the seconds in `Retry-After`, then try again.

## Send events

`POST /tasks/{task_id}/events`

Send a task one event or more. A message tells the worker what you want, and the other events stop it, answer it, or judge its result.

Every event is checked before any applies, and they apply in order. If one fails, none applies. Any mix is taken, except that `user.accept` and `user.send_back` are each sent alone.

| Event | What it does |
| --- | --- |
| `user.message` | A message the worker reads after the work in hand. |
| `user.interrupt` | Stops the work in hand. |
| `user.interrupt`, then `user.message` | Stops the work in hand, then the message starts a new turn: `task.status_idle` with `end_turn`, then `task.status_running`. |
| `user.accept` | Accepts the result. |
| `user.send_back` | Sends the result back for more work. |
| `user.approval` | Allows or denies an action that waits for an answer. |
| `user.custom_tool_result` | Answers a custom tool the worker called. |
| `user.define_outcome` | Sets what the task's work is checked against. |
| `user.message`, then `system.message` | The worker reads the system message in its model's system role, from the turn the message starts. |

A message sent while the worker is busy waits for its next turn. The list and the stream place it where it was accepted. An interrupt ends the stopped turn at once, so after an interrupt and a message, the `task.status_idle` comes before the `user.message`.

A worker takes only a `user.message` as your intent. Text it reads in a page, a file, an email or a tool's answer is information, not an instruction.

### Path parameters

- `task_id` (task id, required): The task, such as `task_01JA8Z6Q4M3T9V2K7X5R1B0C8D`.

### Headers

- `Idempotency-Key` (UUID): Send one so a retry sends the events once. A second call with the same key answers what the first call did. The same key with other events is `409` with `error_code` `idempotency_conflict`. Kept for 7 days.

### Body

- `events` (array of events, required): 1 to 50 events, each one of these kinds. It is applied in the order you give them.
  - `user.message` (event): A message the worker reads after the work in hand. Sent to a task that is done, it reopens the task, as a send back does, with the message as the reason.
    - `type` (string, required): `user.message`.
    - `content` (array of content blocks, required): One block or more. A text block is `{ "type": "text", "text": "..." }`. An image is `{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "..." } }`. A document is the same with `"type": "document"` and a `source` of base64 `application/pdf`, or `{ "type": "text", "media_type": "text/plain", "data": "..." }`; it may also have a `title` and a `context`. At most six images and documents go on one message. A `url` or `file` source is refused for now.
  - `user.interrupt` (event): Stops the work in hand, which ends the turn: `task.status_idle` with `end_turn`. A `user.message` after it starts a new turn. A task held at its own budget has nothing to stop, and an interrupt does nothing.
    - `type` (string, required): `user.interrupt`.
  - `user.accept` (event): Accepts the result. Finished work is done by itself, so this matters only for work that needed a look. Accepting twice changes nothing. Send it alone.
    - `type` (string, required): `user.accept`.
  - `user.send_back` (event): Sends the result back for more work. The `reason` is the next message the worker reads. Send it alone.
    - `type` (string, required): `user.send_back`.
    - `reason` (string, required): What to change, up to 4,000 characters.
  - `user.approval` (event): Allows or denies an action that waits for an answer. It needs a key made with **Can allow actions**. It answers once, and a key cannot choose to remember an answer.
    - `type` (string, required): `user.approval`.
    - `tool_use_id` (event id, required): One of the `event_ids` of the task's `requires_action` stop: the tool use that asks.
    - `result` (string, required): The answer. One of `allow`, `deny`.
    - `deny_message` (string): Why, up to 10,000 characters. Only with `deny`; with `allow` the send is refused. The worker reads it as the reason the action failed.
  - `user.custom_tool_result` (event): Answers a `worker.custom_tool_use` the task waits on. The worker reads `content` as the tool's result.
    - `type` (string, required): `user.custom_tool_result`.
    - `custom_tool_use_id` (event id, required): One of the `event_ids` of the task's `requires_action` stop.
    - `content` (array of text blocks): The result. Text blocks only, for now.
    - `is_error` (boolean): `true` to give the worker a failure instead of a result.
  - `user.define_outcome` (event): Sets what the task's work is checked against. Each check round says whether the work met it.
    - `type` (string, required): `user.define_outcome`.
    - `description` (string): What done looks like. Left out, it is the task's ask.
    - `rubric` (object): The check, as `{ "type": "text", "content" }` or `{ "type": "file", "file_id" }`, up to 262,144 characters.
    - `done_means` (array of strings): Instead of a rubric: 1 to 8 checks of 1 to 200 characters, graded one by one.
    - `max_iterations` (integer): How many check rounds, 1 to 20. The default is 3.
  - `system.message` (event): A message the model reads in its system role, from the turn the message before it starts. It never steers the work in hand. Send one at most, as the last event, right after a `user.message`.
    - `type` (string, required): `system.message`.
    - `content` (array of text blocks, required): 1 to 1,000 text blocks of at least one character.

### Returns

`200` and `{ "data": [...] }`, one entry for each event you sent, in the order you sent them. A send is not a list, so there is no `next_page`. Each entry is one of these.

- `user.message` (event): The message as stored, the same as the list shows it.
- `system.message` (event): The system message as stored, right after its message.
- `user.interrupt` (event): What the interrupt stopped.
  - `type` (string): `user.interrupt`.
  - `run_id` (run id, or null): The run it stopped. `null` when no work was in hand to stop.
- `user.accept` (event): The accepted result.
  - `type` (string): `user.accept`.
  - `task` (object): The task, as it stands now.
- `user.send_back` (event): The result, sent back.
  - `type` (string): `user.send_back`.
  - `task` (object): The task, running again.
  - `run_id` (run id): The new run.
- `user.approval` (event): The answer, taken.
  - `type` (string): `user.approval`.
  - `tool_use_id` (event id): The tool use you answered.
  - `status` (string): What you decided. One of `allowed`, `denied`.
- `user.custom_tool_result` (event): The answer, taken.
  - `type` (string): `user.custom_tool_result`.
  - `custom_tool_use_id` (event id): The custom tool use you answered.
  - `is_error` (boolean): Whether you gave a failure.
- `user.define_outcome` (event): The outcome, as set.
  - `type` (string): `user.define_outcome`.
  - `outcome_id` (outcome id): The outcome, `outc_` and 32 hex digits.
  - `processed_at` (time): When it was set.
  - `description` (string): What done looks like.
  - `rubric` (object): The check.
  - `done_means` (array of strings, or null): The checks, when you sent them in place of a rubric.
  - `max_iterations` (integer): How many check rounds.

### Example

```bash
curl "https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events" \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19" \
  -H "Idempotency-Key: 2f6c9a1e-8b3d-4a57-9e0c-4d1b7f5a3c82" \
  -H "content-type: application/json" \
  -d '{
  "events": [
    {
      "type": "user.message",
      "content": [
        {
          "type": "text",
          "text": "The shop is called Field Day."
        }
      ]
    }
  ]
}'
```

```json
{
  "data": [
    {
      "type": "user.message",
      "id": "event_c9e7a5b3d1f24680a2c4e6f8b0d13579",
      "task_id": "task_01JA8Z6Q4M3T9V2K7X5R1B0C8D",
      "sequence": "12",
      "processed_at": null,
      "message_id": "message_5b3d1f9e7c2a4860b4d2f0e8c6a49371",
      "content": [
        {
          "type": "text",
          "text": "The shop is called Field Day."
        }
      ]
    }
  ]
}
```

### Errors

- 400 `invalid_request_error`: `task_id` is not a task id. `error_code` is `validation_error`.
- 400 `invalid_request_error`: An event is not valid: a missing field, an id of the wrong kind, an event the route does not take, more than 50 events, an `Idempotency-Key` that is not a UUID, or a `deny_message` with `allow`. `message` says which. `error_code` is `validation_error`.
- 400 `invalid_request_error`: The mix of events is not allowed: `user.accept` or `user.send_back` with another event, a `system.message` that is not the last event or does not follow a `user.message`, a `redacted` block, an image that is not an image type, a document that is not a PDF, data that is not base64, or more than six images and documents on a message.
- 400 `invalid_request_error`: The task reached its own budget (`error_code` `budget_reached`). It takes only approvals, tool results and interrupts until you raise its budget with [Change a task](/api/tasks#change-a-task).
- 400 `invalid_request_error`: A `system.message` for a task whose model has no system role (`error_code` `model_does_not_support_mid_conversation_system`).
- 401 `authentication_error`: The key is missing, removed, or not a key. Send it in `x-api-key`.
- 403 `permission_error`: A `user.approval` from a key made without **Can allow actions**, or for a step only the person may answer, such as a worker's first use of a saved password or an email it sends (`error_code` `person_required`). On a shared workspace, a key can answer only approvals of tasks its own person started (`not_task_starter`), and of a Mac its own person paired (`not_allowlisted`).
- 404 `not_found_error`: No task has this id (`Task not found.`), no approval of this task has that `tool_use_id` (`Approval not found.`), or no custom tool use of this task has that `custom_tool_use_id`.
- 409 `conflict_error`: The task is archived and takes no message: give a new task. Or `user.accept` came while the worker is still on the task: wait for the result, or interrupt, then accept. Or a done task that belongs to a project was sent back or sent a message: answer in the app.
- 409 `conflict_error`: The approval was taken back, already has the other answer, or the run was cancelled. Or `error_code` is `approval_consumed` (the action already ran) or `approval_expired` (a Mac action's time ran out). Or the custom tool use is not waiting for a result.
- 409 `conflict_error`: The `Idempotency-Key` was already used with other events (`error_code` `idempotency_conflict`). Use a new key for new events.
- 429 `rate_limit_error`: The workspace made too many requests this minute (`error_code` `rate_limited`), or the worker already has 20 waiting runs (`queue_limit`, and `retryable` is `true`). Wait the seconds in `Retry-After`, or send the message again once the worker has caught up.

## Stream events

`GET /tasks/{task_id}/events/stream`

Follow a task as it works. The answer stays open and sends each event as it is written, as Server-Sent Events. It ends when you close it, or after a `task.deleted` event.

With no `after` and no `Last-Event-ID`, the stream sends only the events written after it opens. `after=0` replays every event that is kept, from the first. After a drop, connect again with the `id` of the last frame you read as `Last-Event-ID` (or as `after`), and the stream sends only what came later. If you send both, the header is used.

Status changes (`task.updated`, `task.status_terminated` and `task.deleted`), model request spans, `worker.thinking` and outcome events are kept outside the chat. Each carries the `sequence` of the chat event before it, so one can be written at a `sequence` you already took as your cursor. A cursor you hold goes on after the chat event it names, and still includes these events at its own `sequence`. Nothing is missed, and one you already saw can come again, so skip event ids you have seen.

To see the worker's answer as it is written, add `event_deltas[]=worker.message` to the URL. Each preview is an `event_start` frame, then `event_delta` frames with the text so far. Previews have no `id`, go only to the connection that asked, only for text written after it opened, and are never listed or replayed. The finished `worker.message` holds the whole text and is the record. `event_deltas[]=worker.thinking` adds a start-only preview: an `event_start` with no deltas, right before the `worker.thinking` it names.

A workspace may hold 10 streams open at once. Past that, a new stream is `429`.

### Path parameters

- `task_id` (task id, required): The task, such as `task_01JA8Z6Q4M3T9V2K7X5R1B0C8D`.

### Query parameters

- `after` (string): Send only events after this sequence. A whole number as a string, such as `"42"`. `"0"` is the start.
- `event_deltas[]` (string): A kind of event to preview while it is written. Repeat it to name both. One of `worker.message`, `worker.thinking`.

### Headers

- `Last-Event-ID` (string): The `id` of the last frame you read, which is its event's `sequence`. The stream sends only what came after it.

### Returns

`200` and a stream, `text/event-stream`. Each frame is an `id` (the event's `sequence`), an `event` (its `type`) and `data`, the event as JSON. The response shown is the `data` of one frame.

```text
id: 10
event: worker.message
data: {"type":"worker.message","sequence":"10",...}

id: 11
event: task.status_idle
data: {"type":"task.status_idle","sequence":"11",...}
```

A preview frame has no `id`. Its `data` is `{ "type": "event_start", "event": { "type", "id" } }`, where `id` is the id the finished message will carry, or `{ "type": "event_delta", "event_id", "delta" }`, where `delta` is `{ "type": "content_delta", "index", "content": { "type": "text", "text" } }`. A line that starts with a colon is a comment that keeps the connection open, so your reader skips it.

### Example

```bash
curl -N "https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events/stream?after=0" \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19"
```

```json
{
  "type": "task.status_idle",
  "id": "event_8f2a6c4e1b9d4375a0c8e6d4b2f19753",
  "task_id": "task_01JA8Z6Q4M3T9V2K7X5R1B0C8D",
  "sequence": "11",
  "processed_at": "2026-10-25T16:04:31.209Z",
  "run_id": "run_6d2e8f1a3c5b4d7e9f0a1b2c3d4e5f6a",
  "stop_reason": {
    "type": "end_turn"
  },
  "stop_details": null
}
```

### Errors

- 400 `invalid_request_error`: `task_id` is not a task id. `error_code` is `validation_error`.
- 400 `invalid_request_error`: `event_deltas[]` names something other than `worker.message` or `worker.thinking`, or has more than 100 values (`error_code` `validation_error`).
- 400 `invalid_request_error`: The cursor is refused. `error_code` is `invalid_cursor` when `page` or `after` cannot be read, or `page` was made with another `order` or filter; `cursor_ahead` when it is past the newest event; and `cursor_expired` when it is older than the history kept, or from before a `history.purged` that deleted events it had not read. For the last two, list the task's events from the start.
- 401 `authentication_error`: The key is missing, removed, or not a key. Send it in `x-api-key`.
- 404 `not_found_error`: No task in the key's workspace has this id, or it was taken back or deleted (`Task not found.`).
- 429 `rate_limit_error`: The workspace made too many requests this minute, or already has 10 streams open (`error_code` `rate_limited`). Wait the seconds in `Retry-After`, or close a stream you no longer read.
