クックブック (実践例集)

最終更新: 2026年7月17日

さまざまなシナリオを完結した ScriptDefinition として示します。文法の根拠は Statement カタログ値式、実行と制約は 実行セマンティクス、制約、セキュリティ を参照してください。すべての例は書き込み fields の値がロケールマップ({ "<locale>": 値 })であり、例のロケールは en-US に統一しています。外部呼び出し(Http)や Media ファイルインジェストを含む例は executionMode"Async" です。

目次

基本 CRUD

1. Content の作成と公開

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
      "fields": { "title": { "en-US": "{ /payload/fields/title }" }, "body": { "en-US": "{ /payload/fields/body }" } },
      "publish": true, "name": "post" },
    { "type": "Return", "value": { "id": "{ /post/sys/id }" }, "statusCode": 201 } ] }

2. 計算値で update (閲覧数 +1)

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
      "fields": { "viewCount": { "en-US": { "+": [ "{ /payload/fields/viewCount }", 1 ] } } } } ] }

3. 読み取り専用 GET: 自分の注文一覧

{ "method": "Get", "executionMode": "Sync",
  "statements": [
    { "type": "ResourcePageRead", "resource": "Content", "contentType": { "sys": { "id": "ct_order" } },
      "where": { "createdBy": ":self" }, "order": "-sys.createdAt", "limit": 20, "name": "orders" },
    { "type": "Return", "value": { "orders": "{ /orders/items }", "next": "{ /orders/next }" } } ] }

Script は書き込みだけでなく 読み取り BFF エンドポイント としても使います。createdBy: ":self" で「自分のものだけ」を取得し、結果をそのまま返します。

4. 単件取得後に guard して承認

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "ResourceRead", "resource": "Content", "target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" },
    { "type": "If", "condition": { "!=": [ "{ /order/fields/status/en-US }", "pending" ] },
      "then": [ { "type": "Return", "value": { "reason": "not pending" }, "isError": true, "statusCode": 409 } ] },
    { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } },
      "fields": { "status": { "en-US": "approved" } }, "publish": true },
    { "type": "Return", "value": { "ok": true } } ] }

ResourceRead で単件を名前にバインドすると { /order/fields/... } で直接参照できます(items/0 は不要)。存在しなければ取得でエラーになります(Try で囲めます)。

検索と upsert

5. slug upsert (find-then-upsert)

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
      "where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" },
    { "type": "If", "condition": { "!!": "{ /found/sys/id }" },
      "then": [
        { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /found/sys/id }" } },
          "fields": { "body": { "en-US": "{ /payload/fields/body }" } } },
        { "type": "Return", "value": { "id": "{ /found/sys/id }", "op": "updated" } } ],
      "else": [
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
          "fields": { "slug": { "en-US": "{ /payload/fields/slug }" }, "body": { "en-US": "{ /payload/fields/body }" } }, "name": "created" },
        { "type": "Return", "value": { "id": "{ /created/sys/id }", "op": "created" }, "statusCode": 201 } ] } ] }

ResourceFind は最初のマッチを直接バインドし(なければ null)、{ "!!": "{ /found/sys/id }" } で存在有無を分岐します。

6. 動的フィールドキーと動的ロケール patch

{ "method": "Patch", "executionMode": "Sync",
  "statements": [
    { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
      "fields": { "{ /payload/fields/fieldKey }": { "{ /payload/fields/locale }": "{ /payload/fields/value }" } } } ] }

フィールドの キー とロケールの バケットキー の両方が { /ptr } 参照です。翻訳を特定のロケールバケットに差し込むときに使います。

外部 API

