Skip to Content

trigger line

trigger 管理 table 的 after-commit automation。它不會 veto 原始寫入;actions 會在事件提交後才非同步執行。執行權限依 action 而異:row-event 的 record writes 使用造成該 run 的 principal 之即時 ACL,scheduled automation 使用 system authority,而 invoke_command 會重新檢查 trigger 作者當下的權限。created_by 主要作 audit attribution,不是所有 action 的通用授權;notify 也會以該作者作為訊息身分,並要求 user row 仍存在。IaC line 讓每一條 trigger 有可移動、可審查的 ref。

JSONL Line

{"kind":"trigger","table":"orders","ref":"notify_on_create","spec":{"name":"Notify on create","on":"created","when":[{"column":"status","op":"eq","value":"open"}],"actions":[{"type":"notify","message":"New order: $row.order_no"}]}}

欄位契約

頂層對應 IacTriggerLine:必填 kind: "trigger"、table、ref;state 可選且預設 present;renamed_from 可選;present 需要 object spec(純 rename 除外),absent 禁止 spec。secrets 是可選的唯讀清單,由 export 寫入以定位被遮罩的憑證;匯入時接受但忽略(見唯寫的 secret)。

IacTriggerLine.spec 在 IaC Pydantic model 中是 Dict[str, Any];真正的精確 trigger payload 由既有 TableTriggersPayload/shared validator 處理:

欄位必填契約
on是created、updated、deleted、restored 或 schedule
actions是1–5 個 action objects;type 為 webhook、api_call、create_record、update_record、notify、send_channel_message、invoke_command、delete_record、materialize_slots 或 submit_sandbox_job
name否string,最多 128 字元
when否最多 5 個 predicates;每個 column 使用本 table column ref
transition否只適用 on: "updated";{"column": ref, "from"?: value, "to"?: value}
schedule條件式on: "schedule" 時必填;其他事件禁止。type 為 date_column_reached、interval、daily 或 cron。在保留的 trigger id 下更改 type 會被 schedule_kind_change_requires_new_trigger_id 拒絕 — 請刪掉該行,並把新 kind 寫成一個新 trigger;詳見排程觸發器

Action 有自己的 type-specific 欄位:webhook/api_call 使用外部目的地;create/update record 使用 target/data;notify 使用通知目的與 message。不要 authored server id 或 created_by;trigger line ref 與 apply actor 分別處理 identity/ownership。每張表的合併上限仍然限制一份文件能宣告多少:每表 50 個 trigger、10 條 rule,且會把佔用名額的未受管理項目一起計算。完整 shape 請查閱規則與觸發器 API。

未知 key 在 parse 階段被拒絕

spec 會先檢查 key 集合。id、name、on、when、transition、schedule 與 actions 以外的 key 會被拒絕;action 內不屬於 type、id 與該 action 類型欄位的 key 也一樣:webhook——url、headers;api_call——method、url、headers、body;create_record 與 update_record——table_id、target、data;notify——chatroom_id、message;send_channel_message——recipient、message、line_flex、attachments;invoke_command——command、command_id、inputs;delete_record——target、data;materialize_slots——source、target、horizon_days、slot_minutes;submit_sandbox_job——task_version_id、chatroom_id、input、timeout_seconds。type 不是上述任何一種的 action,除了 type 與 id 之外不接受其他 key。

這個拒絕是整份文件的 parse 階段 400——extra_forbidden,loc 為 ["trigger", "spec", "{key}"] 或 ["trigger", "spec", "actions", {index}, "{key}"],key 依名稱排序、最多三個 issue、絕不回顯送出的值(見錯誤目錄)——所以不會 plan、不會 apply,也不會儲存任何東西。較早的版本是把這種 key 排除在儲存的基準之外,而不是拒絕文件。本身合法、但不屬於該 action 類型的 key,例如 notify action 上的 url,也以同樣方式被拒絕,因為檢查是依 action 類型逐一進行。

新 action 類型的 authored 形狀

