定義版本 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,其中 insert、update、delete 合計最多 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
}| 子句 | 契約 |
|---|---|
from | table、relation("$rel.<name>")、values(有界的具型別陣列運算式)三選一,加上必填的 as 別名與選填的 filter。 |
joins | 最多 4 個。每個帶 type(inner | left | right)、table / relation / values 三選一、唯一的 as 別名、選填 filter 與 on。 |
on | {"and": [ {left, op:"eq", right} … ]} 形式的 1–8 個 key,或 link join {"link": {"source": …, "target": …}}。兩側都是 $row.<alias>.<field> 參照。 |
select | 1–100 組「別名 → 運算式」。這些別名就是該 relation 的欄位名稱。 |
where / having | 布林運算式。 |
distinct | 布林值。 |
group_by | 最多 20 個運算式。 |
order_by | 最多 20 組 {expr, direction},direction 為 asc(預設)或 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 會把讀不到的資料表變成永不匹配的條件:/query 回 200 加一個空 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_by(Select limit requires deterministic order_by),不是只有 query 模式才需要。
let 與 assert
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" } } }
}insert | update | delete | |
|---|---|---|---|
| 欄位載荷 | values(≤100 個 key) | set(≤100 個 key) | — |
| 選列方式 | — | match / where 恰好一個 | match / where 恰好一個 |
max_rows | 不接受 | 1–1000,預設為 1 | 1–1000,預設為 1 |
returning 可用影像 | new | old、new | old |
版本 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:id、version、created_at、updated_at、sort_order、created_by、created_by_ai、created_by_client、table_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"},op ∈ count、count_distinct、sum、avg、min、max | 純量 |
$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——name、kind、status(succeeded 或 skipped)、schema 與 row_count。steps 裡沒有任何列。呼叫者需要的每一個值都必須宣告。
"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——null、string、text、select、integer、float、boolean、date、datetime、multi_select、link、attachment、json、object、array——外加 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、phase 為 before_execute 或 before_commit、message 必填 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 回 409 與 detail.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 也包含 user、social_client、principal,與 compiler 接受的 scalar types 一致;呈現 query/execution 結果欄位時請保留這些型別。