7. クレジット guard、先行差し引き(CAS)、LLM 呼び出し、失敗時の返金 (代表例)

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_wallet" } },
      "where": { "createdBy": ":self" }, "name": "wallet" },
    { "type": "If", "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
      "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" }, "isError": true, "statusCode": 402 } ] },
    { "type": "Try",
      "body": [
        { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /wallet/sys/id }" } },
          "version": "{ /wallet/sys/version }",
          "fields": { "balance": { "en-US": { "-": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] } } },
          "name": "charged" } ],
      "catch": [ { "type": "Return", "value": { "ok": false, "reason": "version conflict, 再試行" }, "isError": true, "statusCode": 409 } ] },
    { "type": "Try",
      "body": [
        { "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
          "headers": [ { "key": "Authorization", "value": "Bearer sk-...", "secret": true } ],
          "body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "name": "resp" },
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
          "fields": { "text": { "en-US": "{ /resp/body/choices/0/message/content }" } }, "name": "out" },
        { "type": "Return", "value": { "ok": true, "id": "{ /out/sys/id }", "remaining": "{ /charged/fields/balance/en-US }" } } ],
      "catch": [
        { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /wallet/sys/id }" } },
          "fields": { "balance": { "en-US": { "+": [ "{ /charged/fields/balance/en-US }", "{ /payload/fields/cost }" ] } } } },
        { "type": "Return", "value": { "ok": false, "reason": "generation failed, refunded" }, "isError": true, "statusCode": 502 } ] } ] }

guard で残高が十分かを確認したうえで、外部呼び出しより先に差し引きます。差し引きは wallet の sys.version で楽観ロック(CAS)をかけます。読み取った残高と差し引きの間に別の実行が wallet を変更していた場合、バージョン不一致で abort し、catch409 を返します。外部呼び出しは行わないため、同時リクエストが二重に差し引かれることはありません。差し引きが確定した後にのみ LLM を呼び出し、その呼び出しが失敗した場合は catch で差し引き分(cost)を戻して加算し 返金(補償)したうえで 502 を返します。取り消せない外部呼び出しの前に課金を確定し、失敗したときだけ補償する順序です。秘密鍵は secret:true ヘッダーに置きます。補償の限界は 実行セマンティクスのトランザクションなしと補償 を参照してください。

8. 画像(URL)を Media にして Content に添付

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "Http", "method": "POST", "url": "https://api.img.com/gen",
      "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
      "body": { "prompt": "{ /payload/fields/prompt }" }, "name": "gen" },
    { "type": "ResourceCreate", "resource": "Media",
      "fields": { "title": { "en-US": "{ /payload/fields/prompt }" },
                  "file":  { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } } }, "name": "img" },
    { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_artwork" } },
      "fields": { "prompt": { "en-US": "{ /payload/fields/prompt }" }, "image": { "en-US": "{ /img/sys/id }" } } } ] }

Medianame で作成し、ResourceCreate(Content) が { /img/sys/id } を参照フィールドに差し込みます。MediaContent と同じ fields モデルを使います。file はインジェスト指示 { source, encoding } です。ファイルインジェストなので Async です。

9. base64 画像を Media に

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "Http", "method": "POST", "url": "https://api.img.com/gen",
      "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
      "body": { "prompt": "{ /payload/fields/prompt }" }, "name": "gen" },
    { "type": "ResourceCreate", "resource": "Media",
      "fields": { "file": { "en-US": { "source": "{ /gen/body/data/0/b64_json }", "encoding": "base64" } } } } ] }

10. モデレーション後に条件付きで publish または削除

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "Http", "method": "POST", "url": "https://api.mod.com/check",
      "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
      "body": { "text": "{ /payload/fields/body }" }, "name": "mod" },
    { "type": "If", "condition": { "==": [ "{ /mod/body/flagged }", true ] },
      "then": [ { "type": "ResourceDelete",  "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } } ],
      "else": [ { "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } } ] } ] }

11. try/catch: 外部失敗時に fallback

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "Try",
      "body": [
        { "type": "Http", "method": "POST", "url": "https://primary.api/gen",
          "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
          "body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 8000, "name": "resp" },
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
          "fields": { "text": { "en-US": "{ /resp/body/text }" }, "source": { "en-US": "primary" } } } ],
      "catch": [
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
          "fields": { "text": { "en-US": "生成に失敗しました" }, "error": { "en-US": "{ /error/message }" }, "source": { "en-US": "fallback" } } } ] } ] }

並列

12. 並列の外部呼び出し 2 件を合わせて Content

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "Parallel", "branches": [
      [ { "type": "Http", "method": "GET", "url": "https://api.a.com/x", "name": "a" } ],
      [ { "type": "Http", "method": "GET", "url": "https://api.b.com/y", "name": "b" } ] ] },
    { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_merged" } },
      "fields": { "left": { "en-US": "{ /a/body/value }" }, "right": { "en-US": "{ /b/body/value }" } } } ] }