有六個 action 類型具有重要的 IaC authored 欄位或 ref 翻譯。寫成儲存的內部欄位形狀會破壞可攜性或收斂。

send_channel_message 只有在 created/updated 才能選附件來源。Authored 形狀使用 1–5 個不重複的 attachment refs;apply 會轉成內部 key,export 會轉回 ref:

{"type":"send_channel_message","recipient":"created_by_client","message":"$row.order_no 的附件","attachments":[{"column":"invoice"}]}

所選快照最多可解析出 20 個不重複 blobs。LINE/Messenger/Instagram 先送文字再逐 blob 送出;agent 使用一則組合訊息。逐 part receipt 讓 retry 從已完成 parts 後續跑。

invoke_command 以 ref 指名 command,不是 command_id:

{"type":"invoke_command","command":"close_appointment","inputs":{"code":"$row.order_no","actor":"$user:dr.chen"}}

Ref 依下列順序解析,解析不到就失敗:

  1. 本文件的 system 以該 ref 管理、且仍解析得到 live 的 command;
  2. 同文件的 command 行——前向參照是合法的,executor 會把該 trigger 行延到 command 行套用之後;
  3. 同 scope 中由其他 system 以該 ref 管理的 command;
  4. 名稱完全相同、否則 slug 相同的 live command,沿 command 可見鏈搜尋——聊天室、其部門、再到公司——最近的 scope 優先。
invoke_command command ref '{ref}': managed command no longer resolves live in this scope invoke_command command ref '{ref}': ambiguous — {n} systems bind this ref to different live commands invoke_command: unknown command ref '{ref}' invoke_command command ref '{ref}': ambiguous — {n} live commands match

Export 寫出已匯出 command 的 ref,否則寫 live command 名稱的 slug,再否則——command 已刪除或屬於別處,或 slug 與 scope 內其他 ref 衝突——寫原始 command id;匯入時它解析不到,會以 invoke_command: unknown command ref '{id}' 失敗,而不是悄悄指向另一個 command。

身分型別的輸入可帶 $user: / $smc: token;$row.{column ref} 輸入會在觸發當下依觸發列渲染。解析不到、或與輸入型別不符的 token 會以 invoke_command input '{name}': {token error} 失敗。

delete_record 以字面撰寫,不做 ref 翻譯:

{"type":"delete_record","target":"$row"}

materialize_slots 以 $table:{ref} 形式指名目標資料表;slot_date、slot_start、slot_end 以及 link_map 與 defaults 的 key 都是目標表上的 column ref,而 link_map 的值是 trigger 自身資料表的 column ref。解析不到的 ref 會以 materialize_slots target.table_id: {detail} 或 materialize_slots target: {detail} 失敗。以原始 table id 取代 $table: 仍然可用,但目標端的 column ref 會原樣複製,必須寫成內部 key。排程之前值得先知道它自己的上限:每次執行最多掃描 1000 個 live 樣板列(超過會在插入任何時段之前失敗)、每個 tick 最多插入 1000 個時段(被截斷算成功,不是錯誤)、92 天的展望期、時段粒度 5 到 1440 分鐘、weekday 值為 mon…sun,來源時間 cell 為 24 小時制 HH:MM。

api_call 的渲染上限限制觸發當下的 $row. 展開:樣板長度 2000、最多 20 個 header、設定時 body 8192 位元組,渲染後 body 上限 256 KB、URL 與 header 各 8192 位元組。方法為 POST、PUT、PATCH、DELETE 與 GET,只有 POST/PUT/PATCH 可帶 body。

submit_sandbox_job 釘住 published、active 且不要求 confirmation 的 Sandbox task version,並且只適用 created/updated:

{"type":"submit_sandbox_job","chatroom_id":"11111111-1111-4111-8111-111111111111","task_version_id":"55555555-5555-4555-8555-555555555555","timeout_seconds":900,"input":{"order_id":"$row.order_no","amount":"$row.amount"}}

