session-threads changedmanaged-agents/session-threads
Nearest release: v2.1.296, published 4 hours before this site recorded the change. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Recorded here
Lines+815added
Lines−0removed
From line
—
no hunk to open at
First seen
9 Oct 2026
this site's first read of the page
Recorded edits1to this page, all time
## 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
The whole hunk
815 lines, new page
/
lines
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.
No line in this hunk matches that.