Skip to content
Get API Key

Concepts

Runs and events

A task works in runs and writes an event for everything that happens, which you can list or follow live.

Updated
View as Markdown

A task is not one long job. It works in stretches called runs, and it writes an event for each thing that happens along the way: a message, a tool used, a stop. Events are how you follow a task, and how you tell what a worker did and why it stopped.

This page explains runs, the events a task writes, the order they come in, and how to read or follow them without missing one. The routes themselves are listed under /tasks/{task_id}/events.

Runs

A run is one stretch of work inside a task. A task has a new run each time it starts again, such as after you send a message to an idle task or send a result back. A run has an id that starts with run_. You see it on these events:

  • task.status_running and task.status_idle, which open and close a stretch of work.
  • task.status_rescheduled, when a run is held and goes on by itself.
  • user.interrupt, which names the run it stopped.

A send back also answers with the run_id of the new run. Each finished run has a receipt that says what it cost and which app actions were decided in it. Read them on the Tasks reference.

A run is limited to 200 model turns and 2 hours of active model time. A run that reaches either one stops cleanly with the stop reason turn_limit or time_limit, and its work is kept. See Tasks.

Events

An event is one thing that happened in a task. Every event has the same first fields:

Field What it is
type What kind of event it is, such as worker.message.
id The event's id, which starts with event_. The id of a worker.tool_use is also the tool use id that its result, its approval and a stop name it by.
task_id The task it belongs to.
sequence A whole number written as a string. It orders the list, the stream and every cursor, because two events can share a time. Compare it as a number, not as text.
processed_at When Little Worker wrote the event. A message still waiting for the worker has null here until a run takes it.

sequence counts the events of the whole chat, not of one task. The numbers of a single task's events skip, so use the last sequence you saw as your place, and do not expect them to count up by one.

The names follow Claude Managed Agents with Little Worker's nouns: worker.* is agent.*, and task.status_* is session.status_*.

What people and programs send

type What happened
user.message A message about the task, as content blocks: text, image or document. It has a message_id.
system.message A system message sent right after a person's message. It is listed right after that message, with the same sequence.
user.define_outcome An outcome was set for the task: outcome_id, description, rubric, done_means and max_iterations.
user.approval An approval was answered: tool_use_id, result, deny_message, and api_key_id, the key that answered. It is null when the person answered in the app.
user.interrupt The work was stopped, by your user.interrupt or by the person's Stop. run_id names the run it stopped.
user.custom_tool_result Your answer to a custom tool use: custom_tool_use_id, content and is_error.
user.tool_result A tool result a Mac ran and sent back, with is_error. A Mac is a self-hosted computer, so its results are listed as Claude lists a self-hosted runner's.

What the worker says and does

type What happened
worker.message The worker's answer, as content text blocks, with its sources.
worker.thinking The model reasoned during a request. It is a progress signal with no content.
worker.tool_use The worker used a tool: name and input, with your passwords masked. evaluated_permission (allow, ask or deny) and evaluation say what decided the step, and are missing until something has.
worker.tool_result The tool's status, which is succeeded, failed, cancelled or outcome_unknown, and its output or error. tool_use_id names the worker.tool_use.
worker.mcp_tool_use The worker called one of your apps: app_name, the app's own tool name, and input. It has the same evaluated_permission and evaluation.
worker.mcp_tool_result The app's answer. mcp_tool_use_id names the call, and is_error says whether it failed.
worker.custom_tool_use The worker called one of its custom tools: name and input. Little Worker never runs it, and no permission policy applies. The task stops with requires_action until a user.custom_tool_result answers.
worker.thread_context_compacted The worker's context grew too long and was squeezed into a summary. One event for each squeeze.

Where the task stands

type What happened
task.status_running The task began running: its work was accepted, or it went on after it stopped. There is one event for each change of status, so a start that follows a queue adds none. It has a run_id.
task.status_idle The worker stopped. run_id names the run. stop_reason says why, and stop_details is null except after a refusal.
task.status_rescheduled A run hit a passing fault, and Little Worker runs it again by itself.
task.status_terminated The task was taken back. It takes nothing more.
task.error Something failed. error has a type, a message, a retry_status and a code. A billing_error that is retrying also names the cap that holds the task in cap.
task.updated The task was changed with POST /tasks/{task_id}. It shows what the change set: title, metadata and budget.
task.deleted The task was deleted. An open stream reads it and ends.
history.purged History older than before was deleted, on a plan that keeps history for a set time.