只有 table 本身有 chatroom 時才可省略 chatroom_id;department/company table 必須 authored。timeout_seconds 預設取 pinned version,範圍 1–604680。input 是 JSON object,template 最多 8 層、每個 string leaf 最多 2000 字,並受 Sandbox canonical JSON 的 1 MiB/深度 64/100000 nodes 上限約束。巢狀 $row.{column ref} string 可經 export/apply round-trip;剛好等於 token 的 leaf 保留 JSON 型別。Apply 與 fire time 都要求同公司的 live room、active task、published/active 且 requires_confirmation: false 的 version,以及可執行權限;fire time 還會在 input staging 前後重查 causing principal、來源 row read/hidden-column/channel policy 與不可變快照。

Ref 與身分規則

orders.notify_on_create 是 qualified identity,與同 table 的 column、rule、view、public_read refs 以及固定的 client_access identity 共用 namespace。Spec 中 when.column、transition.column、action 的 data keys/target.via,以及 $row.<ref> token 都應使用 authored column refs;plan 會翻成 live internal keys或保留可安全插值的 template。

這些 ref 一律對照 trigger 自身的資料表解析。因此透過原始 table_id 寫入另一張表的 create_record 或 update_record action 無法用 IaC 撰寫:它的 data key 指名的是另一張表的欄位,會以 unknown column ref 失敗——或者當 trigger 自身資料表剛好有同名 ref 的欄位時,悄悄對應到那個欄位的 key。Export 遇到這種 trigger 會直接以 export: internal key '{key}' has no ref mapping (legacy/unmanaged column) 拒絕。跨表的 record action 請留在 REST 端處理(見以 REST 補完)。

泛用本機 walker 會檢查 column、via、data keys 等明確 ref 位置。外部 URL、header 與 message 裡的 template 還要由 server validator 檢查,不能把任意普通字串當成 column ref。

身分 token 與 $row token

Principal 欄位上的 when predicate 值可帶身分 token,由 plan 與 apply 兩側的同一組共用 helper 解析。在 user/social_client 欄位上,這是指 $user: 與 $smc: 解析成原始 ID;在 principal 欄位上,則是 $user:、$smc: 與 $room: 解析成該欄位儲存的帶標籤 cell("value": "room:<id>",運算子維持不變)。identity 型別的 invoke_command 輸入常值只接受 $user: 與 $smc:,而且一律解析成原始 ID——沒有 identity:principal 這種輸入型別。未被替換的 token 會驗證通過卻在觸發時永遠比不中——變成一條靜默失效的 trigger——因此 plan 會把 token 失敗當成 validate 錯誤回報。訊息中的欄位是內部 key,不是 ref:

trigger when column 'col_5f2c9a1e': principal token '$user:bob': user 'bob' is deleted

只有在 trigger 的資料表是 live、且其 column ref 解析得到時,plan 才會檢查這些 token、invoke_command 的 ref 與輸入,以及 materialize_slots 的 ref。若 trigger 的資料表或它引用的欄位是由本文件建立,同樣的檢查改在 apply 執行,失敗時該行在 200 內以 trigger '{table}.{ref}': 前綴失敗。

$row.{column ref} 樣板 token 則是反方向。Export 會把儲存的 $row.{col_hex} 形式改寫成 ref,涵蓋 api_call 的 url/headers/body、notify 與 send_channel_message 的訊息、line_flex 字串,以及整個 submit_sandbox_job.input;plan 與 apply 再把 ref 翻回內部 key。send_channel_message.attachments[].column 也會以明確 column ref round-trip。

$row. token 只讀取英文字母、數字與 _。含有 - 的 column ref 會在連字號處被截斷——$row.order-no 會被讀成 $row.order——所以要放進樣板的欄位,ref 請使用 _。

警告: $row. 的翻譯刻意不是 fail-closed。無法解析的 token 會原樣保留,好讓舊有以內部 key 撰寫的文件、以及只是「看起來像 token」的自由文字繼續運作。因此 api_call body 或 notify 訊息裡打錯的 column ref 會靜默通過 plan,並在觸發時渲染成字面文字。

