쿡북 (실전 예시 모음)
최종 수정: 2026년 7월 16일
다양한 시나리오를 완결된 ScriptDefinition으로 보여줍니다. 문법 근거는 Statement 카탈로그와 값 표현식, 실행과 제약은 실행 시맨틱, 제약, 보안을 참조하세요. 모든 예시는 쓰기 fields 값이 로케일 맵({ "<locale>": 값 })이며, 예시 로케일은 en-US로 통일했습니다. 외부 호출(Http)이나 Media 파일 인제스트가 있는 예시는 executionMode가 "Async"입니다.
목차
- 기본 CRUD: 1. Content 생성과 발행 · 2. 계산 값으로 update · 3. 읽기 전용 GET · 4. 단건 조회 후 guard 후 승인
- 조회와 upsert: 5. slug upsert · 6. 동적 필드 키와 로케일 patch
- 외부 API: 7. 크레딧 선차감(CAS)과 LLM 호출·환불 · 8. 이미지 URL을 Media로 · 9. base64 이미지를 Media로 · 10. 모더레이션 후 조건부 처리 · 11. try/catch fallback
- 병렬: 12. 병렬 호출 후 합치기 · 13. 가입 심사
- 반복과 집계: 14. 배열 입력으로 N개 생성 · 15. counted loop 시드 · 16. cascade 삭제 · 17. 루프 누적 합계 · 18. 전체 페이지네이션 순회 · 19. id 배치 수집
- 사가와 동시성: 20. 결제 사가 · 21. 낙관적 잠금 CAS
기본 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하고 catch가 409를 냅니다. 외부 호출은 하지 않으므로 동시 요청이 이중 차감되지 않습니다. 차감이 확정된 뒤에만 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 }" } } } ] }Media를 name으로 만들고, ResourceCreate(Content)가 { /img/sys/id }를 참조 필드에 꽂습니다. Media도 Content와 동일한 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" } } } ] } ] }for는 from부터 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 }" } } ] } ] }cursor와 while, 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 밖입니다(정상 조기 종료).
관련 문서
- Statement 카탈로그: 예시에 쓰인 문들의 필드와 결과.
- 값 표현식: 참조, JsonLogic, 로케일 맵 규칙.
- 실행 시맨틱, 제약, 보안: 실행 순서, 보상, 낙관적 잠금, 제약.
- Script 개요: 최상위 구조와 실행 모드.
