Skip to Content
核心概念複合指令Commands 總覽

自訂資料表 Commands

Command 是一段已儲存、具名、掛在 tag 上、可帶參數的程式:它在同一個資料庫 transaction 內寫入多張自訂資料表,並且以呼叫者本人的權限執行——沒有 definer rights、沒有 service account、也沒有任何權限提升。所有步驟要嘛全部生效,要嘛全部不生效。

模組裡沒有別的東西能提供這個保證。批次操作掛在單一資料表下,所以「主檔一列 + 明細數列」是兩個各自獨立的 transaction;用兩次 record 呼叫寫入的用戶端,必須自己發明第二次呼叫失敗時的補償邏輯。Command 用一段你只寫一次、命名、再交給表單、trigger 或 AI agent 使用的伺服器端程式取代那件事。

一個 command 擁有什麼

屬性契約
Scopechatroom、department、company 三選一。company 掛載點同樣帶有明確的 {company_id} path 區段。
name1–64 字元,在該 scope 的活指令之間唯一(自 2026-07-28 起,刪除的指令立即釋放名字)。撞名為 409 {"error":"command_name_conflict","name":…}
description最多 1024 字元(2026-07-30 由 512 放寬)。它同時是 AI agent 看到的工具說明。
agent_enabled布林值,預設 true。設為 false 會把 command 從 agent 工具清單中移除,REST 完全不受影響。
tag_id必填。每一張被引用的資料表都必須目前就掛在同一個 scope-local tag 上。
definition具型別的 inputs 與有序程式——版本 1版本 2。編譯後必須至少引用一張資料表。
dependency_contract伺服器建立且唯讀。用戶端送來的值會被丟棄並重建。

建立是 POST .../commands,取代是完整的 PUT(永遠不是 PATCH),DELETE 是軟刪除。完整清單見 commands 參考頁

Tag 邊界在每個階段都會生效

tag_id 在 create 與 update 時必填,而且每次執行都會連同 row lock 重新檢查。它在讀取時也會檢查:當 tag 不再涵蓋全部被引用的資料表時,該 command 會直接消失——從 list/get 消失,也從 agent 工具清單消失,而且在有人真的去執行它以前,任何地方都不會出現錯誤。

  • tag_id422 {"error":"command_tag_required"}
  • Tag 不存在 → 404 Tag not found in this scope
  • 被引用的資料表不在 tag 內 → 422 {"error":"command_tables_outside_tag","tag_id":…,"table_ids":[…]}

Tag 與 manager 檢查適用於遞移可達的資料表集合,不只是你在 step 裡寫出來的那幾張。編譯完成後,伺服器會針對整份 dependency contract 再跑一次這兩項檢查,所以你從未寫下的 link 目標表、或 computed column 的來源表,也必須由你 manage、並且掛在同一個 tag 上。

Dependency contract 上限為 20 張資料表。編譯時超過就是 422 {"error":"query_limit_exceeded","phase":"author","message":"Command dependency table limit exceeded"}

一個 command 至少要引用一張資料表

POSTPUT 都會檢查編譯後的相依集合,空集合直接拒絕:

{ "detail": { "error": "command_references_no_table", "message": "a command must reference at least one custom table in this scope" } }

狀態碼是 422,而且這是補洞而不是加規則。Command 介面上的每一道權限閘門都在「被引用的資料表集合」上迭代,集合是空的就讓每一道都空虛地成立:只有 let、只有 callback、或完全沒有 step 的定義,過去任何租戶使用者都能以 201 建出來,然後讀它、改它、刪它、執行它、讀它的執行快照,並透過它的 callback 帶著 invoker context 對外送資料。

現在這些閘門在空集合上 fail closed,所以 2026-07-31 以前建立的資料列行為變成這樣:

路由已存下的零資料表 command 得到
GET .../commands從清單消失,對任何呼叫者皆然,包含公司管理員
GETPUT/execute/query、兩條 /executions 路由404 Command not found
DELETE仍然可用——manage 探測失敗後改用 command 自身 scope 的權威(聊天室建立者/管理員、部門管理員、公司管理員)
IaC apply 的 create/update 分支applied: falseerrors[] 內容為 command '<ref>' references no table: a command must operate on at least one custom table in this scope
IaC apply 的 delete 分支在閘門之前就返回,所以仍然刪得掉

