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。
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。
新 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。同文件的前向參照是合法的——executor 會把該 trigger 行延到 command 行套用之後——也接受同 scope 內 live command 的名稱或 slug:
{"type":"invoke_command","command":"close_appointment","inputs":{"code":"$row.order_no","actor":"$user:dr.chen"}}身分型別的輸入可帶 $user: / $smc: token;$row.{column ref} 輸入會在觸發當下依觸發列渲染。
delete_record 以字面撰寫,不做 ref 翻譯:
{"type":"delete_record","target":"$row"}materialize_slots 以 $table:{ref} 形式指名目標資料表。排程之前值得先知道它自己的上限:每次執行最多掃描 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。
泛用本機 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 錯誤回報:
trigger when column 'assignee': principal token '$user:bob': user 'bob' is deleted$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.的翻譯刻意不是 fail-closed。無法解析的 token 會原樣保留,好讓舊有以內部 key 撰寫的文件、以及只是「看起來像 token」的自由文字繼續運作。因此api_callbody 或 notify 訊息裡打錯的 column ref 會靜默通過 plan,並在觸發時渲染成字面文字。
生命週期與規劃
Present line可規劃 create、adopt、move、update 或 noop;absent 規劃從 table triggers 集合刪除。Apply 透過 validated full-list PUT path merge-upsert,不會用這一行抹掉未受 IaC 管理的 triggers。
手寫 trigger 現在會收斂
手寫的 trigger 過去會永遠重新規劃成 update,因為 store 會寫入作者從未輸入的欄位。現在 differ 與 executor 共用同一個正規化器,對雙方對稱地補上那些欄位:
transition補上明確的from與to,預設為null。create_record或update_recordaction 補上所屬資料表的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 目前由工作台本機穩定攔截;錯誤 event、actions 數量與 action-specific fields 由 apply 的 downstream trigger validator 回報。精確本機字串來源:components/iac/parse.ts。
動手試試
在 IaC 工作台建立範例 trigger,把 when.column 改成未知 ref,修正後對測試 scope 執行唯讀 plan。Apply 前逐項核對 action target 與外部副作用。