Skip to Content
核心概念觸發器觸發器總覽

自訂資料表觸發器

Trigger 在資料變更提交成功之後建立非同步 run,再依序執行 actions。它適合通知、呼叫外部服務及衍生寫入,不是 validation:action 失敗不會回滾已提交的來源資料,也不能用來否決寫入;需要否決時請使用規則。

定義一個 trigger

{ "name": "新訂單通知與同步", "on": "created", "when": [ { "column": "狀態", "op": "eq", "value": "已送出" } ], "actions": [ { "type": "notify", "chatroom_id": "11111111-1111-4111-8111-111111111111", "message": "新訂單:$row.訂單編號" }, { "type": "api_call", "method": "POST", "url": "https://integrator.example/orders/$row.訂單編號", "body": { "record_id": "$row.訂單編號", "amount": "$row.金額" } } ] }

on 可為 created、updated、deleted、restored 或 schedule。when 最多 5 個 AND predicates(when exceeds max 5 predicates)。每個 column 都必須能以顯示名稱或內部鍵解析(when column '<column>' not found in '<table>'),整個字串值或最上層 list 元素精確等於 $me、$today 等 row policy token 時會被拒絕(區分大小寫,不去除前後空白);token 若只是較長字串的一部分,則是字面值,因為 trigger 評估時沒有操作中的使用者。儲存時還會檢查什麼,取決於欄位:json 欄位只接受 is_null、is_not_null、eq 與 neq(深度比較,可用物件與清單);principal 欄位(user、social_client、principal)只接受 is_null、is_not_null、eq、neq 與 in,值必須是非空的 id 字串;其他欄位只做結構檢查——op 必須是 eq、neq、gt、gte、lt、lte、in、contains、is_null、is_not_null 之一,in 必須是最多 100 個純量的非空清單——不檢查值的型別。所以 decimal 欄位的值必須是像 "12.50" 這樣的純十進位字串:JSON 數字可以存檔,但之後永遠不會命中。觸發時比較的是原始值:principal 欄位請寫帶標記的儲存格值(user:<uuid>),因為沒有標記的 id 在 eq/in 下永遠不會命中,在 neq 下則會命中每個非空儲存格(materialize_slots 的 source.filter 也一樣)。空白儲存格不會符合任何值運算子,neq 也一樣。updated 還可用 transition: { "column", "from"?, "to"? } 篩選前後狀態。每張表最多 50 個 triggers,每個 trigger 必須有 1 至 5 個 actions。

和 rules 相同,triggers.set 是 full-replace PUT。先 triggers.get(僅限表格 moderator 讀取,見下文)、保留既有 trg_<hex> IDs、修改完整清單再送出。但只保留 trigger id 還不夠:每個 action 都會回傳自己的穩定 id(未自行指定時為 act_<12 hex>),這個也必須原樣送回。 沒有帶 id 的 action 會得到新產生的 id。若某個 trigger 的所有 action id 都換掉,failed 或 pending run 重試時會退回依位置續跑;如果同時插入或重排了 action,receipt 就會對到錯的 action。若只有部分 id 換掉,那些 action 會失去 receipt,重試時再執行一次。原樣送回 id,也是讓唯寫機密綁定到正確 action 的前提。

這個 PUT 也會維護相依 commands 的凍結 dependency contract。伺服器不分 DSL version,會重新編譯所有在編輯前 stored dependency contract 仍 valid 的相依 commands,並在同一個 transaction 中提交 trigger 集合、schema history 與刷新後的 command definitions。若 command 的 stored contract 已因其他原因 stale,則維持 stale。候選 action 若加入新的 target table,仍必須符合每個選中 command 的 tag closure;只要有 command 無法刷新,REST 就回傳 409,且 detail.error: "command_dependency_refresh_failed",並回滾 trigger 集合、history 與所有刷新後的 definitions。成功 body 回傳 table_id、遮罩後的 triggers 與其 secrets slot 清單,不會列出刷新後的 command IDs 或 digests。

IaC trigger 行使用相同的刷新規則,並把 trigger 的 IaC state 一併納入原子 bundle。失敗會回滾該行的 bundle,而整次 apply 仍維持 per-line best effort。

誰能讀取 trigger 設定

Trigger 設定(含外呼的 URL、headers 與 body)在每一種讀取中都屬於表格 moderator 的資料。三種 scope 的 GET .../triggers 依序套用兩道閘門:

