自訂資料表 Commands
綁定 Command 的機器寫入者新增範圍受限 service credential 與驗證過的 CT 輸出回呼。沿用既有 executor 與目前來源檢查,不授予原始資料表權限;回呼卡片為特權、僅供參考。
Command 是一段已儲存、具名、掛在 tag 上、可帶參數的程式:它在同一個資料庫 transaction 內寫入多張自訂資料表,並且預設以呼叫者本人的權限執行——沒有隱含權限提升;選擇採用定義者權限的 definition 則改用它自己宣告的資料表 ceiling 執行。所有步驟要嘛全部生效,要嘛全部不生效。
Command 提供可重用的程式,與有界多表匯入 v2 圖及暫存世代啟用不同。批次操作掛在單一資料表下,所以「主檔一列 + 明細數列」是兩個各自獨立的 transaction;用兩次 record 呼叫寫入的用戶端,必須自己發明第二次呼叫失敗時的補償邏輯。Command 用一段你只寫一次、命名、再交給表單、trigger 或 AI agent 使用的伺服器端程式取代那件事。
一個 command 擁有什麼
| 屬性 | 契約 |
|---|---|
| Scope | chatroom、department、company 三選一。company 掛載點同樣帶有明確的 {company_id} path 區段。 |
name | 1–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 存取。 |
visibility | public(預設)或 private。private 讓 agent 只為建立者綁定這個指令,REST 的 get/list/invoke 與執行紀錄讀取也限建立者或所有被引用資料表的管理者。PUT 必填,POST 有預設值。restricted 的 command 不看它。 |
execute_policy | open(預設)或 restricted。restricted 只接納每一張被引用資料表的 manager,以及持有 execute grant 的主體;PUT 時省略代表不變。見 執行權限。 |
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 工具清單消失,/execute 與 /query 也回同樣統一的 404 Command not found;這種情況不會出現任何 tag 專屬的錯誤。
- 缺少或空白的
tag_id→ FastAPI request 驗證的422(tag_id是 1–36 字元的必填欄位) - Tag 不存在,或被引用的資料表不在 tag 內 →
422 {"error":"command_tables_outside_tag","phase":"author","message":"Command dependencies must belong to its tag","retryable":false,"details":{}}——編譯時的 tag 檢查會先執行 404 Tag not found in this scope與本地的{"error":"command_tables_outside_tag","tag_id":…,"table_ids":[…]}形狀,只會在 tag 於編譯與POST/PUT的加鎖重查之間變動時出現- 自後端 5.22.0 起,對不含
child_collectionstep 的 command 呼叫POST .../execute時,若 command 的某次寫入觸發了一條 approval 規則,而這條規則用field_map從 command 的 tag 之外的資料表對應 Review 表單輸入,也會回上述本地422形狀:table_ids列出那些來源資料表,且不會寫入任何東西。5.22.0 之前它回的是通用的500 command_execution_failed。對於authority: definition的 command,只有 execute 權限、且不管理每一張被引用資料表的呼叫者,拿到的狀態碼仍是422,但內容是經過遮蔽的command_failed(見 Status-only 結果)。
Tag 與 manager 檢查適用於遞移可達的資料表集合,不只是你在 step 裡寫出來的那幾張。編譯完成後,伺服器會針對整份 dependency contract 再跑一次這兩項檢查:link 目標表、rollup/lookup/formula 的來源表、跨表規則的目標表(exists/not_exists/count_limit)、trigger 的目標表(create_record/update_record/materialize_slots)、unique_tuple component 的 link 目標表,以及每一條 same_dimension_reference 規則所綁定的資料表(來源、目標與維度)。每一張都必須由你 manage、掛在 command 的 tag 上,並計入 256 張相依資料表的上限。
Dependency contract 上限為 256 張資料表。compiled.table_ids、提示用的 table_ids 與 definition authority 的 table_access 都採這個相依上限。編譯時超過就是 422 {"error":"query_limit_exceeded","phase":"author","message":"Command dependency table limit exceeded","retryable":false,"details":{"limit":256,"actual":257}}(actual 是觀察到的相依表數)。獨立的 20 張直接寫入目標表與 20 張 approval-gated 表上限不變。
一個 command 至少要引用一張資料表
POST 與 PUT 都會檢查編譯後的相依集合,空集合直接拒絕:
{
"detail": {
"error": "command_references_no_table",
"message": "a command must reference at least one custom table in this scope"
}
}狀態碼是 422,而且這是補洞而不是加規則。Command 介面上的每一道權限閘門都在「被引用的資料表集合」上迭代,集合是空的就讓每一道都空虛地成立:只有 let、只有 callback、或完全沒有 step 的定義,過去任何租戶使用者都能建出來(回 200——create 路由從來不回 201),然後讀它、改它、刪它、執行它、讀它的執行快照,並透過它的 callback 帶著 invoker context 對外送資料。
現在這些閘門在空集合上 fail closed,所以 2026-07-31 以前建立的資料列行為變成這樣:
| 路由 | 已存下的零資料表 command 得到 |
|---|---|
GET .../commands | 從清單消失,對任何呼叫者皆然,包含公司管理員 |
GET、PUT、/execute、/query、兩條 /executions 路由 | 404 Command not found |
DELETE | 仍然可用——manage 探測失敗後改用 command 自身 scope 的權威(聊天室建立者/管理員、部門管理員、公司管理員) |
IaC apply 的 create/update 分支 | applied: false,errors[] 內容為 command '<ref>' references no table: a command must operate on at least one custom table in this scope |
IaC apply 的 delete 分支 | 在閘門之前就返回,所以仍然刪得掉 |
資料列還活著時名字就一直被占著,所以殭屍列會卡住同名重建,直到有人刪掉它。刪除是唯一的修法——PUT 回 404,定義永遠改不回合法狀態。
授權:三道不同的門檻
預設情況下 command 不會放寬任何人的存取權;只有選擇採用 definer authority 的 definition 才會。每個介面對「被引用的資料表」問的是不同的問題。
| 介面 | 對被引用資料表的要求 | 失敗 |
|---|---|---|
| Create / update / delete | 每一張都要是 manager。僅 delete 例外:引用的表已解析不到時,改看 command 自身 scope 的權威(見修改與刪除) | Create,以及 PUT body 中新增的引用:404 Referenced table not found in this scope。對你無法 manage 其資料表的既有 command 做 PUT 或 DELETE:統一的 404 Command not found |
| List / get | 每一張都要能 read | List 省略該 command;get 回統一的 404 Command not found |
| Executions | 同一道 command 閘門,之後每一列只顯示給它的執行者,或該列凍結快照中每一張表的 manager | 看不到的列會被略過;看不到的單一列回 404 Command execution not found |
| Execute / query | 先要能 read 才看得到,接著逐個 action 套用呼叫者本人的 ACL 與 SCP 判定 | 該 action 本身的 ACL/SCP 錯誤 |
Authoring 的 404 刻意不列舉:資料表不存在、不在 scope 內、以及你不是 manager,三種情形回同一個 body。不要做出宣稱其中一種原因的 UI。讀取端的 404 是同一個想法的另一面——command 絕不會變成「你看不到的資料表是否存在」的探測器,所以同一個聊天室裡的兩個人看到不同的 command 清單是正常的。
帶 execute_policy: restricted 的 command,會以它自己的 gate——即時管理每一張被引用的資料表,或持有有效的 execute grant——取代上表 List/get/Execute 各列的讀取門檻與 visibility 檢查,並對其他人隱藏;閘外的呼叫者在 execute 時會得到 403 command_execute_denied。帶 authority: definition 的 definition 還會用它自己宣告的資料表 ceiling 執行,而不是用呼叫者逐 action 的 ACL。見 執行權限。
當呼叫者能 read、但無法 manage open command 所引用的每一張資料表時,回應仍會回傳,但 callbacks: []、triggers: [],並帶 lifecycle_redacted: true;restricted 的 command 則對持有 execute grant、但不管理每一張被引用資料表的呼叫者回 execute-only 檢視(definition: null、definition_redacted: true、interface)。空的 callbacks 陣列不代表「沒有設定 callback」——請先看這個旗標。
執行與查詢時,兩條通道解析部門資料表權限的方式不同,同一支 command 可能在一邊成功、在另一邊 403:
| 通道 | 部門資料表權限來自 |
|---|---|
人工 REST /execute、/query | 只看執行者本人——所有授予他這張表的聊天室的聯集。不綁任何 acting room。 |
| Agent 服務通道,以及呼叫聊天室範圍 command 的 trigger | 伺服器解析出的 acting room,作為硬性上限——與 agent 的資料表工具一致。呼叫部門或公司範圍 command 的 trigger 不綁定任何聊天室,因此部門資料表權限只由 trigger 作者本身決定,與手動 REST 相同。 |
綁在某聊天室的 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 根本看不到的資料。Acting room 會先綁定,時間早於任何資料表權限的解析:綁定對象是 command 觸及的每一張受 channel 管控的資料表,沒有受管控的資料表時則是 id 最小的那張。聊天室在該部門資料表上沒有 internal 授權時回 403 This chatroom has no grant on this table.。客戶 principal 的聊天室就是客戶自己的聊天室,受管控的資料表不在其 external 範圍內時回 403 This chatroom has no external grant on this table.。聊天室已刪除或成員資格已失去的情況走不到這一步:principal 檢查會先回 403 Command principal is not authorized 或 403 Command principal is invalid。
Command 路由直接拒絕 API key 認證。request.state.auth_method 必須是 jwt,否則回 403 Custom-table command routes require JWT authentication。程式對程式的存取請走獨立的 agent service-token 通道,而不是 UserAPIKey。
可見性:public 與 private
授權回答的是「你能不能碰這些資料表」。visibility 回答的是另一個問題:這是誰的指令。private 指令仍然是一般指令——同樣的授權門檻、同樣的執行語意——只是不會提供給每一個能執行它的人。這只適用於 open 的 command:restricted 的 command 不看 visibility,在下列每一條 REST 與 agent lane 上,它只對管理全部被引用資料表的人與持有 execute grant 的主體列出、可讀、可執行,與建立者無關(JSONL IaC 仍會對 manage lane 中的所有人顯示它)。
| Lane | open 且 private 的指令對誰可見 |
|---|---|
| 聊天室 agent 工具清單 | 只有建立者。管理全部被引用資料表的人在這裡不例外。 |
REST GET、list、/execute、/query 與兩條 /executions 路由 | 建立者,或管理全部被引用資料表的呼叫者。 |
| 受信任的 agent service lane | 只有建立者——與工具清單同一個判定,每次呼叫都重新檢查。 |
| JSONL IaC export/plan/apply | manage lane 的所有人。IaC 刻意不依可見性過濾。 |
在 GET、列表、/query 與 agent 路由上,看不到的指令與「id 不存在」一樣回不可列舉的 404 Command not found,並且不會出現在清單裡。在 invoke 路由上,你不能執行的 restricted 指令改回 403 command_execute_denied。REST /execute 會把這幾種情況區分開:該 scope 中沒有存活指令的 id 回 404 {"detail": {"error": "command_failed", "message": "The command could not be completed.", "redacted": true}}(沒有資料列可判斷時,status-only 投影一律 fail closed),已存在但你看不到的指令回 404 Command not found——除非它是 authority: definition 的指令、而你並非管理每一張被引用的資料表,這時連這個回應也會被遮蔽 body 取代。
與 POST 不同,visibility 在 PUT 是必填且沒有預設值。PUT 是整筆取代且永遠不會改寫 created_by,若有預設值,資料表管理者只要把別人的指令原樣送一次 PUT,就能悄悄把它從 private 改掉。
IaC 維持「預設值就省略」:只有 private 時匯出才會寫出 visibility,所以既有匯出維持位元穩定、digest 也不會變動。IaC lane 本身是 chatroom-manage lane,不論可見性都會讀到全部指令——若依操作者過濾 differ 看到的線上狀態,plan 會變得具破壞性:看不到的 private 指令會被判讀成「不存在」而規劃刪除,或在名稱上相撞。
visibility 與 agent_enabled 是兩個獨立開關。agent_enabled: false 對所有 agent 隱藏;visibility: "private" 只對建立者以外的 agent 隱藏。兩個一起設定是有意義的,任何一個都不蘊含另一個。
兩個定義版本
definition.version 在兩種共用同一個欄位、但本質上不同的產品之間做選擇。
| 版本 1 | 版本 2 | |
|---|---|---|
| Step 形狀 | 扁平的 {name, table, action, rows[]} | 以 kind 區分:select、let、assert、insert、update、delete |
| 讀取、join、彙總 | 沒有 | 有——每個 select 最多 4 個 join,可 group/having/order |
| 值如何送出 | 逐 step 的寫入後 row | 只透過宣告的 outputs;steps 只回 metadata |
| 上限 | ≤20 steps,最壞展開 ≤1000 | ≤40 steps,其中寫入 steps ≤20 |
| Query 模式 | 不支援 | mode: "query" |
| Effect identity | 不支援 | 寫入型定義可選用 effect_identity |
| Runtime 的相依漂移 | 只檢查它實際碰到的欄位與資料表 | 重驗完整 contract,對未解決 drift fail closed |
outputs、join、彙總、effect identity(僅限寫入模式)與 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
定義外層在 version 與 mode 之外還有第二個與版本無關的 key。delivery_reconciliation_ttl_seconds 是選填整數,範圍 60–604,800 秒(超出範圍在 authoring 時就是 422),用來限制 AI agent 通道要重播一次已執行過的呼叫、而不是再寫一次,最長能持續多久。不設定代表採用 900 秒的執行期預設值,且這個 key 會被省略,因此沒有設定它的定義與功能上線前的定義位元組完全相同,IaC diff 與 dependency contract 摘要都不會變動。與 mode: "write" 不同,明確給的值——即使是 900——會原樣儲存並回傳。版本 1 與版本 2 皆適用,而它對 REST 與 trigger 通道沒有任何作用。完整契約見Agent 派送調解視窗。
effect_identity
版本 2 的寫入型定義還可以帶 effect_identity——{namespace, kind, key},其中 key 由 literal 與 command 在第一個寫入之前所選資料列的不可變 id 組成。它讓每個業務事件只被記錄一次:事件重複時,回傳原本的 receipt 與 replayed: true(若 inputs 或程式不同則是終局的 409 ct.effect_identity_conflict),而不是再寫一次;跨呼叫者、key、trigger 與 agent 都一樣;針對已記錄事件的第二次審批 release,一律以 conflict 讓 apply_failed 結束(semantic_match 表示兩者的細節是否相符)。不設定時這個區塊會被省略,所以沒有宣告它的定義仍然位元組完全相同,而且它不會改變 idempotency_key 的運作方式。宣告方式見定義版本 2,結果見執行語意。
Action policy 的 key
外層另外接受三個選填、與版本無關、屬於 action policy 的 key:authority(definition 代表用宣告的 ceiling 取代呼叫者的資料表權限)、table_access(那份 ceiling,以資料表 id 為鍵,只在 authority: "definition" 時合法)與 exclude_requester(送審者不能核准這個 command 受審批管控的計畫)。三者在等於預設值時都不會寫進儲存的 definition——authority: "invoker" 與 exclude_requester: false 會被丟棄。誰能執行這個 command 並不屬於 definition:它是 command 自己的 execute_policy 欄位,加上它的 execute grant。
attachment_copies
自後端 5.22.0 起,version 2 的寫入型 definition 可以帶 attachment_copies:這是一組 grant,讓 Command 把它自己 select 所讀到的資料列上的指定附件,複製到它寫入的資料列,供原本無法複製的呼叫者使用(例如外部 client)。陣列為空時會省略,只有 invoker authority 的 Command 能帶它,而且只能透過 Commands REST API 撰寫。詳見附件複製。
生命週期 callback 可以打 Custom API tool
Command 的生命週期 callback 的目標,是字面 url 或以 id 指定的聊天室 Custom API tool——只能二選一,不可同時:
"callbacks": [
{ "name": "notify_erp", "api_tool_id": "…", "body": { "order": "$output.order_id" } }
]設定了 api_tool_id 時,method 與 headers 一律取自 tool 本身,body 則變成必填:method 可以省略或帶正規化預設值 POST,headers 必須為空(明確的空 object 可以接受);其他情況都是建立時錯誤。執行時會把 tool 的 method、url、headers 物化進私有執行快照,所以 tool header 裡的憑證永遠不必存在指令定義中。api_tool_id 需要 chatroom scope 的指令;department 與 company 指令會被拒絕,訊息是 api_tool_id requires a chatroom-scope command。一個 tool 能不能接線——它必須屬於這個聊天室、已啟用,並通過它自己的 URL 檢查——屬於 Custom API tool 的設定,不在本手冊範圍內;所有不成立的原因都會在建立時以 400 {"error":"invalid_command_definition","errors":["callback 'notify_erp': …"]} 一次回報。
若在執行當下 tool 不存在、已停用或屬於別的聊天室,該 callback 會被丟棄——連同指名它的 trigger 一起——並記錄在執行快照的 skipped_callbacks。Callback 的投遞永遠不會影響指令本身的結果;但它渲染出來的 body 會——見下方的 64 KiB 上限。
Callback 與生命週期 trigger 的形狀
每個 callbacks[] 項目(最多 20 個)是 {name, method, url | api_tool_id, headers, body}:name 符合 ^[a-z0-9_]{1,32}$,method 是 POST(預設)、PUT 或 PATCH,而且 url(1–2048 字元)與 api_tool_id 恰好擇一。triggers[](最多 40 個)把生命週期事件綁到 callback,形狀是 {"on": "staged" | "succeeded" | "failed", "callback": "<callback 名稱>"};執行到達 on 時,每個符合的項目都會把它的 callback 排入佇列——被抑制的 effect-identity replay 與執行階段的 ct.effect_identity_conflict 例外,它們會被記錄成 failed,但不會排入 callback。
以下規則屬於 request 驗證:在建立、更新以及 validate 上都是 loc ["body","definition",…] 的 FastAPI 422 清單(validate 會直接回 422,而不是回報告)。
| 規則 | 訊息 |
|---|---|
URL 必須是有主機的絕對 http(s) 網址 | callback url must be an absolute http(s) URL |
| URL 不可含 userinfo | callback url must not contain credentials; use callback headers |
主機、port 與 userinfo 不可含 $ | callback url host cannot be templated |
| header 最多 20 個 | callback allows at most 20 headers |
| header 名稱是 1–128 字元的 HTTP token | invalid callback header name '<name>' |
| header 值最多 4096 字元,且不含 CR/LF | invalid callback header value for '<name>' |
| body 樣板最多 65 536 個 UTF-8 bytes | callback body template exceeds 65536 bytes |
url 與 api_tool_id 恰好擇一 | callback needs exactly one of url or api_tool_id |
使用 api_tool_id 時:不可設非 POST 的 method、不可設 header、必須有 body | callback method is taken from the api tool; do not set it、callback headers are taken from the api tool; do not set them、callback body is required when api_tool_id is set |
| 名稱不可重複 | callback names must be unique、rule names must be unique、step, variable, and relation names must be unique |
| trigger 必須指向存在的 callback,每組只能出現一次 | trigger references unknown callback(s): <names>、lifecycle trigger pairs must be unique |
| 版本 2 的 callback 必須有明確的 body | version 2 callbacks require an explicit body: <names> |
Callback 超過 20 個、trigger 超過 40 個或 rule 超過 20 個,都是 422 too_long。需要伺服器才能做的對外檢查——共用對外 HTTP 政策的 SSRF 主機檢查、API tool 可否接線、樣板 token——會在 model 之後執行,並以 400 invalid_command_definition 列出所有原因。字面值為 "<REDACTED>" 的 url 能通過 model,但會在這裡被拒絕(webhook_url must be http(s), got scheme '')。
Callback 的字面 headers 會照原樣存進 definition。任何有權限的 command 管理讀取——REST GET 或 IaC export——都會把它們原封不動地回吐,所以讀得到 definition 的人就讀得到它們。請不要把憑證寫進字面 headers:改指向 api_tool_id 的 tool(它的 headers 永遠不落在 definition 裡),或在 IaC command 行宣告該 callback 的 secret_ref,把值留在 live command 上。這裡只說明什麼是會被儲存與回吐的內容:不變更誰能讀取 command、不遮罩已儲存的 definition,也不擴大 secret_ref 可綁定的範圍。
執行時,每個 callback body 會在事件被暫存的當下渲染,並以緊湊、排序過的 UTF-8 JSON 計算大小,上限 65 536 bytes。渲染發生在 execute 交易之內,所以超過上限的 succeeded callback 會讓整次執行失敗:400 {"error":"output_limit_exceeded","phase":"callback","message":"Rendered callback body exceeds its byte limit","retryable":false,"details":{"actual":…,"limit":65536,"resource":"callback_bytes"},…},寫入全部回滾、該列標為 failed。沒有 body 的版本 1 callback 會送出包含整份 input 快照的隱含 body,所以 input 超過約 64 KB 時也會以同樣方式失敗。在審批 release 時,同一個錯誤會讓暫存的計畫以 apply_failed 結束。
你拿回來的是編譯後的定義,不是你送出的定義
儲存的定義是伺服器編譯過的。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;因此刪除或停用該使用者——若是聊天室範圍的 command,把他踢出聊天室也一樣——會讓每次觸發都以 invoke_command failed: trigger owner can no longer execute this command 失敗。Trigger 不能指向 query 模式的 command,也不能指向會寫入審批控管資料表的 command。它的 idempotency key 由 run id 與 action id 推導而來,所以在當機視窗內的重試會走 replay 而不是重新執行。已被 command 的 effect identity 記錄過的事件同樣會 replay,此時動作結果會帶 replayed: true。該 action 的欄位與設定時驗證見觸發器動作型別。
從 AI agent 呼叫。 在 direct lane(以及 query command)中,工具組把每個可見 command 工具命名為 custom_table_command_{slug}_{id8}——slug 會轉小寫、把 [A-Za-z0-9_] 以外的連續段落收斂成 _,再依完整工具名稱 64 字元上限留下的 34 字元預算截斷;id8 則是把 command uuid 去掉連字號後的前 8 個字元。需要確認的 write 會改用 custom_table_command_prepare_{slug}_{id8},slug 預算為 26 字元。參數是每個宣告 input 一個具名 kwarg;query 模式 command 的工具會多一個選填的 cursor。一個 command 只有在 agent_enabled 為 true、tag 仍然有效、每一張被引用的資料表都能被當下的使用者或 social client 讀取(restricted 的 command 改由 execute grant 或「管理每一張被引用的資料表」把關,所以只對這些主體出現),open 的 private 指令是由該使用者建立——在 agent lane 上,管理者沒有 private 指令例外,而受限 Command 不看 visibility——而且每個宣告的 input 名稱都符合 ^[a-z0-9_]{1,32}$、也不是 cursor(query)或 confirmation_token(write)時,才會變成工具。不符合 input 名稱檢查的 command(例如 input 名稱含 CJK(中日韓)字元或大寫的版本 1 定義)會被靜默略過。最多只提供 200 個可見的 command;command 服務通道未設定、聊天室的 agent job 清單不含 custom_table_commands,或處於唯讀的 direct realtime(語音)工作階段時,則一個都沒有。這就是「為什麼 AI 看不到我的 command」的答案。見 agent 工具組。
模型讀到的工具說明是由 command 產生、有上限並標示為不受信任的內容。開頭是伺服器依模式寫的標頭——唯讀分頁查詢、驗證後立即執行的 direct 寫入,或需經兩輪提案確認的寫入——接著是 UNTRUSTED TENANT-AUTHORED COMMAND METADATA: 與一段有界定符號的區塊:顯示名稱(最多 160 字元)、discovery JSON {"name":…,"required_inputs":[…]}、說明(最多 4000 字元,沒有時以名稱代替)、output 文件(最多 4000 字元,以整行截斷並加上 [Additional tenant-authored output documentation omitted.]),以及第一個範例的緊湊 JSON(最多 1500 字元,以 … 截斷)。每個字串都會壓成一行並跳脫 markup。說明總長超過 8000 字元時,會在行邊界截斷並以 [Command metadata truncated at the server limit.] 結尾。宣告了 effect identity 的寫入型 command,會在該標頭再多兩句由伺服器撰寫的說明:它為每個所選的來源資料列記錄一個業務事件(程式有寫入 step 時,沒有改到任何列的呼叫不會記錄);重複已記錄的事件會回傳原本的結果與 replayed=true,且不寫入任何東西;針對該事件給不同的 inputs 則會以 ct.effect_identity_conflict 拒絕;而且模型絕不可把 replay 說成新的寫入,也不可改 inputs 來繞過 conflict。
agent_enabled 只管這第三條通道。設成 false 之後,command 對通過一般 visibility 與「所有資料表可讀」閘門的 REST 呼叫者仍然完全可用——對這些呼叫者照樣列得出、讀得到、執行得了——但工具組不會為它產生工具,而且可信 agent 路由在每一次呼叫時都會重讀這個旗標,所以對話進行中翻成 false,已經綁定的工具會立刻以 404 Command not found fail closed,內容與「id 不存在」完全相同。工具會回報為 {"status":"denied","error":"custom_table_command_failed","http_status":404,…}(確認通道的 prepare 則改回 custom_table_command_access_revoked,http_status 403),下一回合的 roster 也不會再列出這個工具。翻回 true 則不需要重新綁定就恢復執行。這個旗標出現之前,要讓 command 離開 agent 的手,只能刪掉它或弄壞它的 tag 綁定。
PUT 是整支取代,而 agent_enabled 預設是 true,所以更新時漏掉這個 key,等於默默地把 command 重新對 agent 打開。每一次必須維持隱藏的 PUT 都要送 agent_enabled: false。
儲存之前先檢查草稿
POST .../commands/validate 會跑建立閘門——定義編譯、管理權限、callbacks、tag 歸屬、名稱唯一性、內建範例;不含 execute_policy 檢查,所以帶 authority: "definition" 卻沒有 execute_policy: "restricted" 的草稿可能得到 valid: true,送出 POST 或 PUT 時可能仍會以 422 definition_requires_restricted 失敗——並回傳 CommandValidationReport,但不寫入任何東西:
{
"valid": false,
"errors": [
{ "status": 409, "detail": { "error": "command_name_conflict", "name": "出貨" } }
],
"advisories": [
{ "code": "approval_gated_tables", "message": "Writes to these tables are approval-gated; …", "table_ids": ["…"] },
{ "code": "name_uniqueness_unlocked", "message": "Name uniqueness was checked without the namespace lock; …", "table_ids": [] }
],
"compiled": {
"table_ids": ["…"], "mode": "write", "write_targets": ["…"],
"approval_gated_tables": [], "agent_tool_name": "custom_table_command_execute_00000000",
"input_names": ["customer", "item", "quantity"]
}
}errors[] 的每一筆都帶著 POST /commands 會拋出的 status 與 detail 原文,順序也與 create 相同,所以編輯器可以沿用既有的錯誤處理分支。定義無法編譯時——包括資料表不在 tag 內,也就是 author 階段的 422 command_tables_outside_tag 封包——compiled 為 null,且只會有 name_uniqueness_unlocked 這一則 advisory。與 POST 不同,驗證不會在第一個失敗就停止,最多收集 50 筆;但若定義本身編譯失敗,需要編譯結果的閘門會被略過。
advisories[] 永遠不是錯誤,而是三種只能之後才決定的事實:寫入目標需要審核、identity:* input 只在執行時(以及 agent 確認通道準備提案時)檢查 scope 歸屬,以及——在這裡一定會出現——名稱唯一性是在沒有命名空間鎖的情況下檢查的。因此 valid: true 不是保留:真正儲存時,tag、名稱與管理權限都會在鎖下重新檢查。
compiled.agent_tool_name 是用佔位的指令 id 組出來的,結尾一律是 _00000000,而且一律是 direct lane 的 custom_table_command_{slug}_00000000 形式、slug 預算 34 字元。當聊天室要求 Custom Table 動作需經確認時,真正的寫入工具是 custom_table_command_prepare_{slug}_{id8},預算只有 26 字元,所以較長的 slug 會被截得更短。名稱若沒有 ASCII 英數字,slug 會是空的並退回字面 execute。
同一個狀態碼有兩種錯誤形狀
/execute 與 /query 的 400、409、422 各自會回兩種 body 之一:該路由自己的區分聯集(cap_exceeded、idempotency_key_reuse、approval_required、not_a_query_command……),或 compose 程式的信封 CommandProgramErrorResponse。兩者一直都會出現在網路上,只是第二種沒有被寫進規格,所以依規格產生的用戶端解不出來。現在 schema 明確宣告:/execute 的 400/409/422 與 /query 的 400/409 都是 anyOf[<local union>, CommandProgramErrorResponse]——/query 的 422 本來就一直是信封。有 scope 的 /execute 路由也宣告 403,為 anyOf[CommandExecute403Response, CommandProgramErrorResponse]:純字串或結構化的權限拒絕,或 program-error 信封(column_not_readable、ct.effect_identity_replay_forbidden、command_execute_denied、definition_authority_invalid 或 definition_ceiling_missing)。一個例外:在 /execute 上,authority: definition 的 command 對不管理每一張被引用資料表的呼叫者,不在放行清單上的任何錯誤,都會以原本的狀態碼換成 {"error": "command_failed", "message": "The command could not be completed.", "redacted": true},這個 body 在已宣告的 schema 之外,只有 403 例外(它的自由格式 detail 物件容得下它;404 則完全沒有宣告)。
{
"detail": {
"error": "query_limit_exceeded",
"phase": "execute",
"step": "lines",
"path": null,
"message": "Select result exceeds max_rows",
"retryable": false,
"details": { "resource": "relation_rows", "limit": 500, "actual": 501 }
}
}| 欄位 | 契約 |
|---|---|
error | 判別欄位。封閉的 38 個錯誤碼列舉(最新的是三個 action policy 錯誤碼 command_execute_denied、definition_authority_invalid 與 definition_ceiling_missing,以及五個 effect identity 錯誤碼,其前是 decimal 的 numeric_invalid 與 numeric_overflow,都是 422);告訴你發生什麼事的是它,不是狀態碼。 |
phase | author、execute、stage、release 或 callback。 |
step | ^[a-z0-9_]{1,32}$;錯誤無法歸因到單一 step 時為 null。在 /execute、/query 與 agent 路由上這個 key 一定存在(未設定時為 null);建立類回應(POST/PUT/validate)與審批 release 的原因則在未設定時省略它(PUT 的 409 effect_identity_locked 例外,它帶有 "step": null)。definer authority 之 command 放行的錯誤只保留一組固定的安全 key,所以那裡沒有 step 與 path。 |
path | 1–256 字元,是否出現的規則與 step 相同。 |
message | 1–512 字元。可以安全顯示;它永遠不含 driver 文字或綁定參數。 |
retryable | 一定會輸出,預設 false。true 代表真正的暫時性錯誤——command_lock_conflict、source_version_changed、source_link_changed、query_timeout。只重試這些,以及原樣傳出的 409 {"error": "date_interval_exclusion_retry"} 規則 body(沒有寫入任何東西),其餘一律不要重試。審批 release 的失敗原因也可能帶 retryable: true——release 階段的 command_timeout,以及 lock 衝突時引擎把 staged change 重新排回待處理所留下的 409 command_execution_failed。這些旗標描述的是 release 這次嘗試,不代表要重送 /execute。 |
details | 一定存在,經常是 {}。query_limit_exceeded/relation_limit_exceeded/mutation_limit_exceeded/output_limit_exceeded 帶 {resource,limit,actual};cardinality_failed 帶 {expected,actual};select 的 query_timeout 帶 {limit}(每個 statement 的預算,單位毫秒);核准 release 時的 source_authority_changed 帶 {fields}——1 到 10 個欄位名稱(絕不含值),取自 audience、id、client_id、company_id、chatroom_id、department_id、ai、acting_room_id、authority_user_id(委派型 Agent client 背後已驗證的人)與 authority_owner_id(authority: definition Command 的 definer);ct.effect_identity_conflict 帶 {original_execution_id,semantic_match};command_timeout、numeric_invalid、numeric_overflow 與其餘錯誤碼則是 {}。 |
狀態碼由錯誤碼決定:拋出處明確指定的狀態優先;否則 author 階段的錯誤回 422(除非其預設值是 403 以上),其他階段則回該錯誤碼的預設值。error 為 command_execution_failed 的 body 一律以 500 送出。路由無法分類的任何例外,或 details 會洩漏非公開欄位的 program error,都會變成通用的 500 {"error":"command_execution_failed","phase":"execute","step":null,"path":null,"message":"Command execution failed","retryable":false,"details":{}}。
超過 Command JSON 深度界限屬於 request validation,不是 program-error envelope:HTTP 422 的 detail 項目只含 type: "command_json_limit_exceeded"、loc 與 msg,不複製 input 或 validator context。Definition 在遞迴解析與 serialized-size 走訪前先檢查深度。
| 錯誤碼 | 狀態 | 發生時機 |
|---|---|---|
invalid_expression | 建立時 422,執行時 400 | 運算式無法編譯或型別檢查不通過;執行時例如 output relation 無法取得 |
expression_limit_exceeded | 422 | 單一版本 2 運算式深度超過 12 層或超過 256 個節點(Expression depth or node limit exceeded、Expression node limit exceeded);一定發生在建立時 |
unsupported_clause_expression | 建立時 422,執行時 400 | 運算式無法在該子句中轉成 SQL;執行時形式包含不精確的 decimal 綁定或不支援的 typed JSON/陣列傳遞,不暴露 SQL 或參數文字(見精確十進位);update 的 set 或寫入 step 的 returning 中的 SQL 彙總或行內 $rollup/$lookup/$formula 在存檔時就會被拒絕(見分組彙總) |
ambiguous_reference | 422 | 資料表或欄位 token 同時符合多個目標;請改用 @table:<id> 或 @column:<col_id> |
json_parse_failed/json_pointer_failed | 400 | on_error: "fail" 的 $json_parse/$json_get 失敗,或 $json_stringify 失敗(見 JSON 運算子) |
cardinality_failed | 400 | $single 找到的列數不是恰好一筆(details {expected, actual}) |
join_validation_failed | 422 | join 條件格式錯誤(一定發生在建立時) |
write_validation_failed | 422 | 值無法寫入目標欄位(一定發生在建立時) |
command_output_reserved | 422 | Definition 宣告了保留的 output 名稱 approval_provenance(詳情);一定發生在建立時 |
command_public_value_unsafe、command_public_value_limit_exceeded | 422 | 設了 public: true 的 output 或 assert 無法證明安全,或超過公開上限(詳情);一定發生在建立時 |
command_tables_outside_tag | 422 | 有相依資料表不在指令的 tag 內;自後端 5.22.0 起,不含 child_collection step 的指令在 /execute 時,若某次寫入觸發的 approval 規則所對應的來源資料表不在 tag 內,也回它(附 tag_id 與 table_ids) |
column_not_readable | 403 | select 引用了對呼叫者隱藏的欄位,或 upsert 的 set 經由 $existing 讀取了這類欄位 |
command_execute_denied | 403 | 受限 Command 被既不是所有被引用資料表的管理者、也沒有 execute grant 的呼叫者執行 |
definition_authority_invalid | 403 | authority: definition Command 由伺服器推導出的 owner 已不再是 Command 所屬公司、仍存活、非服務帳號且管理每一張 table_access 資料表的使用者;必須由 manager 重新儲存這個 Command |
definition_ceiling_missing | 403 | 程式引用的某張資料表在執行時沒有對應的 table_access 項目 |
query_limit_exceeded、relation_limit_exceeded、mutation_limit_exceeded、output_limit_exceeded | 400(建立時 422;/query 的頁面 bytes 為 422) | 超過執行期預算 |
numeric_invalid、numeric_overflow | 422 | 精確十進位的值無效或超出界限;numeric_overflow 也用於 $date_add 的天數或結果超出範圍,以及 $local_date/$time_of_day 的轉換超出西元 1–9999 年 |
assertion_failed、command_rule_failed | 422 | 版本 2 的 assert 步驟或 rule 失敗(definer Command 的 execute-only 呼叫者只有在該 assert 是公開時才看得到它的 message) |
effect_identity_invalid | 422 | effect_identity 區塊違反建立規則;一定發生在建立時 |
effect_identity_locked | 409 | PUT 變更或移除已記錄過業務事件的 command 的 effect identity;一定發生在建立時。IaC 的對應是 200 apply 回應內的 per-line IacLineError.code |
effect_identity_unbound | 422 | $first key term 沒選到列,或它的 step 被略過;不寫入任何東西 |
ct.effect_identity_conflict | 409 | 業務事件已經記錄,而這次執行的 inputs 或程式不同;details {original_execution_id, semantic_match};發生在審批 release 時,會讓 staged 變更以 apply_failed 結束 |
ct.effect_identity_replay_forbidden | 403 | 事件已記錄,而呼叫者的列範圍權限無法重新證明可存取這些列;不揭露先前的執行或結果 |
schema_dependency_changed、source_version_changed、source_link_changed、source_authority_changed、frozen_manifest_changed | 409 | 儲存的 contract、凍結的紀錄版本或 link 集合、權限情境,或凍結的 manifest 已不相符 |
command_lock_conflict | 409 | 暫時性的 lock 衝突;retryable: true |
command_execution_failed | 500 | 通用失敗,以及計畫橫跨兩張受審批控管資料表的版本 2 指令 |
query_timeout、command_timeout | 504 | 單一 statement 或整次執行到達期限 |
idempotency_context_conflict | 409 | 以路由自己的純 {"error":"idempotency_context_conflict"} body 送出,不是信封 |
callback_export_limit_exceeded | — | 列舉中保留但從不送出;callback 的 byte 上限回的是 resource: "callback_bytes" 的 output_limit_exceeded |
請先判斷 detail.error 是否存在,再看它的值——絕不要用狀態碼分支。同一個 409 既可能是區域聯集的 {"error":"approval_required",…},也可能是信封的 {"error":"command_lock_conflict","retryable":true,…};同一個 422 既可能是版本 1 的 {"error":"command_rule_failed","rule":…},也可能是版本 2 那個沒有 rule 名稱的信封。用狀態碼分支的用戶端,每一對裡一定會處理錯一個。
修改與刪除
PUT 與 DELETE 都會先鎖住 command 的 scope root(失敗時回 409 command_scope_unavailable),再鎖住 command 這一列,然後才做任何檢查,所以檢查與變更之間不會被插隊。有六種衝突值得在編輯器 UI 裡分開呈現。
| 狀態 | Body | 原因 |
|---|---|---|
| 409 | {"error":"command_scope_unavailable"} | 無法鎖定 scope 根列——不存在、已刪除,或屬於其他租戶。 |
| 409 | {"error":"pending_staged_executions","staged_change_ids":[…]} | 該 command 還有 staged 狀態的執行時做 PUT 或 DELETE。請先處理完審批。 |
| 409 | {"error":"command_referenced_by_trigger","command_id":…,"references":[…]} | 刪除一個仍被有效(或可還原之垃圾桶內)資料表 trigger 引用的 command。 |
| 409 | {"error":"command_mode_trigger_conflict","command_id","from_mode","to_mode","message"} | PUT 把被 trigger 引用的 command 在 write 與 query 模式之間翻轉。 |
| 409 | {"error":"effect_identity_locked", …} | PUT 變更或移除已記錄過業務事件的 command 的 effect identity。 |
| 409 | {"error":"approval_trigger_conflict","command_id","target_table_id","target_table_name","rule_id","message"} | 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 的權威判定(聊天室建立者/管理員、部門管理員、公司管理員,租戶錨定不變);沒有該權威的呼叫者看到的仍是同一顆不可枚舉的
404。PUT刻意仍要求活表:它是整份重編譯,殭屍更新沒有東西可編譯。 - 刪除的 command 立即釋放名字。名稱唯一性現在只計活列。過去軟刪的 command 會永遠占住名字——與資料表不同,command 沒有還原機制,那個占名保留不到任何東西。先刪再以同名重建是合法順序,包括 tag 通道的 purge 掃掉整個系統 commands 的情況。
列出與查找指令
要查看一個範圍裡的指令,有三種讀取方式,路徑都在 /private/module/custom_tables/{scopeWithId}/ 底下(公司範圍是 company/{company_id})。三者決定「哪些指令看得到」的規則完全相同(就是上面提到的資料表讀取、private、restricted 與標籤規則),而且都只接受 JWT:
| 路由 | 回傳 | 適合用在 |
|---|---|---|
GET …/command-summaries | 不含 definition 與 interface 的指令:id、範圍 ID、name、description、agent_enabled、visibility、tag_id、execute_policy(open 或 restricted)、已儲存定義的 version 與 mode(write、query,未設定時為 null)、dependency_status、兩個遮蔽旗標、is_deleted、created_by 與時間戳記 | 選單、指令清單,以及只需要名稱與狀態的場合 |
GET …/commands?name=<完整名稱> | 名稱完全相符(1–64 個字元)、且你看得到的那一支指令(含定義;對你遮蔽時 definition 為 null),沒有就回空的 items | 知道名稱,要取得單一支的定義 |
GET …/commands/{command_id} | 一支指令(含定義) | 知道 id,要取得單一支的定義 |
不帶 name 的 GET …/commands 仍然回傳一頁看得到的指令(limit 預設 50、最多 200),每支都帶完整定義,回應格式沒有改變。定義可能很大,一頁上百支大型指令就有好幾 MB;所以選單請呼叫 command-summaries,它接受相同的 skip、limit 與 name,並以相同的順序回傳相同的指令與相同的 total。agent 與 MCP 的 commands_list 工具沒有改變,也沒有對應的摘要工具。數字見效能指南。
延伸閱讀
- 定義版本 1——扁平的
action+rowsDSL。 - 定義版本 2——關聯式程式。
- 執行語意——原子性、idempotency key、相依漂移與稽核紀錄。
- Query 模式——唯讀、不加鎖、可分頁的通道。
- 審批——被引用的資料表受審批控管時會發生什麼事。
- 附件複製——讓 Command 為任何呼叫者(包含外部呼叫者)跨資料表複製附件的 grant。
- 指令 composer——自然語言編寫通道與它的 WebSocket 協定。
- Commands 參考——所有路由、參數與錯誤。