A task.error has one of these error.type values:

  • model_overloaded_error, model_rate_limited_error, model_request_failed_error, billing_error and unknown_error.
  • app_connection_failed_error and app_authentication_failed_error, which add app_name. An app that refused its sign-in or could not be reached leaves the task working.
  • password_host_unreachable_error, which adds password_id and password_set_id. It comes once for each password in the task's sets whose website the worker's sites block.
  • repository_authentication_error, repository_forbidden_error, repository_not_found_error, repository_checkout_error and repository_clone_error, which add repository_url. They come when a repository of the task could not be used. The task goes on without it.

retry_status.type says what happens next: retrying means Little Worker goes on by itself, exhausted means the work stopped and the task takes another message, and terminal means the task cannot go on.

Cost and checks

type What happened
task.usage What the task's work used, up to the stop that follows. One comes right before every task.status_idle. usage has the fields of the task's usage, and budget is the task's budget.
span.model_request_start The worker sent one request to the model.
span.model_request_end The request ended. model_request_start_id names its start, is_error is true when it failed, broke off or was stopped, and model_usage says what it used.
span.outcome_evaluation_start A check round of an outcome began: outcome_id and iteration, counted from 0.
span.outcome_evaluation_ongoing Listed so a program built for Claude's heartbeat can read this one. Little Worker sends none today.
span.outcome_evaluation_end The round ended, with a result, an explanation and the usage of the check.

Helper threads and workflow runs

A worker can hand work to a helper. That work is a thread of the task, and its steps are also written on the task's own events, each naming the thread in task_thread_id.

type What happened
task.thread_created A thread started. It has worker_name, and a workflow_run_id when a workflow run started it.
worker.thread_message_sent The instructions the worker sent to a thread: to_task_thread_id and content.
task.thread_status_running The thread is at work.
task.thread_status_rescheduled A cap holds the thread, and it goes on when there is room.
task.thread_status_idle The thread stopped, with a stop_reason and stop_details as a task has.
worker.thread_message_received The answer the thread sent back: from_task_thread_id and content.
task.thread_status_terminated The thread ended.

A step of a thread that waits for an Allow is listed on the task too, as a worker.tool_use or worker.mcp_tool_use with a task_thread_id. Answer it on the task with a user.approval, like any other. GET /tasks/{task_id}/threads lists the task's threads, and each thread has its own events and its own stream.

A worker that is allowed to may also start a workflow run: named phases that run in order, each a set of helpers that work side by side. Its steps are on the task's events, and no webhook hears them.

type What happened
workflow_run.created A run started: workflow_run_id, name, description and its phases.
workflow_run.status_running The run is at work, or at work again after a pause.
workflow_run.status_idle The run paused: a helper is held at the task's budget, or an interrupt paused it.
workflow_run.phase_started A phase began: workflow_run_phase_id.
workflow_run.phase_ended A phase ended, or the end of the run closed it. phase_started_id names the start it closes.
workflow_run.error The run failed, right before its end. It also lists a start that made no run, with a null workflow_run_id.
workflow_run.status_ended The run ended, always as its last step. result is completed, stopped, or error with the error.

The order events come in

Events are written in sequence order, and a list or a stream gives them that way. A few facts help when you read them:

  • A model request is bracketed. span.model_request_start is listed before what the answer wrote, and span.model_request_end after it and before the stop that follows.
  • task.usage comes right before every task.status_idle.
  • A task.error whose retry_status is exhausted comes right before the task.status_idle it caused, and that stop is retries_exhausted.
  • A message sent while the worker is busy waits for its next turn. The list places it where it was accepted, with processed_at null until a run takes it.
  • After an interrupt and a message, the task.status_idle with end_turn comes before the user.message, then task.status_running follows.
  • The status changes task.updated, task.status_terminated and task.deleted, the model request spans, worker.thinking and the outcome events are kept outside the chat. Each carries the sequence of the chat event before it. One of them can be written at a sequence after you took that number as your place.

The last point matters for a program that keeps a place. A place you hold goes on after the chat event it names, but still includes these events at its own sequence. Nothing is missed, and one you already saw can come again. Keep the id of each event you handled, and skip an id you have seen.

Read events

