API reference
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 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.
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.*andsystem.messageare 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 atypeyou do not know instead of failing on it.
Fields
typestringWhat kind of event it is.idevent idThe event's id:event_and 32 hex digits. Aworker.tool_useevent's id is also the tool use id that its answers name.task_idtask idThe task it belongs to.sequencestringIts 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_attime or nullWhen Little Worker wrote the event. Auser.messagesent while the worker is busy hasnulluntil a run takes it, and stays listed where it was accepted.user.messageeventA message sent to the task, by a person in the app or by you with a key.Show its 2 fields
message_idmessage idThe message's id:message_and 32 hex digits.contentarray of content blocksWhat was sent, as it was sent:text,imageanddocumentblocks. Aredactedblock is content that is withheld.
system.messageeventA system message sent right after a person's message. It is listed right after that message, with the samesequence.Show its 1 fields
contentarray of text blocksThe text the model reads in its system role.
user.define_outcomeeventAn outcome set for the task: what its work is checked against. Listed where it was sent.Show its 5 fields
outcome_idoutcome idThe outcome,outc_and 32 hex digits.descriptionstringWhat done looks like.rubricobjectThe check, as{ "type": "text", "content" }or{ "type": "file", "file_id" }.done_meansarray of strings or nullA short list of checks graded one by one, sent in place of a rubric.nullwhen the outcome has a rubric.max_iterationsintegerHow many check rounds before the outcome gives up.
user.approvaleventAn approval was answered, by a key or by the person in the app.Show its 4 fields
tool_use_idevent idThe tool use that asked.resultstringThe answer.One of
allow,denydeny_messagestring or nullWhy it was denied, as the worker reads it.api_key_idAPI key id or nullThe key that answered.nullwhen the person answered in the app.
user.interrupteventThe work was stopped, by youruser.interruptor by the person's Stop.Show its 1 fields
run_idrun idThe run it stopped.
user.tool_resulteventA 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.Show its 5 fields
tool_use_idevent idThe tool use it answers.statusstringHow the tool ended.outcome_unknownis a started call that was stopped, when nobody can prove what it did.One of
succeeded,failed,cancelled,outcome_unknownoutputJSONWhat the tool answered. Left out when it answered nothing.errorobjectWhy the tool failed. Left out when it did not.Show its 2 fields
codestringThe reason, such asapproval_expired.messagestringWhat went wrong.
is_errorbooleantruewhen the tool failed.
user.custom_tool_resulteventYour answer to a custom tool use.Show its 3 fields
custom_tool_use_idevent idTheworker.custom_tool_useit answers.contentarray of content blocksThe answer:text,image,documentandsearch_resultblocks.is_errorbooleantruewhen it is a failure the worker should read as one.
worker.messageeventThe worker's answer. It holds the whole text, and it is the record of what the worker said.Show its 3 fields
message_idmessage idThe message's id.contentarray of content blocksThe answer, astextblocks. Aredactedblock is content that is withheld.sourcesarray of objectsThe web pages the answer cites, each once, in the order it cites them: aurland atitle. Empty when it cites none.
worker.tool_useeventThe worker used a tool of its own. Any password in theinputis masked.Show its 6 fields
idevent idThe tool use id. The tool's result, an approval and arequires_actionstop all name the step by it.namestringThe tool's name.inputJSONWhat the worker sent it.evaluated_permissionstringWhat decided the step:allow,askordeny. Left out while nothing has decided it yet.One of
allow,ask,denyevaluationobjectWhy. Left out while nothing has decided the step, and on a step refused before any setting applied. Treat atypeor areason_codeyou do not know as a plainask.Show its 2 fields
typestringalways_allowandalways_askare the tool's own setting.autois the automatic check.fixed_ruleis an action that always asks, whatever the setting says.One of
always_allow,always_ask,auto,fixed_ruleevaluated_permissionobjectOnautoandfixed_ruleonly: the decision. Itstypeisallow,askordeny. Areason_codegoes withaskanddeny: onauto,high_riskfor a block andindeterminatewhen the check was unsure, failed or ran out of time; onfixed_rule, one ofsend,pay,post,delete,change_settingsorfirst_password_use.
task_thread_idthread idSet when the step belongs to a helper's thread and the task's events show it too.
worker.tool_resulteventA tool's answer to the worker.Show its 4 fields
tool_use_idevent idTheworker.tool_useit answers.statusstringHow the tool ended.outcome_unknownis a started call that was stopped, when nobody can prove what it did.One of
succeeded,failed,cancelled,outcome_unknownoutputJSONWhat the tool answered. Left out when it answered nothing.errorobjectWhy the tool failed. Left out when it did not.Show its 2 fields
codestringThe reason, such asapproval_expired.messagestringWhat went wrong.
worker.mcp_tool_useeventThe worker called a tool of one of your apps.Show its 7 fields
idevent idThe tool use id, as on aworker.tool_use.app_namestringThe app's name.namestringThe app's own name for the tool.inputJSONWhat the worker sent it.evaluated_permissionstringWhat decided the step:allow,askordeny. Left out while nothing has decided it yet.One of
allow,ask,denyevaluationobjectWhy. Left out while nothing has decided the step, and on a step refused before any setting applied. Treat atypeor areason_codeyou do not know as a plainask.Show its 2 fields
typestringalways_allowandalways_askare the tool's own setting.autois the automatic check.fixed_ruleis an action that always asks, whatever the setting says.One of
always_allow,always_ask,auto,fixed_ruleevaluated_permissionobjectOnautoandfixed_ruleonly: the decision. Itstypeisallow,askordeny. Areason_codegoes withaskanddeny: onauto,high_riskfor a block andindeterminatewhen the check was unsure, failed or ran out of time; onfixed_rule, one ofsend,pay,post,delete,change_settingsorfirst_password_use.
task_thread_idthread idSet when the step belongs to a helper's thread and the task's events show it too.
worker.mcp_tool_resulteventThe app's answer to the worker.Show its 5 fields
mcp_tool_use_idevent idTheworker.mcp_tool_useit answers.statusstringHow the tool ended.outcome_unknownis a started call that was stopped, when nobody can prove what it did.One of
succeeded,failed,cancelled,outcome_unknownoutputJSONWhat the tool answered. Left out when it answered nothing.errorobjectWhy the tool failed. Left out when it did not.Show its 2 fields
codestringThe reason, such asapproval_expired.messagestringWhat went wrong.
is_errorbooleantruewhen the call failed.
worker.custom_tool_useeventThe worker called one of its custom tools. Little Worker never runs a custom tool: the task waits withrequires_actionuntil auser.custom_tool_resultanswers. A permission check never applies to it, so it has noevaluated_permission.Show its 4 fields
idevent idThe id your answer names incustom_tool_use_id.namestringThe tool's name.inputJSONWhat the worker sent it.task_thread_idthread idSet when the call belongs to a helper's thread and the task's events show it too.
worker.thread_context_compactedeventThe worker's context grew too long and was squeezed into a summary. One event for each squeeze.span.outcome_evaluation_starteventA check round of an outcome began.Show its 2 fields
outcome_idoutcome idThe outcome,outc_and 32 hex digits.iterationintegerThe round, counted from0.
span.outcome_evaluation_ongoingeventA check round is still going. None is sent today; the type is listed so a client written for Claude's heartbeat reads this one.Show its 2 fields
outcome_idoutcome idThe outcome,outc_and 32 hex digits.iterationintegerThe round.
span.outcome_evaluation_endeventA check round ended.Show its 6 fields
outcome_idoutcome idThe outcome,outc_and 32 hex digits.outcome_evaluation_start_idevent idThespan.outcome_evaluation_startit closes.iterationintegerThe round.resultstringHow the round came out.One of
satisfied,needs_revision,max_iterations_reached,failed,interruptedexplanationstringThe grader's reasons, in plain words.usageobjectWhat the grader used. A check is one model call with no prompt cache, so the cache counts are0.Show its 4 fields
input_tokensintegerTokens read.output_tokensintegerTokens written.cache_creation_input_tokensintegerAlways0.cache_read_input_tokensintegerAlways0.
span.model_request_starteventThe worker sent one request to the model. Listed before what the answer wrote.span.model_request_endeventThe request ended. Listed after what its answer wrote, and before the stop that follows.Show its 3 fields
model_request_start_idevent idThespan.model_request_startit closes.is_errorboolean or nulltruewhen the request failed, broke off or was stopped. Nothing is counted then.model_usageobjectWhat the request used.Show its 6 fields
input_tokensintegerTokens read, without the prompt cache's tokens.output_tokensintegerTokens written.cache_creation_input_tokensintegerTokens written to the prompt cache.cache_read_input_tokensintegerTokens read from the prompt cache.speedstringThe speed it ran at.One of
standard,fastinference_geostringThe region that served it.One of
us,global
worker.thinkingeventThe model reasoned in that request. A progress signal with no content, listed right after the request's start.task.erroreventSomething failed. An exhausted error comes right before thetask.status_idleit caused.Show its 2 fields
errorobjectWhat failed.Show its 8 fields
typestringThe 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_errormessagestringWhat went wrong.retry_statusobjectWhether Little Worker tries again, in itstype:retrying(it goes on by itself),exhausted(the work stopped, and the task takes another message) orterminal(the task cannot go on).codestringLittle Worker's own code for the reason, such aspassword_host_blockedorrepository_unavailable.app_namestringOn an app error: the app.password_idpassword idOnpassword_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_idpassword set idOnpassword_host_unreachable_error: its set.repository_urlstring or nullOn a repository error: the repository the task could not use. The task goes on without it.
capstring or nullOn abilling_errorthat isretrying: the plan, trial, balance or Limit cap that holds the task. Left out otherwise.
task.status_runningeventThe 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.Show its 1 fields
run_idrun idThe run, one stretch of the task's work.
task.usageeventWhat the task's work used, up to the stop that follows. One comes right before everytask.status_idle.Show its 2 fields
usageobjectThe fields of the task'susage, 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.budgetobjectThe task's budget.
task.status_idleeventThe worker stopped. Readstop_reasonto know why.Show its 3 fields
run_idrun idThe run, one stretch of the task's work.stop_reasonobjectWhy the worker stopped.Show its 5 fields
typestringend_turn: its turn is over, also after an interrupt.requires_action: it waits for you or a person.retries_exhausted: the work failed, and atask.errorcame just before.budget_reached: the task's own budget holds it.refusal: the model declined the turn.turn_limitandtime_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_limitevent_idsarray of event idsWithrequires_action: theworker.tool_use,worker.mcp_tool_useandworker.custom_tool_useevents it waits on. Answer a tool use with auser.approval, and a custom tool use with auser.custom_tool_result.blockingarray of objectsWithrequires_action: every step it waits on, in the order they were asked, cards in the app included.Show its 3 fields
typestringThe kind of step.One of
approval,auto_review,virtual_card,cookie,email_draft,slack_draft,question,form,password_request,handoff,hosted_decision,custom_tool_useevent_idevent id or nullTheworker.tool_useevent of anapproval, or theworker.custom_tool_useevent of acustom_tool_use.nullfor every other kind, which is a card in the app.person_onlybooleantruewhen only the person can answer it, in the app, and a key never can.
limitintegerWithturn_limit: the most model turns a run takes.limit_msintegerWithtime_limit: the most active model time a run takes, in milliseconds.
stop_detailsobject or nullWhy the model declined the turn, after arefusal.nullfor every other stop.Show its 3 fields
typestringAlwaysrefusal.categorystring or nullThe policy that declined it, such ascyber,bio,frontier_llm,reasoning_extractionorgeneral_harms. More may come.explanationstring or nullA sentence for people. Its wording can change, so do not parse it.
task.status_rescheduledeventA run hit a passing fault, and Little Worker runs it again by itself.Show its 1 fields
run_idrun idThe run, one stretch of the task's work.
task.status_terminatedeventThe task was taken back withPOST /tasks/{task_id}/withdraw. It takes nothing more.task.updatedeventThe task was changed withPOST /tasks/{task_id}. It shows what the change set.Show its 3 fields
titlestring or nullThe new title. Left out when the change did not set it.metadataobjectAll of the task's labels, while any is left. Left out when the change did not set them.budgetobjectThe new budget. Left out when the change did not set it.
task.deletedeventThe task was deleted. An open stream reads it and ends. The task's other status events go with its history.history.purgedeventOn 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.Show its 1 fields
beforetimeThe cutoff: history older than this is gone.
task.thread_createdeventA helper's thread began. The worker handed it work.Show its 3 fields
task_thread_idthread idThe thread of the helper this step is about.worker_namestringThe helper's name.workflow_run_idworkflow run id or nullThe workflow run the thread belongs to, if any.
task.thread_status_runningeventThe thread is at work.Show its 2 fields
task_thread_idthread idThe thread of the helper this step is about.worker_namestringThe helper's name.
task.thread_status_idleeventThe thread stopped.Show its 4 fields
task_thread_idthread idThe thread of the helper this step is about.worker_namestringThe helper's name.stop_reasonobjectWhy, in the same form as ontask.status_idle. A thread can also stop withrequires_action, which waits on theevent_idsit lists.stop_detailsobject or nullAs ontask.status_idle.
task.thread_status_rescheduledeventA cap holds the thread. It goes on when there is room.Show its 2 fields
task_thread_idthread idThe thread of the helper this step is about.worker_namestringThe helper's name.
task.thread_status_terminatedeventThe thread ended.Show its 2 fields
task_thread_idthread idThe thread of the helper this step is about.worker_namestringThe helper's name.
worker.thread_message_senteventThe instructions the worker sent a helper.Show its 3 fields
to_task_thread_idthread idThe helper's thread.to_worker_namestring or nullThe helper's name.contentarray of content blocksThe instructions, astextblocks.
worker.thread_message_receivedeventThe answer a helper sent back.Show its 3 fields
from_task_thread_idthread idThe helper's thread.from_worker_namestring or nullThe helper's name.contentarray of content blocksThe answer, astextblocks.
workflow_run.createdeventA 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.Show its 4 fields
workflow_run_idworkflow run idThe workflow run,wrun_and 32 hex digits.namestringThe run's name.descriptionstring or nullWhat the run is for.phasesarray of objectsIts phases, in order: each has anid(wrph_and 32 hex digits), aname, and adescriptionthat may benull.
workflow_run.status_runningeventThe run is at work, again after a pause.Show its 1 fields
workflow_run_idworkflow run idThe workflow run,wrun_and 32 hex digits.
workflow_run.status_idleeventThe run paused: a helper is held at the task's budget, or an interrupt paused it.Show its 1 fields
workflow_run_idworkflow run idThe workflow run,wrun_and 32 hex digits.
workflow_run.status_endedeventThe run ended. It is always the run's last event.Show its 2 fields
workflow_run_idworkflow run idThe workflow run,wrun_and 32 hex digits.resultobjectHow it ended, in itstype:completed,stopped, orerrorwith theerror.
workflow_run.erroreventThe run's error, right before its end, or a start that made no run.Show its 2 fields
workflow_run_idworkflow run id or nullnullwhen the error kept a run from being made.errorobjectWhat went wrong. It never holds what the run wrote.Show its 2 fields
typestringThe kind of error.One of
timeout_error,program_error,unknown_error,thread_limit_error,max_workflow_runs_errormessagestringA sentence that is safe to log.
workflow_run.phase_startedeventA phase began.Show its 2 fields
workflow_run_idworkflow run idThe workflow run,wrun_and 32 hex digits.workflow_run_phase_idworkflow run phase idThe phase.
workflow_run.phase_endedeventA phase ended, or the run's end closed it.Show its 3 fields
workflow_run_idworkflow run idThe workflow run,wrun_and 32 hex digits.workflow_run_phase_idworkflow run phase idThe phase.phase_started_idevent idTheworkflow_run.phase_startedit closes.
{
"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_idtask idRequiredThe task, such astask_01JA8Z6Q4M3T9V2K7X5R1B0C8D.
Query parameters
types[]stringOnly 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_endedafterstringOnly events after this sequence. A whole number as a string, such as"42"."0"is the start.limitintegerHow many to answer, 1 to 100. The default is 20.pagestringThenext_pageof the answer before, to read the next page.orderstringasc, oldest first (the default), ordesc, newest first.One of
asc,desccreated_at[gt]timeOnly events written after this time.created_at[gte],created_at[lt]andcreated_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.
Errors
| Status | Type | When |
|---|---|---|
| 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. |
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"const response = await fetch("https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events?types%5B%5D=worker.message&order=desc&limit=1", {
headers: {
"x-api-key": process.env.LITTLE_WORKER_API_KEY,
"Little-Worker-Version": "2026-10-19",
},
})
const events = await response.json()import os
import requests
response = requests.get(
"https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events",
headers={
"x-api-key": os.environ["LITTLE_WORKER_API_KEY"],
"Little-Worker-Version": "2026-10-19",
},
params={
"types[]": "worker.message",
"order": "desc",
"limit": "1",
},
)
events = response.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
}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_idtask idRequiredThe task, such astask_01JA8Z6Q4M3T9V2K7X5R1B0C8D.
Headers
Idempotency-KeyUUIDSend 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 is409witherror_codeidempotency_conflict. Kept for 7 days.
Body
eventsarray of eventsRequired1 to 50 events, each one of these kinds. It is applied in the order you give them.Show its 8 fields
user.messageeventA 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.Show its 2 fields
typestringRequireduser.message.contentarray of content blocksRequiredOne 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 asourceof base64application/pdf, or{ "type": "text", "media_type": "text/plain", "data": "..." }; it may also have atitleand acontext. At most six images and documents go on one message. Aurlorfilesource is refused for now.
user.interrupteventStops the work in hand, which ends the turn:task.status_idlewithend_turn. Auser.messageafter it starts a new turn. A task held at its own budget has nothing to stop, and an interrupt does nothing.Show its 1 fields
typestringRequireduser.interrupt.
user.accepteventAccepts 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.Show its 1 fields
typestringRequireduser.accept.
user.send_backeventSends the result back for more work. Thereasonis the next message the worker reads. Send it alone.Show its 2 fields
typestringRequireduser.send_back.reasonstringRequiredWhat to change, up to 4,000 characters.
user.approvaleventAllows 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.Show its 4 fields
typestringRequireduser.approval.tool_use_idevent idRequiredOne of theevent_idsof the task'srequires_actionstop: the tool use that asks.resultstringRequiredThe answer.One of
allow,denydeny_messagestringWhy, up to 10,000 characters. Only withdeny; withallowthe send is refused. The worker reads it as the reason the action failed.
user.custom_tool_resulteventAnswers aworker.custom_tool_usethe task waits on. The worker readscontentas the tool's result.Show its 4 fields
typestringRequireduser.custom_tool_result.custom_tool_use_idevent idRequiredOne of theevent_idsof the task'srequires_actionstop.contentarray of text blocksThe result. Text blocks only, for now.is_errorbooleantrueto give the worker a failure instead of a result.
user.define_outcomeeventSets what the task's work is checked against. Each check round says whether the work met it.Show its 5 fields
typestringRequireduser.define_outcome.descriptionstringWhat done looks like. Left out, it is the task's ask.rubricobjectThe check, as{ "type": "text", "content" }or{ "type": "file", "file_id" }, up to 262,144 characters.done_meansarray of stringsInstead of a rubric: 1 to 8 checks of 1 to 200 characters, graded one by one.max_iterationsintegerHow many check rounds, 1 to 20. The default is 3.
system.messageeventA 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 auser.message.Show its 2 fields
typestringRequiredsystem.message.contentarray of text blocksRequired1 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.messageeventThe message as stored, the same as the list shows it.system.messageeventThe system message as stored, right after its message.user.interrupteventWhat the interrupt stopped.Show its 2 fields
typestringuser.interrupt.run_idrun id or nullThe run it stopped.nullwhen no work was in hand to stop.
user.accepteventThe accepted result.Show its 2 fields
typestringuser.accept.taskobjectThe task, as it stands now.
user.send_backeventThe result, sent back.Show its 3 fields
typestringuser.send_back.taskobjectThe task, running again.run_idrun idThe new run.
user.approvaleventThe answer, taken.Show its 3 fields
typestringuser.approval.tool_use_idevent idThe tool use you answered.statusstringWhat you decided.One of
allowed,denied
user.custom_tool_resulteventThe answer, taken.Show its 3 fields
typestringuser.custom_tool_result.custom_tool_use_idevent idThe custom tool use you answered.is_errorbooleanWhether you gave a failure.
user.define_outcomeeventThe outcome, as set.Show its 7 fields
typestringuser.define_outcome.outcome_idoutcome idThe outcome,outc_and 32 hex digits.processed_attimeWhen it was set.descriptionstringWhat done looks like.rubricobjectThe check.done_meansarray of strings or nullThe checks, when you sent them in place of a rubric.max_iterationsintegerHow many check rounds.
Errors
| Status | Type | When |
|---|---|---|
| 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. |
| 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. |
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."
}
]
}
]
}'const response = await fetch("https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events", {
method: "POST",
headers: {
"x-api-key": process.env.LITTLE_WORKER_API_KEY,
"Little-Worker-Version": "2026-10-19",
"Idempotency-Key": "2f6c9a1e-8b3d-4a57-9e0c-4d1b7f5a3c82",
"content-type": "application/json",
},
body: JSON.stringify({
events: [
{
type: "user.message",
content: [
{
type: "text",
text: "The shop is called Field Day.",
},
],
},
],
}),
})
const sent = await response.json()import os
import requests
response = requests.post(
"https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events",
headers={
"x-api-key": os.environ["LITTLE_WORKER_API_KEY"],
"Little-Worker-Version": "2026-10-19",
"Idempotency-Key": "2f6c9a1e-8b3d-4a57-9e0c-4d1b7f5a3c82",
},
json={
"events": [
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "The shop is called Field Day.",
},
],
},
],
},
)
sent = response.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."
}
]
}
]
}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_idtask idRequiredThe task, such astask_01JA8Z6Q4M3T9V2K7X5R1B0C8D.
Query parameters
afterstringSend only events after this sequence. A whole number as a string, such as"42"."0"is the start.event_deltas[]stringA kind of event to preview while it is written. Repeat it to name both.One of
worker.message,worker.thinking
Headers
Last-Event-IDstringTheidof the last frame you read, which is its event'ssequence. 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.
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.
Errors
| Status | Type | When |
|---|---|---|
| 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. |
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"const response = await fetch("https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events/stream?after=0", {
headers: {
"x-api-key": process.env.LITTLE_WORKER_API_KEY,
"Little-Worker-Version": "2026-10-19",
},
})
const decoder = new TextDecoder()
for await (const chunk of response.body ?? []) {
process.stdout.write(decoder.decode(chunk, { stream: true }))
}import os
import requests
response = requests.get(
"https://core.thelittleworker.com/api/v1/tasks/task_01JA8Z6Q4M3T9V2K7X5R1B0C8D/events/stream",
headers={
"x-api-key": os.environ["LITTLE_WORKER_API_KEY"],
"Little-Worker-Version": "2026-10-19",
},
params={
"after": "0",
},
stream=True,
)
for line in response.iter_lines(decode_unicode=True):
print(line){
"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
}