Cookbook (Exemples pratiques)

Dernière mise à jour : 18 juillet 2026

Divers scénarios sont présentés sous forme de ScriptDefinition complets. Pour le fondement de la syntaxe, voir le Catalogue des statements et les Expressions de valeur ; pour l'exécution et les contraintes, voir Sémantique d'exécution, contraintes et sécurité. Dans tous les exemples, la valeur d'écriture de fields est un mappage de locale ({ "<locale>": valeur }), et la locale des exemples a été unifiée sur en-US. Les exemples qui effectuent un appel externe (Http) ou qui ingèrent un fichier Media ont executionMode défini sur "Async".

Table des matières

CRUD de base

1. Créer et publier un 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. Mise à jour avec une valeur calculée (compteur de vues +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 en lecture seule : ma liste de commandes

{ "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 ne sert pas qu'à écrire : il s'utilise aussi comme endpoint BFF de lecture. Avec createdBy: ":self", il ne récupère que « les miens » et renvoie le résultat tel quel.

4. Lire un élément, appliquer un guard, puis approuver

{ "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 } } ] }

En liant un élément unique à un nom avec ResourceRead, on le référence directement via { /order/fields/... } (pas besoin de items/0). S'il n'existe pas, la lecture génère une erreur (on peut l'envelopper dans un Try).

Recherche et upsert

5. upsert par slug (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 lie directement la première correspondance (ou null s'il n'y en a pas), et { "!!": "{ /found/sys/id }" } fait un branchement selon qu'elle existe ou non.

6. Clé de champ dynamique et patch de locale dynamique

{ "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 }" } } } ] }

La clé de champ et la clé de bucket de locale sont toutes deux des références { /ptr }. On s'en sert pour placer une traduction dans un bucket de locale précis.

API externe

7. Guard de crédits, prélèvement anticipé (CAS), appel LLM, remboursement en cas d'échec (exemple phare)

{ "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, réessayer" }, "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 } ] } ] }

Un guard vérifie d'abord si le solde est suffisant, puis prélève avant l'appel externe. Le prélèvement pose un verrou optimiste (CAS) sur le sys.version du wallet. Si une autre exécution a modifié le wallet entre la lecture du solde et le prélèvement, l'opération est abandonnée pour cause de conflit de version et catch renvoie 409. Comme aucun appel externe n'a été effectué, les requêtes concurrentes ne font pas l'objet d'un double prélèvement. Ce n'est qu'une fois le prélèvement confirmé que le LLM est appelé, et si cet appel échoue, catch rajoute le montant prélevé (cost) pour effectuer un remboursement (compensation), puis renvoie 502. L'ordre consiste à confirmer la facturation avant l'appel externe irréversible et à ne compenser qu'en cas d'échec. La clé secrète est placée dans un en-tête secret:true. Pour les limites de la compensation, voir L'absence de transaction et la compensation dans la Sémantique d'exécution.

8. Transformer une image (URL) en Media et l'attacher à un 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 }" } } } ] }

On crée le Media avec un name, et ResourceCreate (Content) place { /img/sys/id } dans le champ de référence. Media utilise le même modèle de fields que Content. file est la directive d'ingestion { source, encoding }. Comme il s'agit d'une ingestion de fichier, c'est en Async.

9. Image base64 vers 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. Publication ou suppression conditionnelle après modération

{ "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 en cas d'échec externe

{ "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": "Échec de la génération" }, "error": { "en-US": "{ /error/message }" }, "source": { "en-US": "fallback" } } } ] } ] }

Parallèle

12. Fusionner deux appels externes parallèles dans un 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. Examen d'inscription : scores en parallèle, puis décision par 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 } ] } ] }

On peut aussi insérer { /ptr } dans le chemin de l'URL. Les résultats des branches se référencent après la jointure. Il y a deux appels externes (au plus trois).

Boucles et agrégation

14. N Content à partir d'un tableau en entrée (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) : amorcer des slots

{ "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 va de from à to inclus (littéraux entiers, step par défaut 1). as lie le compteur courant à { /i }.

16. Suppression en 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 }" } } } ] }

Au-delà de 100, on procède avec 18. Parcourir toutes les pages.

17. Accumulation en boucle : somme avec 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. Parcourir toutes les pages

{ "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 et SetVar permettent de parcourir toutes les pages. Comme les appels externes sont interdits dans le corps d'une Loop, on n'utilise ici que des opérations sur les ressources.

19. Collecte groupée d'id à partir d'une liste d'e-mails (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 }" } } } ] }

Une lecture (ResourceFind) n'est pas un appel externe : elle est donc autorisée dans le corps d'une Loop. La présence et l'absence sont chacune accumulées avec merge.

Saga et concurrence

20. Saga de paiement (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 }" } } } ] } ] }

Après la réservation (draft) : en cas de succès du paiement, confirmation, publication et 201 ; en cas d'échec, catch supprime la réservation (compensation) et renvoie 402 ; finally journalise toujours. La compensation par suppression produit un nouveau sys.id, elle relève donc de la limite où les références sont rompues (voir L'absence de transaction et la compensation dans la Sémantique d'exécution).

21. CAS à verrouillage optimiste

{ "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, réessayer" }, "isError": true, "statusCode": 409 } ] } ] }

On lit le stock pour obtenir un sys.version à jour, puis on effectue le prélèvement avec cette version (version). Si une autre exécution a modifié la valeur entre la lecture et l'écriture, l'opération est abandonnée pour cause de conflit de version et catch renvoie 409. Le guard de rupture de stock est placé hors du Try (sortie anticipée normale).