What Is a Thread?
A thread is a conversation instance backed by a Cloudflare Durable Object and SQLite state. Each thread has isolated:- messages
- execution state
- logs
- filesystem
Durable Runtime
New projects do not create local Durable Object subclass files. Thebuilder()
Vite plugin generates the Worker entry and exports the built-in thread and
builder Durable Objects with the required Cloudflare bindings and migrations.
ThreadState APIs
Tools/hooks/endpoints receiveThreadState.
Key methods:
getMessages()/injectMessage()/updateMessage()queueMessage()getParentThread()/getChildThread(referenceId)terminate()- filesystem methods (
readFile,writeFile, etc.) status()— the thread’s authoritative run status (see below)log(entry)— record an application log entry (see below)
children(resumable child registry)terminatedarguments(read-only invocation or creation arguments)context(mutable scratch data for the current execution only)
Run Status
await state.status() returns the thread’s ThreadStatus:
{ state: 'idle' | 'running' | 'failed', since, reason }. since is in
microseconds; reason is the raw stop reason code of the last transition out
of running (stop_on_response, stop_tool, awaiting_human,
hook_blocked, aborted_by_user, step_error, execution_lost, …). A run
that hands the turn to a human side settles as idle with reason
awaiting_human (or the code that ended the turn); the status describes
execution, not whose turn it is.
The status is persisted with the thread, so it survives restarts, and a run
the runtime lost settles to failed (execution_lost) instead of staying
running. Clients receive it without polling: GET /api/threads/:id includes
status, and the message stream sends a
thread_status snapshot on connect and a frame on every change.
status() is unrelated to setStatus(label), which writes a free-form label
into this thread’s entry in its parent’s subagent registry.
Application Logs
await state.log(entry) records an application entry in the thread’s logs,
next to the runtime’s own LLM request, tool, and handler rows. Any code with a
ThreadState can write one; only name is required.
A plain event is a name and a message:
Entries are delivered to live log subscribers and returned by
GET /api/threads/:id/logs as rows with kind: 'app': model holds the
name, and the other fields are app_level, app_message, app_status,
app_error, app_duration_ms, app_cost, plus JSON text in app_request,
app_response, app_trigger, and app_data. The builder UI opens these rows
in the log detail pane with the trigger, duration, cost, and Request and
Response tabs, and show the cost in the logs table’s Cost column.
An entry’s cost is part of what the thread cost: GET /api/threads/:id/cost
adds it to totalCost (and to the thread’s row in the per-thread
breakdown), and reports the application share apart as appCost. The
per-model breakdown and requestCount cover LLM requests only. The row’s own
cost_total stays null; it is the LLM request cost.
Logging never creates a message and never starts or wakes an execution, so it
is safe from hooks, tools, effects, and endpoints. It never throws because of
an entry’s content: fields of the wrong type are dropped, values that cannot be
JSON-encoded are replaced by a marker object, and oversized fields are
truncated with a marker (name to 120 characters, message and error to
4 KiB, the JSON of data to 16 KiB, and the JSON of request and response
to 64 KiB each). Entries attach to the thread’s newest message; on a thread
with no messages yet the entry is written to the Worker console only.
User Identity and Application Roles
await state.user() returns the thread owner’s identity and user handle, or
null for a thread without a user. Its optional roles array carries
application-defined strings from the runtime’s identity authority. Hosted
AgentBuilder instances receive them from Platform through signed updates.
Check roles in each protected request or execution and treat absent roles as
an empty assignment. Identity is memoized within an execution; pushed changes
are visible on the next request or execution. These strings do not change built-in instance access levels
or permissions. See application role checks
for a server-side endpoint example.
Queue Semantics
queueMessage() is durable and ordered:
- if thread is active, queued messages are injected before the next model step
- if thread is idle, queueing forces execution
- queue entries survive eviction
- message APIs expose
sendMessage()andqueueMessage()queued entries as pending messages withmetadata.queued: true
Parent/Child Communication
Subagent communication is message + filesystem aware:Parent -> Child
- initial invocation mapping and resumable
subagent_message subagent_create.argumentspersists with the child thread asThreadState.argumentsand renders as prompt variables for both child sides- any referenced attachments are copied parent -> child
Child -> Parent
- completion/failure payloads are queued as silent parent messages
- returned attachments are copied child -> parent
- copied attachment paths are rewritten for the destination thread
Termination
terminate() performs soft shutdown:
- sets
terminatedtimestamp - aborts in-flight execution
- rejects new execution/queue operations
- propagates child termination status to parent registry when linked
Registry and Message Projection
Parent threads maintain a child registry for resumable instances. Messages may carry projected subagent fields (for UI/client grouping), including:subagent_idsubagent_namesubagent_titlesubagent_statussubagent_resumablesubagent_blocking