昨天那篇的結論是:plugin 不是內容包,是系統模組,是 runtime 擴充。
但我寫完之後一直覺得那個結論有點虛。因為「runtime 擴充」聽起來很厲害,可是你如果追問一句——
所以 plugin 拿得到、skill 拿不到的,具體是哪些東西?
我昨天其實答不太出來。
今天可以了。因為有一項能力,OpenClaw 明確只開放給 plugin,而且它的份量大到我認為值得用兩天來講:
編排一連串跨越多個 agent 的工作,而且進度要活過整個系統重啟。
這件事為什麼難?想像你派了五隻子代理出去:一隻在改測試、一隻在查文件、一隻在等外部 API、兩隻還在排隊。這時候有人問你一句:
這件事現在到底做到哪了?
誰回答得出來?
不是那五隻——它們各自只知道自己那一塊。也不是主 agent 的上下文——那裡面塞滿了五份回報,而且隨時會被壓縮掉。就算你把上下文管得再好,只要行程重啟,進度一樣會消失。
所以這個問題的答案不能是「記在腦袋裡」,必須是記在某個地方。
OpenClaw 給這個地方取了名字,叫 Task Flow。它住在 src/tasks/,45 個檔案(不算測試),有自己的 API、自己的 CLI、自己的資料表。
而前 28 天,我一次都沒提過它。今天補上。
官方文件第一句話就把定位講死了:
📄 文件:
docs/automation/taskflow.md
Task Flow is the orchestration layer above background tasks. A flow is a durable
record of multi-step work with its own status, JSON state, revision counter, and
linked task records. Flows survive gateway restarts; individual tasks remain the
unit of detached work.
兩個關鍵詞:orchestration layer(編排層)和 survive gateway restarts(跨重啟存活)。
所以 task 和 flow 的分工是:
文件還直接給了一張決策表,我覺得這是整篇最實用的東西:
| Scenario | Use |
| ----------------------------------------- | ------------------------------------------- |
| Single background job | Plain task |
| Multi-step pipeline driven by plugin code | Task Flow (managed) |
| Detached ACP or subagent spawn | Task Flow (mirrored, created automatically) |
| One-shot reminder | Cron job |
這張表值得念一遍,因為它把四個很容易混在一起的東西分開了。
單發的背景工作,用 task 就好,不需要 flow。
一次性提醒,那是 cron 的事,也不需要 flow。
只有「多步驟、而且由 plugin 程式碼驅動的 pipeline」,才是 flow 的主場。
第三行那個子代理的 case 是自動的,等一下會講——但先記住它是特例,不是主角。
文件給了一個具體例子,我覺得比任何解釋都清楚:
Example: a weekly report flow that (1) gathers data, (2) generates the report,
and (3) delivers it, one background task per step:
Flow: weekly-report
Step 1: gather-data → task created → succeeded
Step 2: generate-report → task created → succeeded
Step 3: deliver → task created → running
一條 flow,三個步驟,每個步驟開一個 task。
這就是 managed 模式。文件對它的描述是:
A managed flow has a controller: plugin code that creates the flow through the
plugin runtime Task Flow API with a goal and a required controller id, then
drives it explicitly.
三個重點:
去翻 source,controllerId 必填這件事是硬性檢查:
📄 原始碼:
src/tasks/task-flow-registry.ts:190-193
function assertControllerId(controllerId?: string | null): string {
const normalized = normalizeOptionalString(controllerId);
if (!normalized) {
throw new Error("Managed flow controllerId is required.");
}
而且我翻遍非測試程式碼,createManagedTaskFlow 只有一個呼叫點:src/plugins/runtime/runtime-taskflow.ts——plugin runtime。
這件事很有意思。第 27、28 天講 skill 和 plugin 的分工,結論是「skill 偏工作能力,plugin 偏系統擴充」。TaskFlow 是這個分工的一個硬證據:
編排多步驟流程的能力,只開放給 plugin,skill 拿不到。
第二種模式,文件講得很坦白:
OpenClaw creates a mirrored one-task flow automatically when a detached ACP or
subagent run starts (session-scoped tasks with deliverable completion). The flow
record mirrors its single backing task - status, goal, and timing - so detached
spawns get a stable flow handle for status and retry surfaces without a controller.
注意兩個地方。
第一,one-task flow——一條 flow 只對應一個 task。它不是在編排多步驟,它就是那一個 task 的鏡子。
第二,without a controller——沒有 controller。這正是它跟 managed 最大的差別。
那它存在的意義是什麼?a stable flow handle for status and retry surfaces:給狀態查詢和重試介面一個穩定的把手。
子代理跑起來了,你想查它、想取消它,總要有個東西可以指。mirrored flow 就是那個東西。
對應的判斷條件在 source 裡:
📄 原始碼:
src/tasks/task-executor.ts:55-63
function isOneTaskFlowEligible(task: TaskRecord): boolean {
if (task.parentFlowId?.trim() || task.scopeKind !== "session") {
return false;
}
if (task.deliveryStatus === "not_applicable") {
return false;
}
return task.runtime === "acp" || task.runtime === "subagent";
}
這也解釋了為什麼 cli 和 cron 不在裡面——你在終端機跑的東西,你自己看得到;單發的排程任務,沒有多步驟要編排。
我一開始看 source 的時候,先看到這個函式,差點把 TaskFlow 整個理解成「子代理專用」。它不是。它是編排層,只是順手幫子代理也開了一份鏡像記錄。
文件的狀態表比我自己從型別推的清楚:
| `waiting` | Managed flow is parked on wait metadata (timer, external event) |
| `blocked` | A step finished without a usable result; `blockedTaskId`/summary say which |
| `lost` | Flow lost its authoritative backing state |
waiting 那一行有個限定詞:Managed flow。
這很合理。mirrored flow 只鏡射一個 task,task 只會跑或不跑,不會「停在那裡等一個計時器或外部事件」。只有被 controller 驅動的多步驟流程,才會有「這一步做完了,要等某個東西才能走下一步」的狀態。
blocked 的定義也比我猜的精確:a step finished without a usable result——某一步跑完了,但沒有產出可用的結果。
跑完了,但沒用。這兩件事必須分開記,而且還要記下是哪一步(blockedTaskId)。這一點明天會整篇展開。
Each write bumps the flow's `revision`; concurrent writers that pass a stale
expected revision get a conflict and must re-read.
這是樂觀鎖的標準做法,但放在這個情境特別值得講。
一條 managed flow 可能同時被三個東西碰:controller 想推進下一步、某個子 task 正好回報完成、維護掃描正在檢查有沒有過期。
沒有 revision,後寫的直接蓋掉先寫的,你就會得到一條狀態錯亂的流程。有了 revision,拿著舊版本號來寫的那個會被擋下來,必須重讀再重試。
這是我最喜歡的一點,也是 flow 跟 OpenClaw 裡其他流程機制最大的差別:
# List active and recent flows
openclaw tasks flow list [--status <status>] [--json]
# Show details for a specific flow
openclaw tasks flow show <lookup> [--json]
# Cancel a running flow and its active tasks
openclaw tasks flow cancel <lookup>
list 會給你 sync mode、status、revision、controller、task counts。
資料就在 ~/.openclaw/state/openclaw.sqlite 的 flow_runs 表裡。
queue、lane、pruning 那些,你只能從 diagnostic log 的蛛絲馬跡去推,推完還不一定對。flow 不用,它有一張表、一組指令,你可以直接看。
這個差別比聽起來重要,而且它其實指向一件我想了很久的事。
這是今天我最想講的一件事。
第 14 天我說「OpenClaw 像工作流引擎」,那句話沒有錯,但它是觀察者的語言。
我看到排隊、分流、等待、回收,於是歸納出一個形狀。那個形狀是真的,但它是很多機制交互作用之後浮現出來的,沒有任何一個模組負責它。
TaskFlow 不一樣。它是設計者的語言:有人明確地說「我需要一個能跨重啟存活的多步驟編排記錄」,然後建了一張表、一組 API、一組 CLI。
一個系統裡這兩種東西都會有。但分不清楚的話,你會在讀 code 的時候一直找不到那個「負責流程的模組」——因為有些流程感根本不來自模組,而來自很多小機制的疊加。
文件最後有一段我覺得很值得抄,它在講怎麼做一個可靠的排程工作流:
1. Use Scheduled Tasks for timing.
2. Use a persistent cron session when the workflow should build on prior context.
3. Use Lobster for deterministic steps, approval gates, and resume tokens.
4. Use Task Flow to track the multi-step run across child tasks, waits, retries,
and gateway restarts.
這四層各管各的:
第 17 天我講 cron 的時候,把它當成一種特殊 turn 在看。現在把它放回這張表,才看清楚它只負責觸發那一層。
很多人做自動化失敗,是因為想用一層解決四層的事——用 cron 硬幹全部,然後在裡面塞重試、塞狀態、塞條件判斷。
我一開始誤判了它的地位,但重新看之後反而更欣賞它。
它解決的是一個很實際的問題:子代理跑起來之後,使用者想查狀態、想取消,得有東西可以指。如果只有 managed flow,那子代理就得自己實作一個 controller,或者查詢介面要為子代理特別寫一條路。
所以 OpenClaw 選了第三條路:免費送一份鏡像記錄,讓所有查詢和取消的介面只要認得 flow 就好,不用管底下是編排出來的還是鏡射出來的。
一個統一的把手,兩種來源。這是很划算的抽象。
最後那一條其實是所有「持久化」設計的共同代價。你一旦決定把東西寫進資料庫,就要負責它的清理、對帳和修復——明天整篇都在講這件事。
waiting 是 managed 專屬,因為只有多步驟流程才會停下來等revision 讓並發寫入產生衝突而不是覆蓋,寫失敗的必須重讀openclaw tasks flow list 直接看到今天講的是流程順利時的樣子。
但文件裡有一個狀態我今天只點了一下,它的定義是「某一步跑完了,但沒有產出可用的結果」。
跑完了,卻沒有用。明天整篇就從這個狀態開始:
成功不等於有進展,TaskFlow 怎麼處理卡住與失聯