As of 2026-10-13, the Claude platform release notes place dynamic workflows on 2026-10-09. The feature is still beta, with the header managed-agents-2026-04-01. The agent writes a program that runs many agents in phases and combines their results. You follow workflow_run.* on the session event stream. Done means two JSON facts at once: every run you have seen has workflow_run.status_ended, and a later session.status_idle carries stop_reason: "end_turn". result.type of completed only means the program finished. It does not mean all 300 contracts were reviewed. Why an event should be an object you can check: why AI models like JSON. How agents hand work to each other: how Claude agents exchange JSON.
You ask an agent to find a change-of-control clause in 300 contracts. It should not stuff 300 reads into one conversation. It writes a workflow and runs the phases in the background. Your program does not see the source. It sees a stream of events. Miss one field, and βthe program endedβ becomes βthe review passed.β
What the docs actually open
As of 2026-10-13, follow the release note and the Workflow runs page:
- Switch: beta header
managed-agents-2026-04-01. Set the agent'smultiagentto{"type":"multiagent_20261001","workflows":{"type":"enabled"}}. Say in the system prompt when a run should start. - Who starts it: only the agent on the session's primary thread. An agent inside a run cannot start another run, so runs do not nest. An event you send cannot end a run. Archiving the session can.
- What you do not see: the workflow code, the tool calls that start and manage the run, and the result each thread returns to the program. After the run ends and the session is
idle, you can ask the agent to print that workflow word for word and not to start another run. - Where events go:
workflow_run.*arrives on the session event stream, the primary thread's stream. Listing events returns them too. The docs say they do not trigger webhooks.
One line. Progress and the end live in event JSON. An idle session is not a finished workflow.
The created event lists phases
A run is open from workflow_run.created until workflow_run.status_ended. The documented example:
{
"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"
}
name and description are text the model wrote. They can repeat words from your request, or the server can assign the name. phases is always present and can be empty. A phase description can be null. A phase event carries workflow_run_phase_id and not the phase name. Look the name up by id in created.phases. The end event also has phase_started_id, matching the start event's id.
The docs say phases usually run one at a time, in the order of phases, each at most once. The API does not guarantee that. Handle more than one open phase, a phase that is not in the list, and a listed phase that never starts. Every phase that starts also ends before status_ended.
A thread is announced by session.thread_created with the same workflow_run_id. Other threads set that field to null. No field says which phase a thread works in. Threads in one run can share a name. Tell them apart with session_thread_id.
ended.result is the end
The last event is always workflow_run.status_ended:
{
"type": "workflow_run.status_ended",
"id": "sevt_01ghi...",
"workflow_run_id": "wrun_01J8XkN5uT3vHpLqRfWdY2",
"result": { "type": "completed" },
"processed_at": "2026-10-09T14:09:12Z"
}
Documented result.type values. Treat an unrecognized type as an ending you do not know yet:
completed: the program finished. It does not say the work passed. A failed thread, or a thread that could not be created, can still end ascompleted. Failed work is in each thread's events.stopped: the agent stopped the run, or the session was archived. The event does not say which.errorwitherror.typetimeout_error: the lifetime ran out. The default is 24 hours. The agent can set a shorter one. No event reports that value. A pause does not stop the lifetime.program_error: the workflow code failed, broke a rule, or a thread failed and the program let that end the run.thread_limit_error: the run started more than 1,000 agents over its life. The server may retry a failed agent on a new thread, so the thread count can pass 1,000.unknown_error: the server could not continue, or another server limit was hit.
An error result looks like {"type":"error","error":{"type":"timeout_error","message":"..."}}. message is safe to log. An unrecognized error.type is still an error.
workflow_run.error arrives before status_ended when a run ends in error, or when a start is refused. A null workflow_run_id means no run was created. It is not the end signal. A model, MCP server, credential, or billing failure puts session.error on the failing thread. That alone does not end the run. If the thread then fails and the program lets that end the run, the result is program_error.
When the job is actually done
The docs define done as two facts. Missing either one leaves the job open:
- Every run you saw created has
workflow_run.status_ended. - After that, a
session.status_idlearrives withstop_reasonend_turn, and your own interrupt did not cause it. After an interrupt, count only an idle that follows your nextuser.messageoruser.define_outcome.
While a run is running, the session can stay running even when no thread is working. When no thread is working and one waits on your client, the session goes idle with requires_action. Idle by itself is not done. At the budget, the session can go idle with budget_reached while the run is still open. The agent can start another run after it reads a result, so check again.
If you defined an outcome, no evaluation starts while a run is open. It can start on the turn where the agent reads the result. When retries_exhausted arrives, a run may still be running. If a run ended and the agent has not read the result, the server starts another turn and the session goes running again. Wait for the next idle.
Lifetime, concurrency, and the thread cap
- Agents a workflow starts over one run's life: 1,000. The next request does not start another agent, and the run ends with
thread_limit_error. - Lifetime: 24 hours by default, or a shorter value the agent sets. Time spent waiting on your client counts. The end reason is
timeout_error. - Runs open at once in a session: 10 by default. Idle runs count. The next start is refused, the tool call gets an error, and you get
workflow_run.errorwitherror.typemax_workflow_runs_error.
Archiving one of the run's threads while the run is open returns 400 with error.details.error_code workflow_run_open. The server archives threads by the end of the run. You cannot stop one thread by its id.
How to check it in JSON Toolbox
This local contract checks the event shape. completed is valid here, and the business check of each thread still comes after:
{
"type": "object",
"properties": {
"type": { "const": "workflow_run.status_ended" },
"workflow_run_id": { "type": "string", "pattern": "^wrun_" },
"result": {
"type": "object",
"properties": { "type": { "type": "string", "enum": ["completed", "stopped", "error"] } },
"required": ["type"],
"additionalProperties": true
}
},
"required": ["type", "workflow_run_id", "result"],
"additionalProperties": true
}
{
"ok_completed": {"type": "workflow_run.status_ended", "workflow_run_id": "wrun_01J8XkN5uT3vHpLqRfWdY2", "result": {"type": "completed"}},
"ok_timeout": {"type": "workflow_run.status_ended", "workflow_run_id": "wrun_01J8XkN5uT3vHpLqRfWdY2", "result": {"type": "error", "error": {"type": "timeout_error", "message": "lifetime reached"}}},
"not_ended": {"type": "workflow_run.error", "workflow_run_id": null},
"missing_id": {"type": "workflow_run.status_ended", "result": {"type": "completed"}}
}
- Paste
created,status_ended, and the four samples into JSON format and confirm they parse. - Use JSON β Schema on
ok_completed, then add apatternonworkflow_run_idand anenumonresult.type. - Run
not_endedandmissing_idthrough Schema validator. Both should fail. Bothok_samples should pass. - After
completedpasses the shape, look for failed threads. A shape pass is not a passed review. Tool inputs on a thread go through Function Calling.
The data stays in the browser. Get an object that rejects a missing workflow_run_id and the wrong event type before you subscribe to the session stream.
FAQ
Does result completed mean all 300 contracts were reviewed?
No. It only means the program finished. A failed thread can still end as completed. Check each thread's events.
Can I take the result when the session goes idle?
No. At the budget the session can go idle while the run stays open. Wait for every run's status_ended, then an idle whose stop_reason is end_turn.
Is workflow_run.error the end?
No. A null workflow_run_id means no run was created. Otherwise wait for status_ended.
Do these events fire webhooks?
No. They arrive on the session event stream. Thread messages stay on the thread's own stream.
Is this the same note as ChatGPT MCP Events?
No. That note is two schemas on a plugin event, in ChatGPT MCP Events. This one is Claude's workflow_run events.
Summary
As of 2026-10-13, the Claude dynamic-workflow release note is dated 2026-10-09. The beta header is managed-agents-2026-04-01. Turn it on with multiagent.type multiagent_20261001 and workflows.type enabled. Read progress from phases on workflow_run.created. Treat only result on workflow_run.status_ended as the end. completed is not a passed review. Done also needs a session idle with end_turn. One run starts at most 1,000 agents, lives 24 hours by default, and a session holds at most 10 open runs. Reject an end event that lacks workflow_run_id, then keep completed separate from a failed thread.