Skip to Content

定義版本 2

版本 2 把 command 變成一支小型的關聯式程式。Steps 以 kind 區分,讀取是第一級公民,而值只能透過宣告的 outputs 對映離開 command。Join、彙總、outputs 與 query 模式都需要它。

{ "version": 2, "mode": "write", "inputs": [], "rules": [], "callbacks": [], "triggers": [], "steps": [], "outputs": {} }

version: 1 下帶 kind 的 step 會被以 kind-discriminated steps require explicit version 2 拒絕;在版本 2 下的扁平 action step 會被以 steps[i] ('name'): version 2 requires kind-discriminated steps 拒絕。明確寫出的 mode: "write" 儲存時會被丟掉——不要期待它被回傳。

外層另外接受選填、與版本無關的 delivery_reconciliation_ttl_seconds;它在版本 1 與版本 2 的行為完全相同,統一記載於 Commands 總覽。與 mode 一樣,不設定時它不會出現在儲存的定義裡。

版本 2 的 input 名稱必須符合 ^[a-z0-9_]{1,32}$。版本 1 另外接受中日韓字元,所以 v1→v2 遷移可能還沒改到 step 就先在 input 名稱上失敗。

六種 step kind

kind用途綁定
select從資料表、既有 relation 或字面值讀出一個有界 relation$rel.<name>
let由運算式算出一個具名值$var.<name>
assert條件為 false 時讓整次執行失敗
insert寫入新列$rel.<name>(它的 returning 對映)
update寫入匹配到的列$rel.<name>
delete刪除匹配到的列$rel.<name>

最多 40 個 steps,其中 insertupdatedelete 合計最多 20 個。Step 名稱唯一且符合 ^[a-z0-9_]{1,32}$

每個 step 都可以帶 when 守衛。條件為 false 時該 step 標記為 skipped,並綁定一個空 relation 而不執行。

select

{ "kind": "select", "name": "lines", "from": { "table": "Order lines", "as": "l" }, "joins": [ { "type": "inner", "table": "Orders", "as": "o", "on": { "and": [{ "left": "$row.l.Order", "op": "eq", "right": "$row.o.id" }] } } ], "select": { "line_id": { "$metadata": { "source": "l", "name": "id" } }, "sku": "$row.l.SKU", "qty": "$row.l.Qty" }, "where": { "$eq": [{ "$metadata": { "source": "o", "name": "id" } }, "$input.order_id"] }, "order_by": [{ "expr": "$row.l.SKU", "direction": "asc" }], "max_rows": 500 }
子句契約
fromtablerelation"$rel.<name>")、values(有界的具型別陣列運算式)三選一,加上必填的 as 別名與選填的 filter
joins最多 4 個。每個帶 typeinner | left | right)、table / relation / values 三選一、唯一的 as 別名、選填 filteron
on{"and": [ {left, op:"eq", right} … ]} 形式的 1–8 個 key,或 link join {"link": {"source": …, "target": …}}。兩側都是 $row.<alias>.<field> 參照。
select1–100 組「別名 → 運算式」。這些別名就是該 relation 的欄位名稱。
where / having布林運算式。
distinct布林值。
group_by最多 20 個運算式。
order_by最多 20 組 {expr, direction}directionasc(預設)或 desc
limit選填,1–1000。
max_rows必填,1–1000。

來源別名與 join 別名在同一個 step 內必須唯一。

select 的來源與每一個 join 目標,執行者都必須讀得到。只要其中一張解析出的 can_read 是 none,整次呼叫就以 403 Read access not granted for this table. 失敗——與 insert、edit 的閘門一樣大聲。2026-07-31 之前,row ACL 會把讀不到的資料表變成永不匹配的條件:/query200 加一個空 relation,agent 回報「查無符合資料」;在寫入程式裡,「先查再寫」的去重步驟查不到東西,就寫進一筆重複資料。這道閘門只管讀取——只寫不讀的 insert 目標本來就可以是 can_read: "none" 搭配 can_insert: true,而 dependency contract 帶進來的 link 或 lookup 資料表仍以只有 id 的替身呈現,兩者都不會 403。