呼叫者回應
表格不存在、已在垃圾桶、屬於其他 scope、(自後端 5.21.0 起)屬於其他公司,或同公司但對這張表沒有任何權利404 {"detail": "CustomTable Not found"},與未知 id 無法區分
對表格有權利、但不是表格 moderatormoderator 閘門的 403,例如公司表格回 Insufficient permissions. Required: table moderator or company management rights.
表格 moderator200,回傳遮罩後的設定

每一種 trigger 寫入都使用同一道 moderator 閘門,403 與 404 的回應和 rules 路由相同,包括所屬聊天室已被刪除的聊天室資料表回 404 Chatroom Not found;它也決定誰能在表格 history 中看到 trigger 設定列(schema_triggers_update):其他人讀 history 時,這些列會從該頁與 total 中一併排除。Command composer 的 schema 工具同樣套用這道閘門。Trigger 的執行、trigger runs 與 IaC 不受影響。

唯寫機密

webhook 與 api_call action 中的憑證對每一位讀者都是唯寫的,moderator 也不例外。機密 slot 依結構判定,不靠猜測值的內容:

  • URL 的 userinfo(user:password@),算一個 slot;
  • 每個 URL query 參數的值,以名稱與出現次序定位(只計入有值的參數;單獨的 ?flag 不是 slot);
  • 每個 header 的值,以不分大小寫的名稱定位;
  • api_call body 中每個純量葉節點,唯一例外是剛好等於 $row.<欄位> 的字串。

Scheme、host、port、path、fragment、header 名稱、query key 與 body key 仍然可見。Body 葉節點 "$row.<x>" 只有在 <x> 是這張表目前的顯示名稱或內部鍵時才會保持可見;否則它就和其他常值一樣是被遮蔽的 slot。每一種讀取(GET 與 PUT 的回傳、表格 history、IaC export、plan 與 apply 輸出、MCP triggers_get、command composer)都在每個 slot 顯示 "__SECRET__"。GET 與 PUT 的回應另外帶 secrets 清單,標出每個 slot 的位置,但絕不透露內容(沒有值、雜湊或前綴);只有表格 moderator 會收到的 schema_triggers_update 類型 history 列,則以 trigger_secrets: {"data": […], "old": […], "new": […]} 帶著同樣的 slot 物件,分別對應該列的 data.triggers、diff.old 與 diff.new(其他 history 列為 null)。只有 worker 在 action 觸發時才會使用儲存的值。

REST PUT .../triggers 與 IaC apply 新寫入的 schema_triggers_update history 列,也會在儲存前遮罩這些憑證。IaC trigger 基準會遮罩宣告中的憑證 slot;action 合約以外的 key 永遠進不了基準,因為 plan 在 parse 階段就會拒絕它(較早的版本是靜默移除這類 key)。較舊的 history 與 state 列需由操作人員執行 trigger-secrets 清理;它不變更即時 action 憑證或讀回格式。對 scope/system apply 後延遲清理舊 state 列的機制仍保留。

{ "table_id": "22222222-2222-4222-8222-222222222222", "triggers": [{ "id": "trg_999999999999", "name": "同步到 ERP", "on": "created", "actions": [{ "id": "act_444444444444", "type": "api_call", "method": "POST", "url": "https://erp.example/orders?api_key=__SECRET__", "headers": { "Authorization": "__SECRET__" }, "body": { "order_no": "$row.訂單編號", "source": "__SECRET__" } }] }], "secrets": [ { "trigger_id": "trg_999999999999", "action_id": "act_444444444444", "action_index": 0, "location": "url_query", "name": "api_key", "occurrence": 0, "path": null, "has_value": true }, { "trigger_id": "trg_999999999999", "action_id": "act_444444444444", "action_index": 0, "location": "header", "name": "Authorization", "occurrence": null, "path": null, "has_value": true }, { "trigger_id": "trg_999999999999", "action_id": "act_444444444444", "action_index": 0, "location": "body", "name": null, "occurrence": null, "path": ["source"], "has_value": true } ] }

每個 slot 中,action_index 是 action 從 0 起算的位置,name 是 query key 或 header 名稱(其他位置為 null),occurrence 是在同名且有值的參數之間從 0 起算的位置(只有 url_query),path 是 body 葉節點的物件鍵與陣列索引清單(只有 body),has_value 在儲存值非 null 且非空字串時為 true——空的 name= 參數、"" header,或 null、"" 的 body 葉節點則為 false。trigger_id 與 action_id 可能為 null。