唯寫的 Secret 與 secrets 註記

webhook 與 api_call 的憑證在每個讀取介面上都是唯寫的,IaC 也不例外,對所有讀者皆然——table moderator 也一樣。Export、plan 與 apply 會把每個 secret slot 換成 "__SECRET__":

Slotlocation定位方式
url userinfo(user:password@)url_userinfo該 action
每個有值的 query 參數(key=value;單獨的 ?flag 不是 slot)url_queryname,加上同名有值參數中的 occurrence
每個 header 值headername(不分大小寫)
除精確 $row.<column> 字串以外的每個 api_call body 葉節點bodypath(物件 key 與陣列索引)

Scheme、host、port、path、method、header 名稱、query key 與 body key 仍然可見,所以絕不要把憑證放在 url path 裡。Trigger 觸發時仍使用儲存的值。

匯出的 trigger 行會在 spec 旁帶著具型別的 slot 清單:

{"kind":"trigger","table":"orders","ref":"sync_crm","spec":{"name":"Sync CRM","on":"created","actions":[{"type":"api_call","method":"POST","url":"https://crm.example.com/hooks?token=__SECRET__","headers":{"Authorization":"__SECRET__"},"body":{"order":"$row.order_no","source":"__SECRET__"}}]},"secrets":[{"action_index":0,"location":"url_query","name":"token","occurrence":0,"has_value":true},{"action_index":0,"location":"header","name":"Authorization","has_value":true},{"action_index":0,"location":"body","path":["source"],"has_value":true}]}