13. 加入審査: 並列スコア後に and 判定

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "Parallel", "branches": [
      [ { "type": "Http", "method": "POST", "url": "https://api.fraud.com/score",
          "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ],
          "body": { "email": "{ /payload/fields/email }" }, "name": "fraud" } ],
      [ { "type": "Http", "method": "GET", "url": "https://api.credit.com/v1/{ /payload/fields/userId }/score",
          "headers": [ { "key": "x-api-key", "value": "...", "secret": true } ], "name": "credit" } ] ] },
    { "type": "If",
      "condition": { "and": [ { "<": [ "{ /fraud/body/risk }", 0.5 ] }, { ">=": [ "{ /credit/body/score }", 700 ] } ] },
      "then": [
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_account" } },
          "fields": { "email": { "en-US": "{ /payload/fields/email }" }, "status": { "en-US": "approved" } } },
        { "type": "Return", "value": { "decision": "approved" }, "statusCode": 201 } ],
      "else": [ { "type": "Return", "value": { "decision": "manual-review" }, "statusCode": 202 } ] } ] }

URL パスにも { /ptr } を挿入できます。ブランチの結果はジョイン後に参照します。外部呼び出しは 2 件です(3 件以下)。

反復と集計

14. 配列入力で N 件の Content (Loop over)

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "Loop", "over": "{ /payload/fields/items }", "as": "item", "maxIterations": 100,
      "body": [
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_item" } },
          "fields": { "name": { "en-US": "{ /item/name }" }, "qty": { "en-US": "{ /item/qty }" } } } ] } ] }

15. counted loop (for): スロットシード

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "Loop", "for": { "from": 1, "to": 5 }, "as": "i", "maxIterations": 100,
      "body": [
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_slot" } },
          "fields": { "index": { "en-US": "{ /i }" }, "status": { "en-US": "open" } } } ] } ] }

forfrom から to まで 含む 範囲です(整数リテラル、step はデフォルト 1)。as が現在のカウンターを { /i } にバインドします。

16. cascade 削除 (PageRead, Loop, Delete)

{ "method": "Delete", "executionMode": "Sync",
  "statements": [
    { "type": "ResourcePageRead", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
      "where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "limit": 100, "name": "comments" },
    { "type": "Loop", "over": "{ /comments/items }", "as": "c", "maxIterations": 100,
      "body": [ { "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /c/sys/id }" } } } ] },
    { "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } } ] }

100 件を超える場合は 18. 全ページネーション走査 で処理します。

17. ループ累積: SetVar 合計

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "SetVar", "var": "total", "value": 0 },
    { "type": "Loop", "over": "{ /payload/fields/items }", "as": "row", "maxIterations": 100,
      "body": [ { "type": "SetVar", "var": "total", "value": { "+": [ "{ /vars/total }", "{ /row/qty }" ] } } ] },
    { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_summary" } },
      "fields": { "totalQty": { "en-US": "{ /vars/total }" } } } ] }

18. 全ページネーション走査

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "SetVar", "var": "cursor",  "value": null },
    { "type": "SetVar", "var": "hasMore", "value": true },
    { "type": "Loop", "while": "{ /vars/hasMore }", "maxIterations": 1000,
      "body": [
        { "type": "ResourcePageRead", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
          "where": { "fields.status": { "eq": "draft" } }, "limit": 100, "cursor": "{ /vars/cursor }", "name": "page" },
        { "type": "Loop", "over": "{ /page/items }", "as": "p", "maxIterations": 100,
          "body": [ { "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /p/sys/id }" } } } ] },
        { "type": "SetVar", "var": "cursor",  "value": "{ /page/next }" },
        { "type": "SetVar", "var": "hasMore", "value": { "!!": "{ /page/next }" } } ] } ] }

cursorwhileSetVar で全ページを走査します。外部呼び出しは Loop の body 内では禁止のため、ここではリソース操作のみを使います。