資料列還活著時名字就一直被占著,所以殭屍列會卡住同名重建,直到有人刪掉它。刪除是唯一的修法——PUT404,定義永遠改不回合法狀態。

授權:三道不同的門檻

Command 不會放寬任何人的存取權。每個介面對「被引用的資料表」問的是不同的問題。

介面對被引用資料表的要求失敗
Create / update / delete每一張都要是 manager。僅 delete 例外:引用的表已解析不到時,改看 command 自身 scope 的權威(見修改與刪除404 Referenced table not found in this scope
List / get / executions每一張都要能 read統一的 404 Command not found
Execute / query先要能 read 才看得到,接著逐個 action 套用呼叫者本人的 ACL 與 SCP 判定該 action 本身的 ACL/SCP 錯誤

Authoring 的 404 刻意不列舉:資料表不存在、不在 scope 內、以及你不是 manager,三種情形回同一個 body。不要做出宣稱其中一種原因的 UI。讀取端的 404 是同一個想法的另一面——command 絕不會變成「你看不到的資料表是否存在」的探測器,所以同一個聊天室裡的兩個人看到不同的 command 清單是正常的。

當呼叫者能 read 但無法 manage 全部被引用的資料表時,回應仍會回傳,但 callbacks: []triggers: [],並帶 lifecycle_redacted: true。空的 callbacks 陣列不代表「沒有設定 callback」——請先看這個旗標。

執行與查詢時,兩條通道解析部門資料表權限的方式不同,同一支 command 可能在一邊成功、在另一邊 403

通道部門資料表權限來自
人工 REST /execute/query只看執行者本人——所有授予他這張表的聊天室的聯集。不綁任何 acting room。
Agent 服務通道與資料表 trigger伺服器解析出的 acting room,作為硬性上限——與 agent 的資料表工具一致。

綁在某聊天室的 agent command 觸及一張該 acting room 從未被授權的部門資料表時,現在會以 403 Insert access not granted for this table.403 Edit access not granted for this table.403 Read access not granted for this table. 失敗。過去它會借用執行者其他聊天室的授權,動到 acting room 根本看不到的資料。

Command 路由直接拒絕 API key 認證。request.state.auth_method 必須是 jwt,否則回 403 Custom-table command routes require JWT authentication。程式對程式的存取請走獨立的 agent service-token 通道,而不是 UserAPIKey

兩個定義版本

definition.version 在兩種共用同一個欄位、但本質上不同的產品之間做選擇。

版本 1版本 2
Step 形狀扁平的 {name, table, action, rows[]}kind 區分:selectletassertinsertupdatedelete
讀取、join、彙總沒有有——每個 select 最多 4 個 join,可 group/having/order
值如何送出逐 step 的寫入後 row只透過宣告的 outputs;steps 只回 metadata
上限≤20 steps,最壞展開 ≤1000≤40 steps,其中寫入 steps ≤20
Query 模式不支援mode: "query"
Runtime 的相依漂移只檢查它實際碰到的欄位與資料表重驗完整 contract,對未解決 drift fail closed

outputs、join、彙總與 query 模式都需要版本 2。混用兩者會在 authoring 時被逐項指名拒絕。版本 1 仍然支援,對於「不需要先查資料的純寫入扇出」來說是比較簡單的選擇。

兩種版本的 dependency contract 都會由支援的 server mutation 維護。一般 column/rule/IaC/purge-repair path 會刷新 selected、mutation 前 valid 且之後仍可編譯的 command;若 mutation 使 command 失效,mutation 優先,command 保持 stale。Multi-table purge/repair probe 依每張 rewritten table 自己的 scope 分組;這種分組是完整的,因為 command contract 採 exact-scope-closed:每個 closure member 都必須通過 command 的精確 scope filter,跨 scope authoring 則以 A referenced table does not exist in this scope 失敗。透過 no-dependent fast path 併發建立的新 command,仍可能依 mutation 前形狀編譯並落成 stale。Trigger write 更嚴格:candidate graph 與所有 selected refresh 都必須成功,否則 trigger、history、IaC state 與 refreshed definitions 一起回滾。原本已 stale 的 command 絕不會被順便修復。見相依漂移

明確寫出的 mode: "write" 在儲存時會被折成「完全不存在這個 key」,所以寫入型定義的序列化結果與功能上線前的定義位元組完全相同。只有 mode: "query" 會原樣保存。不要寫 mode: "write" 然後期待它被回傳。

delivery_reconciliation_ttl_seconds

定義外層在 versionmode 之外還有第二個與版本無關的 key。delivery_reconciliation_ttl_seconds 是選填整數,範圍 60–604,800 秒(超出範圍在 authoring 時就是 422),用來限制 AI agent 通道要重播一次已執行過的呼叫、而不是再寫一次,最長能持續多久。不設定代表採用 900 秒的執行期預設值,而且它與 mode 一樣採 omit-when-default 序列化,因此沒有設定它的定義與功能上線前的定義位元組完全相同,IaC diff 與 dependency contract 摘要都不會變動。版本 1 與版本 2 皆適用,而它對 REST 與 trigger 通道沒有任何作用。完整契約見Agent 派送調解視窗

你拿回來的是編譯後的定義,不是你送出的定義

儲存的定義是伺服器編譯過的。GET 回來的內容與你 POST 出去的 body 本來就會不同:

  • data / match 裡的顯示欄位名稱會變成內部的 col_<hex> key,所以改欄位名稱不會弄壞 command;
  • 版本 1 的資料表參照會被改寫成 table id;
  • 運算式參照會存成正規化的 {"$column":{…}} / {"$metadata":{…}} 節點;
  • mode: "write" 會被丟掉;
  • 用戶端送來的 dependency_contract 會被丟棄並重建。

拿送出的 body 去 diff 回應的用戶端永遠會看到差異。請改為比對編譯後的形式。

Command 的三種執行方式

直接呼叫 REST。 寫入型走 POST .../commands/{command_id}/execute,query 型走 POST .../commands/{command_id}/query。這是表單或前端按鈕使用的通道,細節見執行語意

從資料表 trigger 觸發。 invoke_command action 讓一次列寫入原子性地扇出到多張資料表。該 command 會走完整的 execute pipeline,並以 trigger 的作者created_by)身分執行,絕不是 service account;因此刪除、停用或把該使用者踢出聊天室,會讓這個自動化在每次觸發時都 403。Trigger 不能指向 query 模式的 command,也不能指向會寫入審批控管資料表的 command。它的 idempotency key 由 run id 與 action id 推導而來,所以在當機視窗內的重試會走 replay 而不是重新執行。該 action 的欄位與設定時驗證見觸發器動作型別