secrets 絕不包含值、hash 或前綴,匯入時也會忽略它:真正保留儲存值的是 spec 內的佔位符。對於該行所綁定的已儲存 action,每個 slot 都遵循與 REST triggers PUT 相同的寫入規則:

  • 佔位符保留儲存值;
  • 新值取代它;
  • null 清除它——"<Header>": null 移除該 header,"headers": null/"body": null 移除整個欄位。在 url 內,空的 userinfo(scheme://@host)清除 userinfo,name= 則儲存沒有值的參數;
  • 省略某個欄位或 slot 會保留它(前提是它有值)。

IaC 文件不帶 action id,因此一行只會綁定到它自己的 IaC 受管 trigger,而每個沒有 id 的 action 只在對應無歧義時綁定:先比對完全相同的非 secret 設定,再在相同 action 數量一致時依位置對應,接著——剩下剛好一個傳入與一個已儲存 action 時——依相同類型與目的地,最後只依類型。若 url 的 scheme、host、port 或 Host header 與儲存值不同,保留會被拒絕,所以把 action 指向新目的地就必須重新輸入憑證。綁定成功的 action 會保留原本的 action id,因此更換 secret 不會重新產生 action。

無法綁定的保留是 validate-phase 錯誤,code 為 trigger_secret_keep_refused,什麼都不會套用。在規劃為 create 的行上使用佔位符也會被拒絕——新的 scope,或這個 system 既未管理也無法採用的 trigger——因為沒有可保留的儲存值;請先換成真實憑證。採用時比較的是雙方遮罩後的形式,所以遮罩後的文件可以比對到 live 上未受管理的 trigger,但 trigger 的 adoption 目前不會收斂(見下文)。精確訊息見錯誤目錄。

共用 validator 在每個寫入者上都會把關。未解析就抵達它的佔位符會以 '__SECRET__' is the masked placeholder of a stored {type} secret and cannot be saved as a value 拒絕;只差大小寫的兩個 header 名稱會以 {webhook|api_call} header '{name}' is repeated (header names are case-insensitive) 拒絕。

生命週期與規劃

Present line可規劃 create、adopt、move、update 或 noop;absent 規劃從 table triggers 集合刪除。Apply 透過 validated full-list PUT path merge-upsert,不會用這一行抹掉未受 IaC 管理的 triggers。adopt 目前會在 apply 以 trigger '{table}.{ref}': adopt with no resolvable native id 失敗,下一次仍規劃 adopt;要管理這類 trigger,請先透過 REST 移除 live 的那一份,讓該行規劃 create(見 adoption)。

手寫 trigger 現在會收斂

手寫的 trigger 過去會永遠重新規劃成 update,因為 store 會寫入作者從未輸入的欄位。現在 differ 與 executor 共用同一個正規化器,對雙方對稱地補上那些欄位:

  • transition 補上明確的 from 與 to,預設為 null。
  • create_record 或 update_record action 補上所屬資料表的 table_id。
  • 排程的 offset_minutes: 0 與 timezone: "UTC" 會被移除,因為 store 本來就省略它們。

結果是手寫的 trigger 重新 plan 會是 noop。如果你一直在對付某條永遠不收斂的 trigger 行,原因就在這裡——而且不需要改文件。

最後一項有個值得知道的推論:在排程中釘住 offset_minutes: 0 或 timezone: "UTC" 是合法的,但 store 會把兩者丟掉,所以文件與 live 在這些鍵上的差異是正規化產物,不是意圖。

那個永遠存在的 update 不只是噪音。它每次 apply 都會重新產生 action ID,並改變讀取該表之相依 commands 所儲存的 trigger digest。現在穩定地沿用 action ID,已移除這種人造變動。

當 IaC trigger 行變更 live trigger 集合時,apply 也會建立候選 dependency closure。每個新的 target table 都必須符合所有在編輯前 stored dependency contract 仍 valid 之相依 commands 的 tag closure,不限 DSL version。伺服器會用候選 triggers 重新編譯那些 commands;若 command 的 stored contract 已因其他原因 stale,則刻意維持 stale。Trigger 集合、schema history、trigger IaC state 與刷新後的 command definitions 會在同一個 transaction 中提交。只要 validation、refresh、state 或 commit 失敗,該行的整個 bundle 都會回滾,而 apply 繼續處理其他行。

Plan 與本行執行之間若發生外部 trigger drift,會產生 per-line code: "iac_plan_stale",detail 為 trigger changed after planning; run plan again。相對地,若本行成功並推進相依 command 的 dependency digest,同一次 apply 中較後面的 command 行會接受這個精確、由伺服器擁有的推進,不會產生假的 iac_plan_stale。

Trigger action 可以寫入另一張表、呼叫外部服務或送通知,具有比 spec diff 更廣的影響面。Plan 審查必須核對 actor、event、target refs、外部 URL、action data 與 chain/depth 保護;free-dict 的完整 trigger validator 仍在 apply 持久化路徑執行,本機 parse 成功也不驗證目的系統可用性。

驗證錯誤

state=absent lines carry no spec (table lines are the sole adopt-then-delete exception) state=present lines require a spec (unless a pure rename via renamed_from) unknown table ref 'orders' duplicate ref 'orders.notify_on_create' (already declared in this document) unknown column ref 'orders.status' {kind} cap exceeded ({final} > {cap}); unmanaged {kind}s occupying slots: [names]

上限訊息是 validate 錯誤,回報在該表的第一行 rule 或 trigger 上;對全新的表則完全跳過——沒有 live 表就沒有東西會被撐破。

前兩類 lifecycle detail 來自丟出一般 ValueError 的 validator:伺服器在 plan 與 apply 都以 parse 階段的 400 回應,detail 是 loc 為 ["trigger"] 的 typed value_error issue(見 Validator 訊息類別),工作台顯示相同的 detail。錯誤 event、actions 數量與 action-specific fields 由 apply 的 downstream trigger validator 回報,形式為 trigger '{table}.{ref}': {reason},多個原因以 ; 串接。精確本機字串來源:components/iac/parse.ts。

動手試試

在 IaC 工作台建立範例 trigger,把 when.column 改成未知 ref,修正後對測試 scope 執行唯讀 plan。Apply 前逐項核對 action target 與外部副作用。

Last updated on