쿡북 (실전 예시 모음)

최종 수정: 2026년 7월 16일

다양한 시나리오를 완결된 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 }" } } ] } ] }

cursorwhile, SetVar로 전 페이지를 순회합니다. 외부 호출은 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가 예약 삭제(보상)와 402, finally는 항상 로그입니다. 삭제 보상은 새 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 밖입니다(정상 조기 종료).