從 AI agent 呼叫。 工具組會為每個可見的 command 產生一個工具,名稱為 custom_table_command_{slug}_{id8}——slug 會轉小寫、把非英數字元連續段落收斂成 _、截斷到 40 字元,id8 則是把 command uuid 去掉連字號後的前 8 個字元。參數是每個宣告 input 一個具名 kwarg;query 模式 command 的工具會多一個選填的 cursor。一個 command 只有在 agent_enabledtrue、tag 仍然有效,每一張被引用的資料表都能被當下的使用者或 social client 讀取時,才會變成工具——這就是「為什麼 AI 看不到我的 command」的答案。見 API 工具

agent_enabled 只管這第三條通道。設成 false 之後,command 對任何讀得到它資料表的人仍然完全可用——照樣列得出、讀得到、執行得了——但工具組不會為它產生工具,而且可信 agent 路由在每一次呼叫時都會重讀這個旗標,所以對話進行中翻成 false,已經綁定的工具會立刻以 404 Command not found fail closed,內容與「id 不存在」完全相同。翻回 true 則不需要重新綁定就恢復執行。這個旗標出現之前,要讓 command 離開 agent 的手,只能刪掉它或弄壞它的 tag 綁定。

PUT 是整支取代,而 agent_enabled 預設是 true,所以更新時漏掉這個 key,等於默默地把 command 重新對 agent 打開。每一次必須維持隱藏的 PUT 都要送 agent_enabled: false

同一個狀態碼有兩種錯誤形狀

/execute/query400409422 各自會回兩種 body 之一:該路由自己的區分聯集(cap_exceededidempotency_key_reuseapproval_requirednot_a_query_command……),或 compose 程式的信封 CommandProgramErrorResponse。兩者一直都會出現在網路上,只是第二種沒有被寫進規格,所以依規格產生的用戶端解不出來。現在 schema 明確宣告:/execute 的 400/409/422 與 /query 的 400/409 都是 anyOf[<區域聯集>, CommandProgramErrorResponse]——/query 的 422 本來就一直是信封。