19. メールアドレス一覧で id をバッチ収集 (merge)

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "SetVar", "var": "ids",     "value": [] },
    { "type": "SetVar", "var": "missing", "value": [] },
    { "type": "Loop", "over": "{ /payload/fields/emails }", "as": "email", "maxIterations": 100,
      "body": [
        { "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_account" } },
          "where": { "fields.email": { "eq": "{ /email }" } }, "name": "acc" },
        { "type": "If", "condition": { "!!": "{ /acc/sys/id }" },
          "then": [ { "type": "SetVar", "var": "ids",     "value": { "merge": [ "{ /vars/ids }",     [ "{ /acc/sys/id }" ] ] } } ],
          "else": [ { "type": "SetVar", "var": "missing", "value": { "merge": [ "{ /vars/missing }", [ "{ /email }" ] ] } } ] } ] },
    { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_campaign" } },
      "fields": { "recipients": { "en-US": "{ /vars/ids }" }, "unresolved": { "en-US": "{ /vars/missing }" } } } ] }

読み取り(ResourceFind) は外部呼び出しではないため Loop の body で許可されます。存在と不在をそれぞれ merge で累積します。

サガと並行性

20. 決済サガ (Try/catch/finally)

{ "method": "Post", "executionMode": "Async",
  "statements": [
    { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_order" } },
      "fields": { "sku": { "en-US": "{ /payload/fields/sku }" }, "status": { "en-US": "reserved" } },
      "publish": false, "name": "order" },
    { "type": "Try",
      "body": [
        { "type": "Http", "method": "POST", "url": "https://api.pay.com/charge",
          "headers": [ { "key": "Authorization", "value": "Bearer sk-...", "secret": true } ],
          "body": { "amount": "{ /payload/fields/amount }", "ref": "{ /order/sys/id }" }, "timeoutMs": 10000, "name": "pay" },
        { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } },
          "fields": { "status": { "en-US": "paid" }, "txId": { "en-US": "{ /pay/body/transactionId }" } }, "publish": true },
        { "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 } ],
      "catch": [
        { "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /order/sys/id }" } } },
        { "type": "Return", "value": { "reason": "payment failed", "detail": "{ /error/message }" }, "isError": true, "statusCode": 402 } ],
      "finally": [
        { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_paylog" } },
          "fields": { "orderRef": { "en-US": "{ /order/sys/id }" }, "amount": { "en-US": "{ /payload/fields/amount }" } } } ] } ] }

予約(draft)後、決済成功時は確定と publish と 201、失敗時は catch が予約削除(補償)と 402finally は常にログです。削除補償は新しい sys.id になるため参照が壊れる限界の範囲です(実行セマンティクスのトランザクションなしと補償 を参照)。

21. 楽観ロック CAS

{ "method": "Post", "executionMode": "Sync",
  "statements": [
    { "type": "ResourcePageRead", "resource": "Content", "contentType": { "sys": { "id": "ct_stock" } },
      "where": { "fields.sku": { "eq": "{ /payload/fields/sku }" } }, "limit": 1, "name": "stock" },
    { "type": "If", "condition": { "<": [ "{ /stock/items/0/fields/qty/en-US }", "{ /payload/fields/amount }" ] },
      "then": [ { "type": "Return", "value": { "reason": "out of stock" }, "isError": true, "statusCode": 409 } ] },
    { "type": "Try",
      "body": [
        { "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /stock/items/0/sys/id }" } },
          "version": "{ /stock/items/0/sys/version }",
          "fields": { "qty": { "en-US": { "-": [ "{ /stock/items/0/fields/qty/en-US }", "{ /payload/fields/amount }" ] } } } },
        { "type": "Return", "value": { "ok": true } } ],
      "catch": [
        { "type": "Return", "value": { "reason": "version conflict, 再試行" }, "isError": true, "statusCode": 409 } ] } ] }

在庫を読み取って新鮮な sys.version を確保したうえで、そのバージョンで差し引きます(version)。読み取りと書き込みの間に別の実行が値を変更していた場合、バージョン不一致で abort され、catch が 409 を返します。在庫不足の guard は Try の外です(正常な早期終了)。