Select 絕不截斷。沒有宣告 limit 時,結果超過 max_rows 會讓該 step 失敗並回 400 {"error":"query_limit_exceeded","details":{"resource":"relation_rows","limit":…,"actual":…}}。設定 max_rows 是正確性決策,不是效能提示。宣告的 limit 大於 max_rows 是 authoring 錯誤(Select limit cannot exceed max_rows);而且只要宣告了 limit在所有模式下都必須同時宣告非空的 order_bySelect limit requires deterministic order_by),不是只有 query 模式才需要。

letassert

let 只計算一次,並把結果綁定為 $var.<name>。選填的 type 用來宣告預期的純量、物件或陣列型別。

{ "kind": "let", "name": "unit_total", "value": { "$aggregate": { "relation": "$rel.lines", "expr": "$item.qty", "op": "sum" } } }

assert 是寫在程式裡的商業規則。它接受布林 condition、選填的 when 守衛,以及必填的 message(1–512 字元)。條件為 false 是 422 {"error":"assertion_failed",…},並讓整次執行回滾。

{ "kind": "assert", "name": "has_lines", "condition": { "$exists": { "relation": "$rel.lines" } }, "message": "That order has no lines to ship." }

寫入型 steps

{ "kind": "update", "name": "stamp", "table": "Orders", "match": { "id": "$input.order_id" }, "set": { "Status": "Shipped" }, "max_rows": 1, "returning": { "order_id": { "$metadata": { "source": "old", "name": "id" } } } }
insertupdatedelete
欄位載荷values(≤100 個 key)set(≤100 個 key)
選列方式match / where 恰好一個match / where 恰好一個
max_rows不接受1–1000,預設為 11–1000,預設為 1
returning 可用影像newoldnewold

版本 2 寫入型 step 的 for_each 是完整的運算式,不再限定 $input.<array>;每個元素在該 step 內以 $item.<field> 取用。when 守衛整個 step。returning(≤100 個別名)會綁定一個具名 relation,供後續 steps 與 outputs 讀取——它就是版本 1「只能取 id 的 $steps 擷取」的替代品。

values / set 內唯一可用的資料表別名是 current(僅 update 有),所以你不能$row.<step>.<alias> 拼出前一個 step 的列。請用 relation helper 讀取前一個寫入 step 的 returning relation——單列情境用 $single,其餘用 $aggregate / $collect / $count_rows / $exists

參照節點與運算式詞彙

值文法由四種參照家族構成。

節點意義
$rel.<name>select step 或寫入 step 的 returning 綁定的 relation。
$row.<alias>.<field>綁定到某個來源或 join 別名的列的欄位。這是 authoring 期的語法糖。
{"$column": {"source": "<alias>", "id": "col_<hex>"}}編譯器儲存的正規化、抗改名欄位參照。
{"$metadata": {"source": "<alias>", "name": "<key>"}}紀錄 metadata:idversioncreated_atupdated_atsort_ordercreated_bycreated_by_aicreated_by_clienttable_id

你寫 $row.l.SKU,讀回來是 {"$column": {"source": "l", "id": "col_…"}}——兩種拼法指的是同一個參照。$input.<name>$ctx.<key> 的行為和版本 1完全相同,而 ACL row policy 的 $me / $me.department / $now / $today±Nd 一樣不屬於這套文法。

運算子涵蓋算術與比較($add$sub$mul$div$neg$eq$neq$gt$gte$lt$lte$between$in$not_in$contains$and$or$not$is_null$is_not_null)、字串($concat$len$lower$upper)、日期($today$year$month$datediff)、JSON($json_get$json_parse$json_stringify$json_literal$array_literal),以及 $case / $literal / $reference

另有六個 helper 讀取已綁定的 relation。在 expr 內,該 relation 的欄位以 $item.<field> 取用。

Helper形狀結果
$exists{"relation": "$rel.x"}boolean
$count_rows{"relation": "$rel.x"}integer
$aggregate{"relation", "expr", "op"}opcountcount_distinctsumavgminmax純量
$collect{"relation", "expr", "max_items"}(1–1000)陣列——需要有序的 relation
$first{"relation", "expr"}純量或 null——需要有序的 relation
$single{"relation", "expr"}純量;relation 不是恰好一列時回 400 {"error":"cardinality_failed","details":{"expected":1,"actual":N}}

$single 讓你用宣告式的方式主張「剛好一筆匹配」,不必寫在用戶端程式裡。注意寫入 step 的 returning relation 不是有序的,所以對它使用 $first$collect 是 authoring 錯誤——需要有序語意時請在 select 上宣告 order_by