{ "detail": { "error": "query_limit_exceeded", "phase": "execute", "step": "lines", "message": "Select result exceeds max_rows", "retryable": false, "details": { "resource": "relation_rows", "limit": 500, "actual": 501 } } }
欄位契約
error判別欄位。封閉的 28 個錯誤碼列舉;告訴你發生什麼事的是它,不是狀態碼。
phaseauthorexecutestagereleasecallback
step選填,^[a-z0-9_]{1,32}$。只有在錯誤可歸因到單一 step 時才會出現。
path選填,1–256 字元。
message1–512 字元。可以安全顯示;它永遠不含 driver 文字或綁定參數。
retryable一定會輸出,預設 falsetrue 代表真正的暫時性錯誤——command_lock_conflictsource_version_changedsource_link_changedquery_timeout。只重試這些,其餘一律不要重試。
details一定存在,經常是 {}query_limit_exceededrelation_limit_exceededmutation_limit_exceededoutput_limit_exceeded{resource,limit,actual}cardinality_failed{expected,actual}query_timeoutcommand_timeout{limit}

請先判斷 detail.error 是否存在,再看它的值——絕不要用狀態碼分支。同一個 409 既可能是區域聯集的 {"error":"approval_required",…},也可能是信封的 {"error":"command_lock_conflict","retryable":true,…};同一個 422 既可能是版本 1 的 {"error":"command_rule_failed","rule":…},也可能是版本 2 那個沒有 rule 名稱的信封。用狀態碼分支的用戶端,每一對裡一定會處理錯一個。

修改與刪除

PUTDELETE 都會先鎖住 command 這一列,所以檢查與變更之間不會被插隊。有五種衝突值得在編輯器 UI 裡分開呈現。

狀態Body原因
409{"error":"command_scope_unavailable"}無法鎖定 scope 根列——不存在、已刪除,或屬於其他租戶。
409{"error":"pending_staged_executions","staged_change_ids":[…]}該 command 還有 staged 狀態的執行時做 PUTDELETE。請先處理完審批。
409{"error":"command_referenced_by_trigger","command_id":…,"references":[…]}刪除一個仍被有效(或可還原之垃圾桶內)資料表 trigger 引用的 command。
409{"error":"command_mode_trigger_conflict","from_mode","to_mode"}PUT 把被 trigger 引用的 command 在 write 與 query 模式之間翻轉。
409{"error":"approval_trigger_conflict","target_table_id","target_table_name","rule_id"}PUT 讓被 trigger 引用的 command 新增引用了一張審批控管資料表。

最後一項的不對稱是刻意的,也是真正的陷阱。REST PUT 的守衛走的是完整 dependency contract,所以在被 trigger 引用的 command 上就算只加入一個唯讀的控管資料表參照,也是 409。Trigger 端的守衛只走 insert/update/delete 分支,所以設定 trigger 時同樣的唯讀參照是被接受的。同一個錯誤碼,兩組不同的參照集合。

PUT 另外要求你對已儲存定義新 body 所引用的每一張資料表都具有 manager 權限;同名的並行建立則會透過 name namespace lock 序列化成一次成功加一次 409 command_name_conflict

2026-07-28 版變更了兩條生命週期規則,都與「資料表先於 command 消失」有關:

  • **殭屍 command 刪得掉了。**過去刪除一支資料表已被刪掉的 command 會失敗,因為權限檢查要解析那張活表。現在 REST 與 IaC 兩條刪除路徑在表不在時,改以 command 自身 scope 的權威判定(聊天室建立者/管理員、部門管理員、公司管理員,租戶錨定不變);沒有該權威的呼叫者看到的仍是同一顆不可枚舉的 404PUT 刻意仍要求活表:它是整份重編譯,殭屍更新沒有東西可編譯。
  • 刪除的 command 立即釋放名字。名稱唯一性現在只計活列。過去軟刪的 command 會永遠占住名字——與資料表不同,command 沒有還原機制,那個占名保留不到任何東西。先刪再以同名重建是合法順序,包括 tag 通道的 purge 掃掉整個系統 commands 的情況。

延伸閱讀

Last updated on