GET /tasks/{task_id}/events lists a task's events, oldest first. Add order=desc for newest first. It takes these filters:

  • types[] keeps only the types you name. Repeat it for more than one.
  • created_at[gt], created_at[gte], created_at[lt] and created_at[lte] compare each event's processed_at. A message still waiting has none, so it matches no time filter.
  • after lists only events that come after a sequence.
  • limit is 20 by default and at most 100.

The answer is { "data": [...], "next_page": "..." }. next_page is a sequence: send it back as page, with the same order and filters, until it is null. A page works only with the order and filters that made it.

Shell
curl -g "https://core.thelittleworker.com/api/v1/tasks/task_01M5TTRJKWYAK3XHFX432K5MBV/events?types[]=worker.message&order=desc&limit=1" \
  -H "x-api-key: $LITTLE_WORKER_API_KEY" \
  -H "Little-Worker-Version: 2026-10-19"

A bad place is 400 invalid_request_error, and error.details.error_code says which:

error_code When it happens
invalid_cursor The page cannot be read, or it was made with another order or created_at filter.
cursor_ahead The place is beyond the history that is committed.
cursor_expired The place is older than the history kept, or comes from before the task's last history.purged. List the task's events again from the start. after=0 never expires.

Follow events

GET /tasks/{task_id}/events/stream follows a task as it works, as Server-Sent Events. The answer stays open. Each frame is one event:

Text
id: 11
event: task.status_idle
data: {"type":"task.status_idle","sequence":"11","run_id":"run_488241351b7a4b528b8bf6df21c7b7a8","stop_reason":{"type":"end_turn"},"stop_details":null,...}

The id line is the event's sequence, event is its type, and data is the event as JSON. Lines that start with : are comments, such as : connected and a heartbeat. Ignore them. The stream stays open after a stop, so close it yourself when you have what you need. It ends by itself when the task is deleted.

With no cursor, the stream sends only events written after it opens. after=0 replays every kept event from the first. To resume after a drop, send the last id you saw in a Last-Event-ID header, or as after.

A workspace may hold 10 open streams. Another is 429 with Retry-After.

Catch up without a gap

Two ways work, and either one loses nothing:

  • List, then follow. List the history, then open the stream with after set to the last sequence you listed.
  • Follow, then list. Open the stream, list the history, and skip the ids you have already seen.

In both, skip ids you have seen, for the reason in The order events come in.

Show an answer as it is written

Add event_deltas[]=worker.message to the stream URL, and event_deltas[]=worker.thinking if you want it. Any other value is 400, and so is more than 100. A preview of a message is an event_start frame, with the id the finished message will carry, then event_delta frames. Each delta has an event_id and a delta of type content_delta, with an index and a text block in content.

A preview has no id line. It goes only to the connection that asked, only for text written after that connection opened, and it is never listed or replayed. The finished worker.message holds the whole text and is the record. A thinking preview is only a start: an event_start right before the worker.thinking it names.

Send events

POST /tasks/{task_id}/events sends 1 to 50 events in { "events": [...] }. Every event is checked before any applies, they apply in order, and if one fails, none applies. The answer is { "data": [...] }, with one entry for each event in the order sent.

Event What it does
user.message Sends a message the worker reads after the work in hand. It carries up to 6 images and documents, as base64 or plain text. A url or file source is 400 for now.
user.interrupt Stops the work in hand.
user.approval Allows or denies a waiting approval. See Approvals.
user.custom_tool_result Answers a custom tool use the task waits on.
user.define_outcome Sets the task's next outcome.
user.accept Accepts the result. Send it alone.
user.send_back Sends the result back with a reason. Send it alone.
system.message Gives the model a system message. Send it right after a user.message, one for each send, with 1 to 1,000 text blocks. A task whose model has no system role answers 400 with error_code model_does_not_support_mid_conversation_system.

A worker treats 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. Send an Idempotency-Key header, a UUID, and a retry does the same thing once.

History that goes

On a plan that keeps history for a set time, Little Worker deletes older history and writes a history.purged event with a before time. A list with no cursor, oldest first, starts with the newest history.purged, then the events that were kept. No webhook is sent for a purge. The free plan keeps 7 days, and paid plans keep history until you delete it. Keep your own copy of events you need for longer. See Tasks.

Next steps

  • Approvals: the steps a task stops on, and how an answer reaches the worker.
  • Tasks: what each stop reason means and what to do about it.
  • Quickstart: follow a real task from your own code.

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