Outputs 是值離開的唯一出口

版本 2 的 execute 回應只報告 step 的metadata——namekindstatussucceededskipped)、schemarow_countsteps 裡沒有任何列。呼叫者需要的每一個值都必須宣告。

"outputs": { "shipment_id": { "value": { "$single": { "relation": "$rel.shipment", "expr": "$item.shipment_id" } }, "description": "Id of the shipment row this run created." }, "lines": { "relation": "$rel.lines", "select": ["line_id", "sku", "qty"], "max_rows": 100, "description": "The order lines covered by this shipment." } }

最多 100 個 outputs,key 符合 ^[a-z0-9_]{1,32}$。純量 output 是 {"value": <運算式>};relation output 是 {"relation": "$rel.<step>", "select": [別名…], "max_rows": 1..100}。兩者都接受選填的 description,長度上限 1024 字元。

description 不是裝飾。每一個宣告的 output 都會被寫進 agent 工具說明的 Outputs: 區塊——每個 output 一行 - <名稱>: <說明>,沒寫說明時則是單純的 - <名稱>——讓模型讀得到 output 的意義,而不是從 key 去推。過去 output 名稱只會以回應 body 的 key 形式抵達模型。這個欄位預設為 null,未設定時完全不輸出,所以既有定義與 IaC 文件的來回轉換仍然位元組穩定。

Relation output 的實際上限是 min(100, max_rows) 列,整個 outputs 物件則上限 65 536 bytes。超過是 400 {"error":"output_limit_exceeded","details":{"resource":"output_rows"|"output_bytes",…}}。Command 不是批次匯出 API——每次執行 100 列、64 KB 就是硬上限。

Step 回報的 schema 中,每個欄位都帶一個來自封閉 15 值清單的 type——nullstringtextselectintegerfloatbooleandatedatetimemulti_selectlinkattachmentjsonobjectarray——外加 nullable 旗標。這讓你能直接依 command 產生用戶端程式碼而不必猜。每個 step 最多回報 100 個欄位,row_count 範圍 0–1000。

完整範例:出貨一筆訂單

讀出訂單明細、拒絕空訂單、以加總數量建立出貨單、蓋章訂單,最後交回出貨單 id 與出貨明細。

{ "name": "fulfil_order", "description": "原子性地出貨一筆訂單:建立出貨單並更新訂單狀態。", "tag_id": "55555555-5555-4555-8555-555555555555", "definition": { "version": 2, "inputs": [ { "name": "order_id", "type": "string", "required": true, "nullable": false, "max_length": 36 }, { "name": "carrier", "type": "string", "required": true, "nullable": false, "max_length": 64 } ], "steps": [ { "kind": "select", "name": "lines", "from": { "table": "Order lines", "as": "l" }, "joins": [ { "type": "inner", "table": "Orders", "as": "o", "on": { "and": [{ "left": "$row.l.Order", "op": "eq", "right": "$row.o.id" }] } } ], "select": { "line_id": { "$metadata": { "source": "l", "name": "id" } }, "sku": "$row.l.SKU", "qty": "$row.l.Qty" }, "where": { "$eq": [{ "$metadata": { "source": "o", "name": "id" } }, "$input.order_id"] }, "order_by": [{ "expr": "$row.l.SKU", "direction": "asc" }], "max_rows": 500 }, { "kind": "assert", "name": "has_lines", "condition": { "$exists": { "relation": "$rel.lines" } }, "message": "That order has no lines to ship." }, { "kind": "insert", "name": "shipment", "table": "Shipments", "values": { "Order": "$input.order_id", "Carrier": "$input.carrier", "Units": { "$aggregate": { "relation": "$rel.lines", "expr": "$item.qty", "op": "sum" } }, "Created by": "$ctx.user_id" }, "returning": { "shipment_id": { "$metadata": { "source": "new", "name": "id" } } } }, { "kind": "update", "name": "stamp", "table": "Orders", "match": { "id": "$input.order_id" }, "set": { "Status": "Shipped" }, "max_rows": 1 } ], "outputs": { "shipment_id": { "value": { "$single": { "relation": "$rel.shipment", "expr": "$item.shipment_id" } } }, "lines": { "relation": "$rel.lines", "select": ["line_id", "sku", "qty"], "max_rows": 100 } } } }

