村里办喜酒,三十几桌流水席。掌总的是一位老支客,大家叫他总管。
天没亮,总管先摊开一本旧账,照着上头的工种把人点齐:洗菜的、剖鱼的、掌勺的、蒸笼的、上菜的、洗碗的。每一组他都不按人名叫,而是按「工种 + 今天的席号」记——「东头席·掌勺组」「东头席·上菜组」。这样的好处是:明年这家二儿子再办,总管照着同一本账,还能把同样名头的组一字不差地重新点出来。账是死的,可照搬的。
点齐了,总管把活儿撒下去:洗菜组、剖鱼组、备料组同时开工,各干各的,谁也不等谁;可上菜组得等掌勺组的菜起锅才能动,蒸笼组得等备料组把料码好——有的并排走,有的得排着队。
每一组都发一本自己的流水账,记着这组今天做了哪几样、做到哪一步;记完往哪儿放也有讲究——按今天的席号编了号的竹篮,一组一篮,各记各的,绝不混到一处。回头哪道菜出了岔子,翻那一篮就清楚。
总管不下灶,他只盯着场子。哪道菜临时不做了,他一句话就把那道菜从某组的单子上划掉;哪盘菜砸了锅,他原样退回去叫那组重做;哪组实在不顶用,他干脆把整组遣散,另点人手。撤一道、退一回、散一组,都单独点名,动不着别组。
有一条规矩雷打不动:组长再能干,也不许自封总管、再去支使满场的人马。掌总的名分,只此一人,到组长这一层就打住,不许往下再生一层。
灶上热火朝天的时候,门口立着块小牌,过路的扫一眼就知道:此刻有几组在灶上忙着。
可你要是真把两年的席摆到一处尝,会发现一桩怪事:总管那本分工的账,两年分毫不差;锅里的味道,却年年不同——厨子到底是凭手感下盐,同一道菜,这一锅和那一锅,从来不会一模一样。能照搬的,从来只是那本「谁干什么、归谁记账、谁能撤谁」的台账;真到了锅边,还是活的。
这位老支客掌总的整套章法——照工种加席号点人、各组并排或排队开工、各记各的账、可逐组撤换却不许自立山头——放进 Claude 的世界里,就是这一页要讲的:确定性的多代理编排,也就是 Workflows。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Deterministic orchestration (Workflows)
Multi-Agent · 多智能体
中文速览 · Quick read
Workflows is a deterministic multi-agent orchestration mechanism — a background task type that coordinates multiple Claude subagents running in parallel or in sequence. It is registered in getAllTasks() next to LocalAgentTask, LocalShellTask, and friends, but only when the WORKFLOW_SCRIPTS feature flag is on (claude-code/src/tasks.ts:9). Its task-type identifier is 'local_workflow' (claude-code/src/Task.ts:11) and its task-ID prefix is 'w' (claude-code/src/Task.ts:84). The invocation surface is WorkflowTool, itself lazily loaded behind the same flag — and the act of loading it first calls initBundledWorkflows() to register the bundled workflow scripts (claude-code/src/tools.ts:129, claude-code/src/tools.ts:131).
The word "deterministic" here is narrow and worth pinning down at the outset: it means the agent identities and assignment plan are reproducible, not that the agents' outputs are. Agent IDs are derived from role + context — formatAgentId(TEAM_LEAD_NAME, finalTeamName) mints a stable ID for a lead from its name and team (claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:146) — so the same orchestration can reconstitute the same addressable agents. Each agent still calls a non-deterministic model, so the content varies run to run. The steward's roster is fixed; the flavor in the pot is not.
(Two of the core types are empty stubs in the leaked source — LocalWorkflowTaskState (claude-code/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts:2) and SdkWorkflowProgress (claude-code/src/types/tools.ts:7) — because the orchestrator body is ant-only and not fully present. This page therefore reads the visible seams: registration, gating, transcript routing, progress, and controls.)
How it works
Invoking WorkflowTool registers a background task of type 'local_workflow' (claude-code/src/Task.ts:11), tracked in AppState alongside every other task the harness manages (claude-code/src/tasks.ts:9).
That task spawns multiple subagents through the standard agent path, with one extra parameter. Each subagent's transcript is routed into a grouping subdirectory — workflows/<runId> — via the transcriptSubdir argument (claude-code/src/tools/AgentTool/runAgent.ts:322), applied by setAgentTranscriptSubdir(agentId, transcriptSubdir) (claude-code/src/tools/AgentTool/runAgent.ts:351). This is the "one basket per group, one record per basket" rule: every agent's history is filed separately under the run.
The controllers for those agents live on LocalWorkflowTaskState as a Map, not a Record — deliberately. The source spells out why next to the identical session-hooks pattern: with a Record + spread, each of N parallel spawns would cost O(N) to copy the growing container (O(N²) total) and fire every store listener; with a Map, .set() is O(1) and returning prev unchanged lets the store's Object.is(next, prev) check skip listener notification entirely (claude-code/src/utils/hooks/sessionHooks.ts:48, claude-code/src/utils/hooks/sessionHooks.ts:54, claude-code/src/utils/hooks/sessionHooks.ts:60). This is what keeps high-concurrency fan-out cheap.
Progress flows back through emitTaskProgress() (claude-code/src/utils/task/sdkProgress.ts:10), which carries an optional workflowProgress: SdkWorkflowProgress[] payload (claude-code/src/utils/task/sdkProgress.ts:19) onto a task_progress SDK event as workflow_progress (claude-code/src/utils/task/sdkProgress.ts:34).
The operator gets per-agent surgical control. The background tasks dialog wires up killWorkflowTask, skipWorkflowAgent, and retryWorkflowAgent from the workflow task module (claude-code/src/components/tasks/BackgroundTasksDialog.tsx:111, claude-code/src/components/tasks/BackgroundTasksDialog.tsx:112, claude-code/src/components/tasks/BackgroundTasksDialog.tsx:113) — skip a step, retry a botched one, or kill the whole run, each independent of the other agents. The status pill reads "1 background workflow" / "N background workflows" so the run is visible at a glance (claude-code/src/tasks/pillLabel.ts:57, claude-code/src/tasks/pillLabel.ts:58).
Two guardrails bound the mechanism. First, no recursion: WORKFLOW_TOOL_NAME is in ALL_AGENT_DISALLOWED_TOOLS, with the comment "Prevent recursive workflow execution inside subagents," so a spawned agent cannot itself launch a workflow (claude-code/src/constants/tools.ts:44, claude-code/src/constants/tools.ts:45) — the group leader may not crown himself steward. Second, permission integrity: WorkflowTool is allowlisted in the auto-mode classifier as safe, precisely because "subagents go through canUseTool individually" — the orchestrator is trusted only because each agent it spawns is still permission-checked on its own (claude-code/src/utils/permissions/classifierDecision.ts:84, claude-code/src/utils/permissions/classifierDecision.ts:85).
Why it matters
Because agent IDs are derived from role + team rather than randomly assigned (claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:146), the same orchestration plan reconstitutes the same addressable agents — they can be resumed, targeted, and debugged by name — even though the model's content differs each run. That is the honest meaning of "deterministic" here: the 台账 (ledger) is fixed; the output is alive.
Routing each agent into its own workflows/<runId> transcript subdir (claude-code/src/tools/AgentTool/runAgent.ts:322, claude-code/src/tools/AgentTool/runAgent.ts:351) means parallel agents never trample each other's history, and any one agent's run can be read back afterward.
The Map-based controller store (claude-code/src/utils/hooks/sessionHooks.ts:48) is what makes spawning many agents in a single synchronous tick affordable instead of quadratic.
The recursion ban (claude-code/src/constants/tools.ts:45) prevents a workflows-spawning-workflows fan-out explosion, while per-agent skip/retry/kill (claude-code/src/components/tasks/BackgroundTasksDialog.tsx:111) gives the operator a scalpel instead of an all-or-nothing switch. And because every subagent is still individually permission-gated (claude-code/src/utils/permissions/classifierDecision.ts:85), "allowing the orchestrator" never means "allowing whatever the orchestrator decides to do."
Read the source
Start at the registry: getAllTasks() in claude-code/src/tasks.ts:9 shows LocalWorkflowTask gated on WORKFLOW_SCRIPTS. The task identity lives in claude-code/src/Task.ts:11 (type) and claude-code/src/Task.ts:84 (ID prefix 'w'). The invocation tool and its bundled-workflow init: claude-code/src/tools.ts:129 and claude-code/src/tools.ts:131. The spawn-time transcript grouping: claude-code/src/tools/AgentTool/runAgent.ts:322 and claude-code/src/tools/AgentTool/runAgent.ts:351. Determinism of IDs: claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:145. The Map-vs-Record concurrency rationale: claude-code/src/utils/hooks/sessionHooks.ts:48. Progress emission: claude-code/src/utils/task/sdkProgress.ts:10. UI controls: claude-code/src/components/tasks/BackgroundTasksDialog.tsx:111. Recursion guard: claude-code/src/constants/tools.ts:44. Permission allowlisting: claude-code/src/utils/permissions/classifierDecision.ts:84. The pill label: claude-code/src/tasks/pillLabel.ts:57. Finally, note the empty stubs — claude-code/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts:2 and claude-code/src/types/tools.ts:7 — which mark where the ant-only core was elided from the leak.
classDiagram
class WorkflowTool {
<<gated WORKFLOW_SCRIPTS>>
+initBundledWorkflows()
}
class LocalWorkflowTask {
+type local_workflow
+idPrefix w
+controllers ControllerMap
+killWorkflowTask()
+skipWorkflowAgent()
+retryWorkflowAgent()
}
class Subagent {
+id derivedFromRoleTeam
+transcriptSubdir workflowsRunId
+canUseTool perAgent
}
WorkflowTool ..> LocalWorkflowTask : registers task
LocalWorkflowTask "1" *-- "N" Subagent : spawns
Subagent ..> WorkflowTool : blocked no recursion 读法:WorkflowTool 注册类型 local_workflow(id 前缀 w),仅在
WORKFLOW_SCRIPTS 开启时(tasks.ts:9,Task.ts:11,84)。每个子代理 ID 由 role+team
派生可复现(formatAgentId,TeamCreateTool.ts:146),transcript 落到
workflows/<runId>(runAgent.ts:322,351)。controllers 用 Map 非 Record 保证 O(1)
并发写入(sessionHooks.ts:48);可逐个 skip/retry/kill(BackgroundTasksDialog.tsx:111-113)。
两道护栏:禁递归(constants/tools.ts:45)、子代理仍各自过权限(classifierDecision.ts:84)。
总管掌总,不下灶。点开跑工作流:按「工种 + 席号」点齐子代理——
ID 由 formatAgentId(role, team) 派生,可复现;各组写各组的
workflows/<runId> 流水账;洗菜/剖鱼并排开工,上菜得等掌勺起锅。
每组都能单独 skip / retry / kill。再点同样计划重跑:ID 一字不差,锅里的味道却年年不同。
点「开跑工作流」,看总管按台账点人。
「确定性」很窄:可复现的是身份与分工(ID 由 role+team 派生,TeamCreateTool.ts:146),不是 输出——每个子代理仍调一个非确定的模型。各代理的 transcript 落到 workflows/<runId> 子目录(runAgent.ts:322,351),互不踩踏;子代理被禁止再开工作流(ALL_AGENT_DISALLOWED_TOOLS, constants/tools.ts:44-45),组长不许自封总管;且每个子代理仍各自过 canUseTool 权限校验 (classifierDecision.ts:84-85)。
Workflows is a deterministic multi-agent orchestration mechanism — a background task type that coordinates multiple Claude subagents running in parallel or in sequence. It is registered in getAllTasks() next to LocalAgentTask, LocalShellTask, and friends, but only when the WORKFLOW_SCRIPTS feature flag is on (claude-code/src/tasks.ts:9). Its task-type identifier is 'local_workflow' (claude-code/src/Task.ts:11) and its task-ID prefix is 'w' (claude-code/src/Task.ts:84). The invocation surface is WorkflowTool, itself lazily loaded behind the same flag — and the act of loading it first calls initBundledWorkflows() to register the bundled workflow scripts (claude-code/src/tools.ts:129, claude-code/src/tools.ts:131).
The word "deterministic" here is narrow and worth pinning down at the outset: it means the agent identities and assignment plan are reproducible, not that the agents' outputs are. Agent IDs are derived from role + context — formatAgentId(TEAM_LEAD_NAME, finalTeamName) mints a stable ID for a lead from its name and team (claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:146) — so the same orchestration can reconstitute the same addressable agents. Each agent still calls a non-deterministic model, so the content varies run to run. The steward's roster is fixed; the flavor in the pot is not.
(Two of the core types are empty stubs in the leaked source — LocalWorkflowTaskState (claude-code/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts:2) and SdkWorkflowProgress (claude-code/src/types/tools.ts:7) — because the orchestrator body is ant-only and not fully present. This page therefore reads the visible seams: registration, gating, transcript routing, progress, and controls.)
How it works
Invoking WorkflowTool registers a background task of type 'local_workflow' (claude-code/src/Task.ts:11), tracked in AppState alongside every other task the harness manages (claude-code/src/tasks.ts:9).
That task spawns multiple subagents through the standard agent path, with one extra parameter. Each subagent's transcript is routed into a grouping subdirectory — workflows/<runId> — via the transcriptSubdir argument (claude-code/src/tools/AgentTool/runAgent.ts:322), applied by setAgentTranscriptSubdir(agentId, transcriptSubdir) (claude-code/src/tools/AgentTool/runAgent.ts:351). This is the "one basket per group, one record per basket" rule: every agent's history is filed separately under the run.
The controllers for those agents live on LocalWorkflowTaskState as a Map, not a Record — deliberately. The source spells out why next to the identical session-hooks pattern: with a Record + spread, each of N parallel spawns would cost O(N) to copy the growing container (O(N²) total) and fire every store listener; with a Map, .set() is O(1) and returning prev unchanged lets the store's Object.is(next, prev) check skip listener notification entirely (claude-code/src/utils/hooks/sessionHooks.ts:48, claude-code/src/utils/hooks/sessionHooks.ts:54, claude-code/src/utils/hooks/sessionHooks.ts:60). This is what keeps high-concurrency fan-out cheap.
Progress flows back through emitTaskProgress() (claude-code/src/utils/task/sdkProgress.ts:10), which carries an optional workflowProgress: SdkWorkflowProgress[] payload (claude-code/src/utils/task/sdkProgress.ts:19) onto a task_progress SDK event as workflow_progress (claude-code/src/utils/task/sdkProgress.ts:34).
The operator gets per-agent surgical control. The background tasks dialog wires up killWorkflowTask, skipWorkflowAgent, and retryWorkflowAgent from the workflow task module (claude-code/src/components/tasks/BackgroundTasksDialog.tsx:111, claude-code/src/components/tasks/BackgroundTasksDialog.tsx:112, claude-code/src/components/tasks/BackgroundTasksDialog.tsx:113) — skip a step, retry a botched one, or kill the whole run, each independent of the other agents. The status pill reads "1 background workflow" / "N background workflows" so the run is visible at a glance (claude-code/src/tasks/pillLabel.ts:57, claude-code/src/tasks/pillLabel.ts:58).
Two guardrails bound the mechanism. First, no recursion: WORKFLOW_TOOL_NAME is in ALL_AGENT_DISALLOWED_TOOLS, with the comment "Prevent recursive workflow execution inside subagents," so a spawned agent cannot itself launch a workflow (claude-code/src/constants/tools.ts:44, claude-code/src/constants/tools.ts:45) — the group leader may not crown himself steward. Second, permission integrity: WorkflowTool is allowlisted in the auto-mode classifier as safe, precisely because "subagents go through canUseTool individually" — the orchestrator is trusted only because each agent it spawns is still permission-checked on its own (claude-code/src/utils/permissions/classifierDecision.ts:84, claude-code/src/utils/permissions/classifierDecision.ts:85).
Why it matters
Reproducibility without false determinism. Because agent IDs are derived from role + team rather than randomly assigned (claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:146), the same orchestration plan reconstitutes the same addressable agents — they can be resumed, targeted, and debugged by name — even though the model's content differs each run. That is the honest meaning of "deterministic" here: the 台账 (ledger) is fixed; the output is alive.
Isolation that survives parallelism. Routing each agent into its own workflows/<runId> transcript subdir (claude-code/src/tools/AgentTool/runAgent.ts:322, claude-code/src/tools/AgentTool/runAgent.ts:351) means parallel agents never trample each other's history, and any one agent's run can be read back afterward.
Concurrency-safe bookkeeping. The Map-based controller store (claude-code/src/utils/hooks/sessionHooks.ts:48) is what makes spawning many agents in a single synchronous tick affordable instead of quadratic.
Bounded blast radius and surgical control. The recursion ban (claude-code/src/constants/tools.ts:45) prevents a workflows-spawning-workflows fan-out explosion, while per-agent skip/retry/kill (claude-code/src/components/tasks/BackgroundTasksDialog.tsx:111) gives the operator a scalpel instead of an all-or-nothing switch. And because every subagent is still individually permission-gated (claude-code/src/utils/permissions/classifierDecision.ts:85), "allowing the orchestrator" never means "allowing whatever the orchestrator decides to do."
Read the source
Start at the registry: getAllTasks() in claude-code/src/tasks.ts:9 shows LocalWorkflowTask gated on WORKFLOW_SCRIPTS. The task identity lives in claude-code/src/Task.ts:11 (type) and claude-code/src/Task.ts:84 (ID prefix 'w'). The invocation tool and its bundled-workflow init: claude-code/src/tools.ts:129 and claude-code/src/tools.ts:131. The spawn-time transcript grouping: claude-code/src/tools/AgentTool/runAgent.ts:322 and claude-code/src/tools/AgentTool/runAgent.ts:351. Determinism of IDs: claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:145. The Map-vs-Record concurrency rationale: claude-code/src/utils/hooks/sessionHooks.ts:48. Progress emission: claude-code/src/utils/task/sdkProgress.ts:10. UI controls: claude-code/src/components/tasks/BackgroundTasksDialog.tsx:111. Recursion guard: claude-code/src/constants/tools.ts:44. Permission allowlisting: claude-code/src/utils/permissions/classifierDecision.ts:84. The pill label: claude-code/src/tasks/pillLabel.ts:57. Finally, note the empty stubs — claude-code/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts:2 and claude-code/src/types/tools.ts:7 — which mark where the ant-only core was elided from the leak.
来源 · Source citations
- [1]
claude-code/src/tasks.ts:9 - [2]
claude-code/src/Task.ts:11 - [3]
claude-code/src/Task.ts:84 - [4]
claude-code/src/tools.ts:129 - [5]
claude-code/src/tools.ts:131 - [6]
claude-code/src/constants/tools.ts:44 - [7]
claude-code/src/constants/tools.ts:45 - [8]
claude-code/src/tools/AgentTool/runAgent.ts:322 - [9]
claude-code/src/tools/AgentTool/runAgent.ts:323 - [10]
claude-code/src/tools/AgentTool/runAgent.ts:351 - [11]
claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:145 - [12]
claude-code/src/tools/TeamCreateTool/TeamCreateTool.ts:146 - [13]
claude-code/src/utils/hooks/sessionHooks.ts:48 - [14]
claude-code/src/utils/hooks/sessionHooks.ts:54 - [15]
claude-code/src/utils/hooks/sessionHooks.ts:60 - [16]
claude-code/src/utils/task/sdkProgress.ts:10 - [17]
claude-code/src/utils/task/sdkProgress.ts:19 - [18]
claude-code/src/utils/task/sdkProgress.ts:34 - [19]
claude-code/src/components/tasks/BackgroundTasksDialog.tsx:111 - [20]
claude-code/src/components/tasks/BackgroundTasksDialog.tsx:112 - [21]
claude-code/src/components/tasks/BackgroundTasksDialog.tsx:113 - [22]
claude-code/src/utils/permissions/classifierDecision.ts:84 - [23]
claude-code/src/utils/permissions/classifierDecision.ts:85 - [24]
claude-code/src/tasks/pillLabel.ts:57 - [25]
claude-code/src/tasks/pillLabel.ts:58 - [26]
claude-code/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts:2 - [27]
claude-code/src/types/tools.ts:7