One read of Claude Developer Platformapi-20261009T213714Z
59 pages moved out of 762 read.
What this read moved
26-50 of 59, page 2 of 3This capture is too large to show at once. Changes 26-50 of 59 are below, significant first; the rest are on the following screens.
api/beta/sessions/threads/events Changed · +1667 / -879 lines
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
api/beta/sessions/threads/events/list Changed · +833 / -439 lines
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
api/beta/sessions/threads/events/stream Changed · +834 / -440 lines
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
api/beta/sessions/threads/list Changed · +67 / -6 lines
api/beta/sessions/threads/retrieve Changed · +53 / -6 lines
api/beta/sessions/update Changed · +571 / -437 lines
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
api/completions Changed · +0 / -12 lines
api/completions/create Changed · +0 / -8 lines
api/messages Changed · +28 / -64 lines
This page is larger than the 256 KiB this site keeps, so one side of the diff below stops where the stored text does.
api/messages/batches Changed · +9 / -29 lines
This page is larger than the 256 KiB this site keeps, so one side of the diff below stops where the stored text does.
api/messages/batches/create Changed · +9 / -13 lines
api/messages/count_tokens Changed · +9 / -13 lines
api/messages/create Changed · +9 / -17 lines
build-with-claude/preserved-thinking Changed · +10 / -2 lines
#### Output-token cost of `"drop_block"`
cli-sdks-libraries/sdks/typescript Changed · +4 / -4 lines
managed-agents/budgets Changed · +10 / -6 lines
managed-agents/session-threads New page · 815 lines, new page
## Primary thread and session threads ## List threads ## Interrupt a session thread ## Archive a session thread ## Primary thread events ## Session thread events ## Tool permissions and custom tools
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Session threads
url: https://platform.claude.com/docs/en/managed-agents/session-threads
description: List, interrupt, and archive the threads of a multiagent session, read their events, and handle tool permissions across them.
featureMetadata:
topic:
title: Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/overview
status: beta
betaHeader: managed-agents-2026-04-01
---
In a [multiagent session](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration), each agent works in its own **session thread**. This page covers how to list, interrupt, and archive threads, the events they send, and how tool permissions work across them. A [workflow run](https://platform.claude.com/docs/en/managed-agents/workflow-runs) creates session threads too.
## Primary thread and session threads
The **session-level event stream** (`/v1/sessions/{session_id}/events/stream`) is considered the **primary thread**, containing a condensed view of all activity across all threads. You don't see the full activity from [subagents](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#delegate-to-subagents), but you do see the start and end of their work, and blocking events such as tool permission requests.
**Session threads** are where you drill into a specific agent's activity.
The session `status` is an aggregation of all agent activity; if at least one thread is `running`, then the overall session status is `running` as well. A [workflow run](https://platform.claude.com/docs/en/managed-agents/workflow-runs) that's running can keep the session `running` too, even while none of its threads is working. When no thread is working and a thread waits on your client, the session is `idle`; see [Know when the work is done](https://platform.claude.com/docs/en/managed-agents/workflow-runs#know-when-the-work-is-done).
A [session budget](https://platform.claude.com/docs/en/managed-agents/budgets) is a single shared cap across all of a session's threads. As the cap is reached, threads pause independently, and each thread's cost is priced at the thread's own served model.
<Note>
A session can have at most 25 child threads at a time. Idle threads count until you archive them, and the primary thread doesn't count. The primary thread's agent can call multiple copies of a single subagent, creating multiple threads associated with one `agent`. [Advisor](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor) consultation threads are exempt from this limit. So are a [workflow run](https://platform.claude.com/docs/en/managed-agents/workflow-runs)'s threads.
</Note>
## List threads
List all threads associated with a session as follows:
<CodeGroup>
```bash cURL
curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01"
```
```bash CLI
ant beta:sessions:threads list --session-id "$SESSION_ID"
```
```python Python
for thread in client.beta.sessions.threads.list(session.id):
agent = thread.agent
label = agent.type if agent.type == "advisor" else agent.name
print(f"[{label}] {thread.status}")
```
```typescript TypeScript
for await (const thread of client.beta.sessions.threads.list(session.id)) {
const label = thread.agent.type === "advisor" ? thread.agent.type : thread.agent.name;
console.log(`[${label}] ${thread.status}`);
}
```
```csharp C#
await foreach (var thread in (await client.Beta.Sessions.Threads.List(session.ID)).Paginate())
{
var label = thread.Agent.TryPickBetaManagedAgentsSessionThread(out var agent)
? agent.Name
: thread.Agent.Json.GetProperty("type").GetString();
Console.WriteLine($"[{label}] {thread.Status.Raw()}");
}
```
```go Go
threads := client.Beta.Sessions.Threads.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionThreadListParams{})
for threads.Next() {
thread := threads.Current()
label := cmp.Or(thread.Agent.Name, thread.Agent.Type)
fmt.Printf("[%s] %s\n", label, thread.Status)
}
if err := threads.Err(); err != nil {
panic(err)
}
```
```java Java
for (var thread : client.beta().sessions().threads().list(session.id()).autoPager()) {
var agent = thread.agent();
var label = agent.isAgent() ? agent.asAgent().name() : agent.type().asString();
IO.println("[" + label + "] " + thread.status());
}
```
```php PHP
foreach ($client->beta->sessions->threads->list($session->id)->pagingEachItem() as $thread) {
$label = $thread->agent instanceof \Anthropic\Beta\Agents\BetaManagedAgentsAdvisor
? $thread->agent->type
: $thread->agent->name;
echo "[{$label}] {$thread->status}\n";
}
```
```ruby Ruby
client.beta.sessions.threads.list(session.id).auto_paging_each do |thread|
agent = thread.agent
label = agent.type == :advisor ? agent.type : agent.name
puts "[#{label}] #{thread.status}"
end
```
</CodeGroup>
The full list includes the primary thread. `parent_thread_id` is `null` for the primary thread. Every other thread is a child thread. `workflow_run_id` is `null` except on [a run's threads](https://platform.claude.com/docs/en/managed-agents/workflow-runs#a-runs-threads).
To list only threads that have certain statuses, add `statuses[]` to the request, and repeat it to give more than one status, as in `?statuses[]=running&statuses[]=idle`. Leave it out to return threads of every status.
## Interrupt a session thread
Send `user.interrupt` with `session_thread_id` to stop a specific thread. Omitting `session_thread_id` interrupts every non-archived thread in the session, including the primary. In a session with dynamic workflows, an interrupt ends no run, and one that names a run's thread stops nothing. An interrupt closes other child threads' pending tool calls, but don't rely on it to close a run thread's. See [Interrupt a session with runs open](https://platform.claude.com/docs/en/managed-agents/workflow-runs#interrupt-a-session-with-runs-open).
<CodeGroup>
```bash cURL
curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d "{\"events\": [{\"type\": \"user.interrupt\", \"session_thread_id\": \"$THREAD_ID\"}]}"
```
```bash CLI
ant beta:sessions:events send \
--session-id "$SESSION_ID" \
--event "{type: user.interrupt, session_thread_id: $THREAD_ID}"
```
```python Python
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.interrupt", "session_thread_id": thread.id}],
)
```
```typescript TypeScript
await client.beta.sessions.events.send(session.id, {
events: [{ type: "user.interrupt", session_thread_id: thread.id }],
});
```
```csharp C#
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserInterruptEventParams
{
Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt,
SessionThreadID = thread.ID,
},
],
});
```
```go Go
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{
Type: anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt,
SessionThreadID: anthropic.String(thread.ID),
},
}},
}); err != nil {
panic(err)
}
```
```java Java
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserInterruptEventParams.builder()
.type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT)
.sessionThreadId(thread.id())
.build())
.build());
```
```php PHP
$client->beta->sessions->events->send(
$session->id,
events: [
['type' => 'user.interrupt', 'session_thread_id' => $thread->id],
],
);
```
```ruby Ruby
client.beta.sessions.events.send_(
session.id,
events: [{type: "user.interrupt", session_thread_id: thread.id}]
)
```
</CodeGroup>
Against a subagent's thread blocked on `requires_action`, the interrupt closes each pending tool call with an error tool result ("Tool execution was interrupted before completion. Please retry.") and re-emits `session.thread_status_idle` with `stop_reason: end_turn` directly; the model is not sampled. Against a child thread that's idle with `end_turn` or `budget_reached`, the interrupt is a no-op. An interrupt that names a terminated thread returns a 400 error. An interrupted child doesn't send the primary thread's agent the report it sends when a turn ends. While that agent waits on the child, it doesn't start another turn until something else reaches it, such as a `user.message` or another thread's report.
## Archive a session thread
Optionally archive a session thread when it has completed its work. Archiving a thread frees its place under the 25-child-thread limit. The server archives a [workflow run](https://platform.claude.com/docs/en/managed-agents/workflow-runs)'s threads itself. You don't need to archive them, and you can't while the run is open.
<CodeGroup>
```bash cURL
curl -fsS -X POST "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/archive" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01"
```
```bash CLI
ant beta:sessions:threads archive \
--session-id "$SESSION_ID" \
--thread-id "$THREAD_ID"
```
```python Python
archived = client.beta.sessions.threads.archive(thread.id, session_id=session.id)
print(archived.status, archived.archived_at)
```
```typescript TypeScript
const archived = await client.beta.sessions.threads.archive(thread.id, {
session_id: session.id,
});
console.log(archived.status, archived.archived_at);
```
```csharp C#
var archived = await client.Beta.Sessions.Threads.Archive(thread.ID, new() { SessionID = session.ID });
Console.WriteLine($"{archived.Status} {archived.ArchivedAt}");
```
```go Go
archived, err := client.Beta.Sessions.Threads.Archive(ctx, thread.ID, anthropic.BetaSessionThreadArchiveParams{
SessionID: session.ID,
})
if err != nil {
panic(err)
}
fmt.Println(archived.Status, archived.ArchivedAt)
```
```java Java
var archived = client.beta().sessions().threads().archive(
thread.id(),
ThreadArchiveParams.builder()
.sessionId(session.id())
.build());
IO.println(archived.status() + " " + archived.archivedAt().orElseThrow());
```
```php PHP
$archived = $client->beta->sessions->threads->archive($thread->id, sessionID: $session->id);
echo "{$archived->status} {$archived->archivedAt->format(DATE_ATOM)}\n";
```
```ruby Ruby
archived = client.beta.sessions.threads.archive(thread.id, session_id: session.id)
puts "#{archived.status} #{archived.archived_at}"
```
</CodeGroup>
Archive only succeeds if the thread is `idle`. A thread parked on `requires_action` counts as idle and can be archived directly; only a running thread must be interrupted first:
<CodeGroup>
```bash cURL
# Interrupt the thread, then archive it
curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d "{\"events\": [{\"type\": \"user.interrupt\", \"session_thread_id\": \"$THREAD_ID\"}]}"
curl -fsS -X POST "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/archive" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01"
```
```bash CLI
ant beta:sessions:events send \
--session-id "$SESSION_ID" \
--event "{type: user.interrupt, session_thread_id: $THREAD_ID}"
ant beta:sessions:threads archive \
--session-id "$SESSION_ID" \
--thread-id "$THREAD_ID"
```
```python Python
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.interrupt", "session_thread_id": thread.id}],
)
archived = client.beta.sessions.threads.archive(thread.id, session_id=session.id)
print(archived.status, archived.archived_at)
Cut at 300 lines. The page has the rest.
managed-agents/workflow-runs New page · 824 lines, new page
## How dynamic workflows work ## How a run moves through its states ## Run events ## A run's threads ## Know when the work is done ## Follow a run ## Interrupt a session with runs open ## While a run is open ### Rebuild run state after you reconnect ## Budgets and limits ### Rate limits
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Workflow runs
url: https://platform.claude.com/docs/en/managed-agents/workflow-runs
description: "Follow an agent's workflow runs: their states and events, when the work is done, what a run blocks, budgets, and limits."
featureMetadata:
topic:
title: Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/overview
status: beta
betaHeader: managed-agents-2026-04-01
---
A **workflow** is a program that an agent writes to run many agents and combine what they return. A **workflow run** is the execution of one workflow. **[Dynamic workflows](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#dynamic-workflows)** is the feature that lets an agent write workflows and start runs. You turn it on or off with the `workflows` setting in the agent's `multiagent` block.
The server runs a workflow in the background. Its agents work in [session threads](https://platform.claude.com/docs/en/managed-agents/session-threads) that the server creates as the workflow needs them. You follow runs on the session's [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming). Only the agent starts a run. No event you send ends one; archiving the session can.
## How dynamic workflows work
The agent that the session runs writes each workflow for the work that you describe. A workflow is a program: it runs other agents, collects what each one returns, and combines the results. That way, the agent can take on a task that is too large for one conversation, such as a review of hundreds of documents. During a run, the agent can keep working or end its turn, and it can check on the run.
<Frame>

</Frame>
The diagram shows one example. Each workflow that the agent writes has its own phases and agents. A run has these layers:
* **Workflow run:** The server runs the workflow in the background, as one workflow run. A session can have several runs open at the same time.
* **Phases:** A workflow can divide its work into phases. A phase is a named stage of the run, such as "Read the contracts". You follow a run's progress by its phase events.
* **Agent threads:** In a phase, the program runs agents. Each agent works in its own [session thread](https://platform.claude.com/docs/en/managed-agents/session-threads), on a prompt that the program wrote. An agent in a run can be an inline agent, which the program defines itself, or a predefined agent, which you list in [`workflows.predefined_agents`](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#predefined-and-inline-agents). For what each thread shows, see [A run's threads](https://platform.claude.com/docs/en/managed-agents/workflow-runs#a-runs-threads).
The program can do the following:
* **Run agents at the same time:** The program can run many agents at the same time, which is called fanning out. In the diagram, three agents read contracts in the first phase.
* **Pass results from one agent to another:** Each agent returns its result to the program. The program can pass that result on to another agent. In the diagram, the agent in the second phase works with what the first three returned. A run's agents also work with the same files, in the session's sandbox.
* **Take the next step by itself:** An agent's result goes to the program, not to the agent that the session runs. The program determines which agents run next, and it writes their prompts.
* **Repeat and choose:** Inside a phase, the program can repeat work and choose its next step from what an agent returned. For example, it can have a draft revised until a review passes or a set number of rounds is used up. In the diagram, the program can repeat a step inside the second phase.
* **Handle a failed agent:** When one of its agents fails, the program can handle the failure or let it end the run.
When the run ends, the agent that the session runs gets a turn to read what the run did. It can then answer you or start another run. [Run events](https://platform.claude.com/docs/en/managed-agents/workflow-runs#run-events) lists the cases where that turn comes later or doesn't come.
You can guide how a run does the work, for example how it splits the work and what it does when an agent fails. See [Tell the agent when to use a run](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#tell-the-agent-when-to-use-a-run).
## How a run moves through its states
<Frame>

</Frame>
A run starts as running or as idle. Reaching the budget, for example, pauses a running run, which makes it idle; raising or removing the budget then makes it run again, unless an interrupt paused it too. A running run ends when its workflow finishes, the agent stops it, it fails, its lifetime passes, or the session is archived. An idle run can end too, for example when the agent stops it or the session is archived.
A run is **open** from its `workflow_run.created` event until its `workflow_run.status_ended` event, whether it's running or idle. A run is idle while it's paused, for example at the session's budget. A run's lifetime is 24 hours by default. The agent can set a shorter lifetime when it starts the run. Time that a run spends waiting on your client counts toward that lifetime. A pause doesn't stop a run's lifetime from passing, so a run that stays paused can end with `timeout_error`. The following events report a run's start, its phases, and its end. A pause at the budget sends one too. A pause after an interrupt might send none. Every `workflow_run.*` event includes `workflow_run_id`, which is `null` only on a `workflow_run.error` when no run was created.
## Run events
Run events arrive on the session's event stream, which is the primary thread's stream, and listing the session's events returns them too. Run events don't trigger [webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks). Status events from the run's threads arrive on the same stream. Each names its thread in `session_thread_id`, and a run's threads are the ones whose `session.thread_created` event had the run's `workflow_run_id`.
| Event | When it arrives | What to do |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workflow_run.created` | The agent started a run. Includes `workflow_run_id` (`wrun_…`), the run's `name` and `description`, and `phases`, the phases the workflow declares, each with an `id`, a `name`, and a `description`. A `description` is `null` when the workflow gives none. `phases` is always present and can be empty. The run's and phases' `name` and `description` are text the model wrote, so they can repeat words from your request. A run's `name` can also be one the server assigned. | Track the run as open. Show its `name`, and progress against `phases`. |
| `workflow_run.status_running` | When the run starts to execute, which can be a while after `created`, and each time it resumes after a pause at the budget. A resume after an interrupt might not send it. A run that starts idle might get `workflow_run.status_idle` first. | Show the run as running. |
| `workflow_run.status_idle` | The run was paused, for example at the session's budget. The event doesn't say why. A pause after an interrupt might not send it. | To continue, see [Budgets and limits](https://platform.claude.com/docs/en/managed-agents/workflow-runs#budgets-and-limits) or [Interrupt a session with runs open](https://platform.claude.com/docs/en/managed-agents/workflow-runs#interrupt-a-session-with-runs-open). |
| `workflow_run.phase_started`, `workflow_run.phase_ended` | The workflow entered or left a phase, or the run's end closed a phase that was still open. The end event doesn't say whether the phase's work finished. Both include `workflow_run_phase_id`. The end also has `phase_started_id`, the `id` of the start event it closes. Neither has the phase's name: look it up by `workflow_run_phase_id` in the `phases` of `workflow_run.created`. | Update progress. Phases run one at a time, in the order of `phases`, each at most once, but the API doesn't guarantee it. Match a phase's end to its start by `phase_started_id`. Handle more than one open phase, a phase that isn't in `phases`, and a listed phase that never starts, even in a run that completes. Every phase that starts also ends, before the run's `workflow_run.status_ended`. |
| `workflow_run.status_ended` | The run ended. Always the last of the run's `workflow_run.*` events. Includes `result`. | Read `result` (next table). The agent then gets a turn to read how the run ended. At the budget, or while the primary thread waits on your client, that turn comes later. After an interrupt, that turn might not come: send a `user.message`, or read `result` yourself. After an archive or termination, it doesn't come. |
| `workflow_run.error` | The server reports an error of a run, or a start that it refused. A run that ends in `error` gets this event, with the same error, before its `workflow_run.status_ended`. Includes `error`: a `type` and a `message` that is safe to log. `workflow_run_id` is `null` when no run was created. | Log it, and don't take it as the run's end. If `workflow_run_id` is `null`, no run started. Otherwise keep tracking the run until its `workflow_run.status_ended`. |
| `result` | Meaning |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{"type": "completed"}` | The workflow finished running. The result doesn't say whether the work passed. A run can end `completed` even though work on its threads failed, or a thread couldn't be created. To find failed work, read the events of each of the [run's threads](https://platform.claude.com/docs/en/managed-agents/workflow-runs#a-runs-threads). |
| `{"type": "stopped"}` | The agent stopped the run, or the session was archived. The event doesn't say which, and later releases might add other causes. |
| `error` with `timeout_error` | The run reached its lifetime: 24 hours by default, or the one the agent set. |
| `error` with `program_error` | The workflow failed. Its code failed, or it broke a rule for workflows, other than a limit. Or one of the run's threads failed, or couldn't be created, and the workflow let that end the run. |
| `error` with `thread_limit_error` | The run went over its [limit on the agents that a workflow starts](https://platform.claude.com/docs/en/managed-agents/workflow-runs#budgets-and-limits). |
| `error` with `unknown_error` | The server couldn't continue the run, or the run went over one of the server's other limits on workflows. |
An error result looks like `{"type": "error", "error": {"type": "timeout_error", "message": "..."}}`, where `message` is safe to log. Treat an unrecognized `result.type` as a run that ended some other way, and an unrecognized `error.type` as an error. When something the session depends on fails, such as the model, an MCP server, credentials, or billing, the failing thread's stream gets a `session.error`. That doesn't end a run by itself. But if it makes one of the run's threads fail, and the workflow lets that end the run, the run ends with `program_error`.
For example, you ask the [contract-review agent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#turn-on-dynamic-workflows) which of 300 contracts have a change-of-control clause, and the agent starts a run:
1. `workflow_run.created` names the run "Find change-of-control clauses" and lists the phases "Read the contracts" and "Reconcile the findings" in `phases`. Then `workflow_run.status_running` follows.
2. Phase events mark each phase, and each thread the run creates sends `session.thread_created` with the run's `workflow_run_id`.
3. `workflow_run.status_ended` arrives with `result: {"type": "completed"}`.
4. The agent answers, "41 of the 300 contracts have one," and `session.status_idle` arrives with `end_turn`.
The run's first event lists its phases:
```json
{
"type": "workflow_run.created",
"id": "sevt_01abc...",
"workflow_run_id": "wrun_01J8XkN5uT3vHpLqRfWdY2",
"name": "Find change-of-control clauses",
"description": "Reads each contract and lists those that have the clause.",
"phases": [
{
"id": "wrph_01Kd3a1f3",
"name": "Read the contracts",
"description": "Reads each contract for the clause."
},
{ "id": "wrph_01Kd3b7c9", "name": "Reconcile the findings", "description": null }
],
"processed_at": "2026-10-09T14:01:45Z"
}
```
Each phase event names its phase by `workflow_run_phase_id`. That is an `id` in `phases`, but the API doesn't guarantee it:
```json
{
"type": "workflow_run.phase_started",
"id": "sevt_01def...",
"workflow_run_id": "wrun_01J8XkN5uT3vHpLqRfWdY2",
"workflow_run_phase_id": "wrph_01Kd3a1f3",
"processed_at": "2026-10-09T14:01:46Z"
}
```
The run's last event reports how it ended:
```json
{
"type": "workflow_run.status_ended",
"id": "sevt_01ghi...",
"workflow_run_id": "wrun_01J8XkN5uT3vHpLqRfWdY2",
"result": { "type": "completed" },
"processed_at": "2026-10-09T14:09:12Z"
}
```
## A run's threads
Each agent in a run works in its own [session thread](https://platform.claude.com/docs/en/managed-agents/session-threads), which the server creates as the workflow needs it. You can list, read, and stream a run's threads like any child thread, and answer their tool calls from the primary stream. To stop them, ask the agent to stop the run (see [Interrupt a session with runs open](https://platform.claude.com/docs/en/managed-agents/workflow-runs#interrupt-a-session-with-runs-open)). You can't stop one by its ID, or archive one while its run is open.
* **Grouping:** A run's thread carries the run's `workflow_run_id`, as does the `session.thread_created` event that announces it. Other threads, and the `session.thread_created` events that announce them, have `workflow_run_id` set to `null`.
* **Agent:** `agent` shows the agent the thread runs. For an agent you listed in [`multiagent.workflows.predefined_agents`](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#turn-on-dynamic-workflows), `agent` has that agent's `id` and `version`, as on the thread of a subagent you listed. For an agent the workflow defines (an inline agent), `agent` has `type` `inline` and no `id` or `version`. It has the system prompt the workflow wrote, not the session agent's. It also has the name and description the workflow gave it; the server assigns a name if the workflow gave none. It uses the model of the session agent, the agent the session runs. Its tools, MCP servers, and skills are a subset of the session agent's. It gets all of them, but the API doesn't guarantee that. Its tools keep their permission policies.
* **What the threads share:** A run's threads work in the session's sandbox, so every thread works with the same files. That includes the files of a memory store the session mounts. An agent the workflow defines uses its MCP servers with the credentials the session resolves for them. Each thread has its own conversation history.
* **Events:** A run thread's `session.thread_created`, `session.thread_status_running`, `session.thread_status_idle`, and `session.thread_status_terminated` events also arrive on the primary stream (see [Run events](https://platform.claude.com/docs/en/managed-agents/workflow-runs#run-events)). Its message events stay on its own stream. Its thread webhooks are sent as for any child thread. For what the thread's own stream records, see [Session thread events](https://platform.claude.com/docs/en/managed-agents/session-threads#session-thread-events).
* **Phases:** No event or field says which phase a thread works in, and threads of one run can have the same `agent_name`. Follow a run's progress by its phase events, and tell its threads apart by `session_thread_id`.
* **Thread limit:** A run's threads are exempt from the session's [child-thread limit](https://platform.claude.com/docs/en/managed-agents/session-threads#primary-thread-and-session-threads).
* **Starting runs:** Only the agent on the session's primary thread starts runs. An agent working in a run's thread can't start a run of its own, so runs don't nest.
* **Archiving:** The server archives each thread no later than the end of its run. It can archive one sooner, once the thread returns its result or the run finishes with it. If the thread is still running or waiting on your client at that point, the server stops it first. An archived thread stays in the thread list, with status `terminated`. You don't need to archive a run's threads yourself. While the run is open, a request to archive one that the server hasn't archived yet returns 400 with `error.details.error_code: "workflow_run_open"`.
* **Visibility:** You don't see the workflow's code, but you can ask the agent for the workflow, as the tip after this list describes. You also don't see the tool calls the agent makes to start and manage runs, or the result each thread returns to the workflow.
<Tip>
You can ask the agent for the workflow that it wrote for your request. Wait until the run has ended and the session is `idle`. Then send a `user.message` that asks the agent to print the workflow that it started the run with, word for word, and not to start another run.
```text wrap
Print the workflow that you started the run with, word for word, in one code block. Do not start another run.
```
</Tip>
## Know when the work is done
While a run is running, expect the session to stay `running`, even while none of its threads is working. It goes `idle` with `requires_action` when no thread is working and a thread waits on your client. An idle by itself doesn't mean that the work is done. The work is done when both are true:
1. Every run you've seen created has its `workflow_run.status_ended`.
2. After that, a `session.status_idle` arrives with `stop_reason` `end_turn`, and your own request, such as an interrupt, didn't cause it. After you interrupt, count only an idle that comes after your next `user.message` or `user.define_outcome`.
* **Paused runs:** A paused run doesn't keep the session `running`, so the session can go idle while the run is still open. At the budget, for example, the session goes idle with `budget_reached`. The work isn't done until the run ends.
* **Another run:** The agent can start a new run when it reads a result, so check again.
* **Outcomes:** If you [defined an outcome](https://platform.claude.com/docs/en/managed-agents/define-outcomes), no evaluation starts while a run is open, whether it's running or idle. The turn in which the agent reads the run's result can start one.
* **`retries_exhausted`:** The agent's turn failed on an error: retries ran out, or the error can't be retried, such as a billing failure. A run might still be running when this idle comes. If a run ended and the agent hasn't read its result yet, the server starts a new turn with no input from you. The session goes `running` again, so wait for the next idle. If the session stays idle, read the `session.error` that came before it and fix the cause. Then send a `user.message`, or read each run's `result` yourself.
## Follow a run
This sample follows a session from your message to the agent's answer. It opens the stream and sends the message. Then it does the following:
* **Tracks each run** from its `workflow_run.created` to its `workflow_run.status_ended`, and prints each phase as it starts.
* **Answers custom tool calls** when each `agent.custom_tool_use` arrives, because a run's thread can wait on your client while the session stays `running`. If your agent's tools [ask for confirmation](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#tool-confirmation), add a branch that answers each `agent.tool_use` or `agent.mcp_tool_use` whose `evaluated_permission` is `ask`. The sample has none, because a branch that allows every call would turn `always_ask` into always allow.
* **Stops** when the [work is done](https://platform.claude.com/docs/en/managed-agents/workflow-runs#know-when-the-work-is-done): no run is open, and the session goes idle with `end_turn`. It also stops if the session terminates. On an idle with any other stop reason except `requires_action`, such as `budget_reached`, `retries_exhausted`, or `refusal`, it prints the reason and stops, so handle those in your own code. It stops on `retries_exhausted` even when the server is about to start a new turn by itself. It keeps waiting on `requires_action`, and on `end_turn` while a run is open.
<CodeGroup>
```bash cURL
# This workflow does not translate well to a one-off shell command.
# Use one of the SDK examples in this code group instead.
```
```bash CLI
# This workflow does not translate well to a one-off shell command.
# Use one of the SDK examples in this code group instead.
```
```python Python
open_runs: dict[str, str] = {} # workflow_run_id -> run name
phase_names: dict[tuple[str, str], str] = {} # (run ID, phase ID) -> phase name
# Open the stream first, then send the user message
with client.beta.sessions.events.stream(session_id) as stream:
client.beta.sessions.events.send(
session_id,
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": "Which contracts in /contracts have a change-of-control clause?",
},
],
},
],
)
for event in stream:
match event.type:
case "workflow_run.created":
open_runs[event.workflow_run_id] = event.name
for phase in event.phases:
phase_names[event.workflow_run_id, phase.id] = phase.name
print(f"Run started: {event.name}")
case "workflow_run.phase_started":
phase_id = event.workflow_run_phase_id
key = (event.workflow_run_id, phase_id)
print(f" Phase: {phase_names.get(key, phase_id)}")
case "workflow_run.status_ended":
name = open_runs.pop(event.workflow_run_id, event.workflow_run_id)
print(f"Run ended: {name} ({event.result.type})")
case "agent.custom_tool_use":
# Answer when the event arrives. A run's thread can wait on your
# client while the session stays running.
result = call_tool(event.name, event.input)
try:
client.beta.sessions.events.send(
session_id,
events=[
{
"type": "user.custom_tool_result",
"custom_tool_use_id": event.id,
"content": [{"type": "text", "text": result}],
},
],
)
except anthropic.BadRequestError as error:
# The server refuses a result that comes too late, after it
# archived the call's thread. Keep following the run.
print(f" Answer to {event.name} refused: {error.message}")
case "session.status_idle":
# Done when every run has ended and the agent has finished its turn
if not open_runs and event.stop_reason.type == "end_turn":
break
# An idle with requires_action waits on your client, so keep reading.
# On any other stop reason, print it and stop.
if event.stop_reason.type not in ("end_turn", "requires_action"):
print(f"Session idle: {event.stop_reason.type}")
break
case "session.status_terminated":
break
```
```typescript TypeScript
const openRuns = new Map<string, string>(); // workflow_run_id -> run name
const phaseNames = new Map<string, string>(); // "run ID:phase ID" -> phase name
// Open the stream first, then send the user message
const stream = await client.beta.sessions.events.stream(sessionId);
await client.beta.sessions.events.send(sessionId, {
events: [
{
type: "user.message",
content: [{ type: "text", text: "Which contracts in /contracts have a change-of-control clause?" }],
},
],
});
events: for await (const event of stream) {
switch (event.type) {
case "workflow_run.created":
openRuns.set(event.workflow_run_id, event.name);
for (const phase of event.phases) {
phaseNames.set(`${event.workflow_run_id}:${phase.id}`, phase.name);
}
console.log(`Run started: ${event.name}`);
break;
case "workflow_run.phase_started": {
const phaseId = event.workflow_run_phase_id;
const phaseName = phaseNames.get(`${event.workflow_run_id}:${phaseId}`);
console.log(` Phase: ${phaseName ?? phaseId}`);
break;
}
case "workflow_run.status_ended": {
const name = openRuns.get(event.workflow_run_id) ?? event.workflow_run_id;
openRuns.delete(event.workflow_run_id);
console.log(`Run ended: ${name} (${event.result.type})`);
break;
}
case "agent.custom_tool_use": {
// Answer when the event arrives. A run's thread can wait on your
// client while the session stays running.
const result = await callTool(event.name, event.input);
try {
await client.beta.sessions.events.send(sessionId, {
events: [
{
type: "user.custom_tool_result",
custom_tool_use_id: event.id,
content: [{ type: "text", text: result }],
},
],
});
} catch (error) {
// The server refuses a result that comes too late, after it
Cut at 300 lines. The page has the rest.
api/beta/organization Changed · +2 / -2 lines
This page is larger than the 256 KiB this site keeps, so one side of the diff below stops where the stored text does.