執行它只會回傳 metadata 與宣告的 outputs:

{ "execution_id": "bbbbbbbb-2222-4222-8222-bbbbbbbbbbbb", "status": "succeeded", "steps": [ { "name": "lines", "kind": "select", "status": "succeeded", "schema": { "line_id": { "type": "string", "nullable": false }, "sku": { "type": "string", "nullable": true }, "qty": { "type": "integer", "nullable": true } }, "row_count": 2 }, { "name": "has_lines", "kind": "assert", "status": "succeeded", "row_count": 0 }, { "name": "shipment", "kind": "insert", "status": "succeeded", "schema": { "shipment_id": { "type": "string", "nullable": false } }, "row_count": 1 }, { "name": "stamp", "kind": "update", "status": "succeeded", "row_count": 1 } ], "outputs": { "shipment_id": "cccccccc-3333-4333-8333-cccccccccccc", "lines": [ { "line_id": "dddddddd-4444-4444-8444-dddddddddddd", "sku": "WIDGET-1", "qty": 2 }, { "line_id": "eeeeeeee-5555-4555-8555-eeeeeeeeeeee", "sku": "WIDGET-2", "qty": 1 } ] } }

回應會丟掉所有值為 null 的 key,因此請把「key 不存在」視為 null。

版本 2 的 rules

rules 陣列的用法與版本 1 相同——最多 20 條具名 rule、phasebefore_executebefore_commitmessage 必填 1–512 字元——而且 before_execute 的 rule 仍然只能參照 $input$ctx

失敗封套不同。版本 1 的 rule 失敗是 422 {"error":"command_rule_failed","rule":"<name>","message":…};版本 2 則是 program error 封套:422 {"error":"command_rule_failed","phase":"execute","message":"<rule message>","retryable":false,"details":{}}——body 裡沒有 rule 名稱。請對 error 分支,不要對 detail.rule 分支。

相依漂移時 fail closed

這是最要緊的操作差異。版本 2 的 command 在每次 execute 與每次 query 都會重新驗證整份 dependency contract。因此,任何尚未解決的 schema、rules、審批、trigger、改名、軟刪除、scope 或 tag drift 都會產生 409 schema_dependency_changed,直到作者以 PUT 重新存檔。

支援的 server mutation 以兩種不同政策避免無謂 drift。一般 column/rule 維護(包含 IaC、purge survivor 與 root cross-reference repair)只選 mutation 前 contract 仍 valid 的 commands,並在同一 commit 刷新 selected 且仍可編譯者。原本 stale 的 command 保持 stale。若 mutation 使 command 失效,mutation 優先:compile、closure escape 或 concurrent-edit failure 會略過該 command、讓 mutation 提交,command 則維持 fail-closed。非資料庫 prepare/refresh failure 也同樣降級;DB lock/deadlock 仍可能中止或重試 transaction。Multi-table purge/repair 依 rewritten table 自己的 scope 選取;這種分組是完整的,因為 dependency contract 採 exact-scope-closed:每個 closure member 都必須通過 command 的精確 scope filter,跨 scope authoring 則以 A referenced table does not exist in this scope 失敗。每個 participating scope 都會收到完整的 rewritten-table set。另一方面,透過 no-dependent fast path 併發建立的新 command,仍可能依 mutation 前形狀編譯並落成 stale。

Trigger edit 更嚴格。支援的 REST trigger PUT 或 IaC trigger apply 會先驗證 candidate graph,並要求所有 selected commands 都刷新成功,不限 DSL version。Trigger、history、refreshed definitions 與適用的 IaC state 以原子方式提交;任何 compile/closure/refresh failure 都 veto trigger mutation,REST 回 409detail.error: "command_dependency_refresh_failed"。兩種政策都不會修復編輯前已 stale 的 command。見執行語意

延伸閱讀

布林運算與 null(v5.10.0)

$and$or 在執行時計算與 SQL 查詢編譯採相同的三值邏輯:false AND null 為 false,true OR null 為 true;沒有決定性布林值時,null 運算元讓結果維持未知。既非布林也非 null 的值同樣維持未知,不會被視為 truthy。

Execution column 的 value_type 也包含 usersocial_clientprincipal,與 compiler 接受的 scalar types 一致;呈現 query/execution 結果欄位時請保留這些型別。

Last updated on