PUT 時,對於送入 action 所綁定的已儲存 action,每個 slot 都遵循同一條規則:

送出內容效果
在該 slot 放 "__SECRET__"保留已儲存的值
新值取代原值
null清除:"<Header>": null 移除該 header,"headers": null / "body": null 移除整個欄位,body 葉節點為 null 則存成 JSON null。URL 字串內無法寫 null,因此空的 userinfo(scheme://@host)代表移除 userinfo,name= 則把該參數存成空值
不送保留:省略 url、headers 或 body 欄位會保留已儲存的欄位;在有送出的欄位中缺少某個 header、query 參數、userinfo 或 body key,會保留其已儲存的值(已儲存值為空的 slot 則會被移除)

因此把 GET 的 body 原封不動送回是安全的。保留的值只綁定一個身分,不會搬家:同 id 的已儲存 trigger,以及其中同 id 的 action(未知的 action id 視為新 action;同一個 id 送兩次會被拒絕)。沒有 id 的 action 只在對應關係明確時才綁定:先比對非機密設定完全相同者(相同設定的 action 數量一致時依位置對應),若只剩一個送入 action 與一個已儲存 action,再依相同型別與目的地,最後才依相同型別。有效目的地改變(URL 的 scheme、host 或 port,或任何大小寫的 Host header)時也會拒絕保留,所以改指向另一個目的地時,必須重新輸入憑證。被拒絕的保留不會儲存任何東西,並回傳物件形式 detail 的 400:

{ "detail": { "error": "trigger_secret_keep_refused", "code": "destination_changed", "message": "triggers[0] action 1: the url's scheme, host or port, or its Host header, changed, so its stored secrets cannot be kept — re-enter every header, query, userinfo and body value for the new destination (or clear them)" } }

code 說明原因;message 只會點名 slot,不會出現值,並以 triggers[<i>] action <n>: 開頭(n 從 1 起算)——trigger_id_reused 例外,它的內容是 triggers[<i>]: trigger id '<id>' is used by more than one trigger:

code何時拒絕修正方式
unknown_action_idaction 帶的 id 不是同一個 trigger 已儲存的 action,而且含有 "__SECRET__"(沒有佔位符的未知 id 只是一個新 action)送已儲存的 action id,或送真正的值
action_id_reused機密還原階段,兩個 incoming webhook/api_call action 使用同一個已儲存 action id,即使沒有保留值也會拒絕;其他 action 類型的重複 id 則在型別檢查後由一般驗證處理讓 id 不重複
trigger_id_reused逐一還原機密時遇到重複、非空的字串 trigger id;較早 trigger 的機密錯誤可能先於後面的重複 id 錯誤回報讓 id 不重複
ambiguous_keep沒有 id 的 action 要保留已儲存的機密(佔位符或省略的 slot),但有好幾個同型別的已儲存 action 都可能提供它送該 action 的 id,或送真正的值
no_stored_twin佔位符找不到可綁定的已儲存 action:新的 trigger 或 action、從別的 trigger 移過來的 action,或目的地或路徑已改變的無 id action送真正的值
type_changedaction 依 id 綁定到任何不同型別(不只 webhook ↔ api_call)的已儲存 action,而且送了佔位符送真正的值
destination_changed有值要保留,但 URL 的 scheme、host 或 port,或 Host header,與已儲存的 action 不同為新的目的地重新輸入或清除每個機密
nothing_to_keep佔位符放在綁定的已儲存 action 沒有的 slot(userinfo、該 occurrence 的 query 參數、header 名稱、body 路徑)送真正的值,或拿掉佔位符
duplicate_header同一個 action 送了兩次相同的 header 名稱(不分大小寫)——即使是沒有值要保留的新 action每個 header 只送一次

這個路由的其他 400 仍是純字串 detail,包括伺服器無法解析的佔位符會被最後一道防線拒絕(例如 header 值寫成 "Bearer __SECRET__"),所以請依 detail 是否為物件來分支處理。

Trigger PUT 屬於機密寫入路徑:它的 422 驗證 body 絕不回顯輸入(每筆只有 loc、type 與 msg: "validation_error:<type>"),request log 只保留遮罩標記,失敗也以不含值的方式記錄。離開伺服器的 trigger 設定摘要一律加上金鑰:command 的 dependency_contract.triggers_digest 是 tcm1: HMAC commitment,而不是讀者可以拿來比對猜測的純雜湊。

十種 action

type最小 shape用途
webhook{ "type": "webhook", "url": "https://...", "headers": {}? }POST trigger-time row snapshot
api_call{ "type": "api_call", "method": "POST", "url": "https://...", "headers": {}?, "body": {}? }可設定 GET/POST/PUT/PATCH/DELETE 的外部 JSON 呼叫
create_record{ "type": "create_record", "table_id": id?, "data": { "<欄位>": ... } }在相同 scope 或合法的同公司 ancestor scope 建立列;data 不可為空
update_record{ "type": "update_record", "table_id": id?, "target": "$row", "data": { "<欄位>": ... } }更新本列或一跳目標;明確 target table 使用相同的向上 scope matrix
notify{ "type": "notify", "chatroom_id": id?, "message": "..." }對同公司的 chatroom 發訊息;chatroom 表預設使用自己的房間,department/company 表必須明訂 chatroom_id
send_channel_message{ "type": "send_channel_message", "recipient": "created_by_client", "message": "..." }依資料列解析出一位社群客戶,推送一則訊息
invoke_command{ "type": "invoke_command", "command_id": id, "inputs": {} }以 trigger 作者的身分執行一個複合指令
delete_record{ "type": "delete_record", "target": "$row" }只軟刪除觸發它的那一列
materialize_slots{ "type": "materialize_slots", "target": {...}, "source": {...}, "horizon_days": 30 }在每日排程上把班表列展開成可預約時段
submit_sandbox_job{ "type": "submit_sandbox_job", "task_version_id": id, "input": {} }對 created/updated 資料列快照排入已授權的 Sandbox task version

Actions 按陣列順序執行,遇到第一個失敗就停止。$row.<欄位> 可用於模板;api_call URL 的 scheme、host 與 port 必須是靜態的:URL 內只有 path/query 可插值,header 值與 body 葉節點則同樣可以放 $row token。字串模板在儲存時轉為 rename-stable key,執行時再從 trigger-time snapshot 取值;唯一例外是 notify.message,它以原文儲存,所以欄位改名會讓 notify 模板失效。

各種 action 的欄位、上限與失敗模式見動作型別。

一次執行是「誰」在做

由 trigger 寫出的資料列仍然標記為 trigger 的 created_by,但實際權限現在取決於是什麼造成這次執行:

觸發來源資料列寫入以誰的權限執行
created / updated / deleted / restored造成該次寫入的 principal,且用他自己對目標表的權限
schedule系統權限,與以前相同
invoke_command(不論來源)trigger 作者當下的有效權限

所以一個「只有管理者會寫資料時」運作良好的自動化,可能在低權限使用者觸發它的那一刻開始失敗,而失敗會以 ok: false 的 action receipt 呈現,整個 run 標記為 failed。webhook、api_call、notify、send_channel_message 不受這條 ACL 閘門限制。詳見 Grant 與 row policy。

提交、排隊、觀測

來源 transaction 會同時留下 run row;提交後才送入 worker。Run 使用同一個 chain_id 傳遞衍生寫入,最大 chain depth 為 3,避免 trigger 互相循環,且每條 chain 的 generated-run 預算為 1,000 個子 run。At-least-once 傳輸會以原子 pending → running 更新認領 run;同時送達的重複 worker 看到 rowcount=0 就會在任何 action 前退出。generated-run 計數器是內部欄位,不在 run 回應中。從 triggerRuns.list 讀取 status、attempts、error、payload、chain_id、depth 與逐 action 結果,再依執行紀錄與重試處理失敗。

Warning Trigger 不能與 channel SCP 規則共存,因為背景 action 沒有 acting room。require_approval 的互斥有四條,每一條都回 409 approval_trigger_conflict:create_record / update_record 的目標表、delete_record 的自身表、invoke_command 所引用 command 會寫入的每一張表,以及 materialize_slots 的目標表。invariant 可以與 trigger 共存,且會驗證 trigger 寫入。

時間型工作見排程觸發器。端到端組合 rules、trigger、callback 與 run observation,請依自動化指南或使用流程精靈。

Last updated on