Statement Catalog

Each element of the statements array is a single statement. This document catalogs the fields, behavior, and result of all 25 statement types. Every value slot follows the rules of Value Expressions (reference, literal, JsonLogic, locale map), with two exceptions: the pattern of Regex and the key of Cache (see Regex and Cache).

Statement summary

CategorytypeOne-line summary
Resource writesResourceCreateCreate Content/Media (optionally publish)
ResourceUpdateFull replacement of Content/Media fields (any field/locale not provided is deleted)
ResourcePatchPartial merge of Content/Media fields (only the specified fields/locales; a literal null deletes)
ResourceDeleteDelete (Draft/Archived only; if Published, unpublish first)
ResourcePublish / ResourceUnpublishPublish / unpublish
ResourceArchive / ResourceUnarchiveArchive / unarchive
Resource readsResourceReadRead a single item by id
ResourceFindFirst matching single item by filter (null if none)
ResourceForEachIterate internally over the resources matching a filter, running onEach per item
ResourceCountCount only the number of items matching a filter (the items are not read)
ExternalHttpExternal HTTP call ({ status, body })
EmailSendSend one email through a registered EmailAccount
VariablesSetVarDeclare / update a script-scoped variable
CachingCacheRead, write, or remove a short-lived cache private to that Script
Value parsingParseJsonParse JSON text into a value (object, array, scalar) and bind it
Signing and textSignatureVerify that a received signature code matches the one built with a secret key (Boolean)
HashCompute an unkeyed digest (string)
RegexApply a regular expression. Whether it matched (Boolean) or the captured groups (array)
Control flowIfConditional branch
LoopIterate (foreach / while / counted)
ParallelRun branches concurrently
ReturnReturn a result and terminate early
TryException handling (catch/finally)

A Content statement that does not name its target by id must state the Content Type it works on. On ResourceFind, ResourceForEach, and ResourceCount, contentType is required when resource is "Content". There is no Content query that spans a whole Space. ResourceCreate states the Content Type to create in as well. Media carries no scope because there is one set per Space, and the statements that name their target by id (ResourceRead, ResourceUpdate, ResourcePatch, ResourceDelete, and the publish and archive statements) have target, so they need no scope.

Cyclic calls are capped at 3. Turning on propagateEvents (off by default) on the resource-write statements above (ResourceCreate, ResourceUpdate, ResourcePublish, etc.) raises change events, and those events can run a Script again through a Webhook. Such a chain (Script → event → Webhook → Script → …) continues at most 3 times; beyond that it is dropped automatically to prevent infinite loops.

Common fields

{ "type": "<StatementType>", "name": "<optional, unique within script>", /* ...type-specific fields... */ }
  • type: The discriminator. One of the values in the table above (required).
  • name: Optional. When set, the result is bound into the context at /<name>, so later statements can reference it as { /<name>/... }. Omit it if you don't use the result.
  • Binding-name rules: name is a key laid directly onto the context root, so it is validated on save. It may use only ASCII letters (a-z, A-Z), digits, _, and - (it has to work as a JSON Pointer key, so any other character, or an empty name, is rejected), must not equal a reserved root (payload, rawPayload, headers, vars, error, now), and must be unique within a single Script. On a format violation, a reserved word, or a duplicate, the save is rejected.

Entity reference shape

Entity references such as contentType and target are unified into a single shape: { "sys": { "id": <value expression> } }. Only sys.id is needed; the target type is inferred from resource (sys.type and sys.targetType are omitted).

  • contentType.sys.id is usually a literal (e.g. "ct_post").
  • target.sys.id is usually a { /ptr } value expression (resolved at runtime; e.g. { /payload/sys/id }).

resource

Resource-family statements specify the target kind with resource: "Content" | "ContentType" | "Media" | "ServiceUser".

Content Type is accepted by ResourceCount only. Writing it in any other statement makes the save fail. Creating or changing a Content Type itself is CMA's job, not a Script's.

A ServiceUser (a member who signed up for the product) is read-only. Only the three read statements (ResourceRead, ResourceFind, ResourceForEach) accept this value; writing it in a write statement makes the save fail (see Errors). The rules are covered in Reading the member directory.

Resource writes

Every write statement has propagateEvents (default false). Setting it to true makes that write raise a change event, which runs downstream work such as a Webhook. The default does not raise one (a quiet system write).

ResourceCreate

Creates a Content or Media. Content and Media share the fields model, and values are locale maps.

FieldApplies toDescription
resourceCommon"Content" or "Media" (required)
contentTypeContentThe Content Type to create ({ sys: { id } }). Required for Content
fieldsCommonField map { "<field>": { "<locale>": value } }. Each populated field requires the default locale bucket. Content keys follow the Content Type definition; Media keys are fixed (title, description, file)
localeCommon(Convenience) When provided, each value in fields is auto-wrapped as { <locale>: value }
publishCommonPublish after the write (exposed on CDA/ACDA). Default true
  • Media file: The fields.file.{locale} value is an ingest instruction { "source": <value expression>, "encoding": "url"|"base64" } (both required). On a write that includes a file, the engine performs the ingest (downloading it for url, or decoding it for base64, then uploading and processing it). This ingest declares no time, so it comes out of the 30-second base budget (Time budget), and it does not count toward the external-call limit. You can also create a fileless Media. If publish:true but there is no file or processing is incomplete, the publish step errors; if publish:false, it stays Draft.
  • Result (name binding): The created resource. { /<name>/sys/id }, { /<name>/fields/<field>/<locale> }.
// Content
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "fields": { "title": { "en-US": "{ /payload/fields/title }" } }, "publish": true, "name": "post" }
 
// Media. file is an ingest instruction
{ "type": "ResourceCreate", "resource": "Media",
  "fields": {
    "title": { "en-US": "{ /payload/fields/prompt }" },
    "file":  { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } }
  }, "name": "img" }

ResourceUpdate

Fully replaces the fields of the target Content or Media (PUT). Whatever you pass in fields becomes the new set of fields, and any field and locale not present here is removed. To change only part of it, use ResourcePatch.

FieldDescription
resource"Content" or "Media"
targetThe target ({ sys: { id } }, required). The id is usually { /ptr }
fieldsThe complete set of fields to write. Values are locale maps. Because this is a full replacement, any field and locale not present here is removed. For Media, file is an ingest instruction (see ResourceCreate above); listed files are always re-ingested, and files for locales not provided are deleted
locale(Convenience) Auto-wraps fields
version(Optional) A value expression (Int). Optimistic locking. When provided, the update runs only if it matches the target's current sys.version; on a mismatch it aborts with a version-conflict error (catchable with Try). If omitted, there is no check (last-write-wins)
publishRepublish after the update. Default true

Using Update to change only a Media's metadata leaves out file, so every file is deleted (because it is a full replacement). For a partial change, always use ResourcePatch.

{ "type": "ResourceUpdate", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
  "fields": { "title": { "en-US": "Hello", "ko-KR": "안녕" }, "status": { "en-US": "published" } } }

ResourcePatch

Partially merges the fields of the target Content or Media (PATCH). It overwrites only the fields (and the locales within them) you pass in fields, and leaves any field and locale you don't mention untouched. The value shape, locale, version, and publish are the same as ResourceUpdate.

FieldDescription
resource"Content" or "Media"
targetThe target ({ sys: { id } }, required). The id is usually { /ptr }
fieldsThe fields to overwrite. Values are locale maps. Updates only the specified fields and locale buckets (the rest are kept). If a value is a literal null, that (field, locale) is deleted. For Media, file is an ingest instruction (see ResourceCreate above)
locale(Convenience) Auto-wraps fields
version(Optional) Same as ResourceUpdate (optimistic locking)
publishRepublish after the update. Default true
  • Delete a specific locale or file: Give a literal null as the value. For example: "title": { "fr-FR": null } (deletes the fr-FR title), "file": { "en-US": null } (deletes the en-US file). A value expression that evaluates to null at runtime is not a deletion but an error (only a literal null deletes).
  • Giving an ingest instruction to a Media file replaces that locale's file. If you don't provide a file, it is kept.
// +1 to viewCount(en-US) only. title, other locales, and everything else are kept as-is
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
  "fields": { "viewCount": { "en-US": { "$+": [ "{ /payload/fields/viewCount }", 1 ] } } } }

ResourceDelete

Deletes the target. Only the Draft and Archived statuses can be deleted. If the target is Published or Changed the delete is rejected, so you must ResourceUnpublish first. For a Media, the delete is rejected while the file is being processed. It does not auto-unpublish (same as CMA/ACMA).

FieldDescription
resource"Content" or "Media"
targetThe target ({ sys: { id } }, required)
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }

ResourcePublish, ResourceUnpublish, ResourceArchive, ResourceUnarchive

Independently control the target's publish and archive state. All four share the same fields. The status precondition for each operation is the same as CMA/ACMA. ResourcePublish is not allowed from Archived and requires the file processing to be complete. ResourceUnpublish is allowed only from Published or Changed, ResourceArchive only from Draft, and ResourceUnarchive only from Archived.

FieldDescription
resource"Content" or "Media"
targetThe target ({ sys: { id } }, required)
version(Optional) A value expression (Int). Optimistic locking. When provided, the operation runs only if it matches the current sys.version
{ "type": "ResourcePublish",   "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceUnpublish", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceArchive",   "resource": "Media",   "target": { "sys": { "id": "{ /m/sys/id }" } } }

Resource reads

ResourceRead and ResourceFind read a resource and bind it as a value, and ResourceCount only counts. None of the three change state (no propagateEvents). ResourceForEach is also a read for the query itself, but if you put resource-write statements in onEach, that write runs per item and changes state.

All four statements (ResourceRead, ResourceFind, ResourceForEach, ResourceCount) use from (default Current) to choose which stored version to read. Current is the latest draft that the content studio sees (the value CMA/ACMA reads); Published is the published snapshot (the value CDA/ACDA delivers, as of the last publish). A ServiceUser is never published, so it accepts only Current (see Reading the member directory).

In addition, ResourceFind, ResourceForEach, and ResourceCount turn Advanced Search on and off via advanced (default true). If you do not state it, it is on. It is Content-only, so it is ignored for Media and ServiceUser reads. When on, where can use the regex, near, and within operators and full-text search on text (on a full-text-enabled LongText field, eq also matches items that contain the value, by partial and fuzzy matching), and order can sort by fields.*. When off, those three operators are rejected, eq on text is exact match, and prefix and the comparison and list operators are unaffected by Advanced Search. A newly created or edited item takes a short moment (about 1 second) to be reflected in Advanced Search, so it may be missed by the immediately following Advanced Search query. Because it is on by default, this delay applies to every query unless you set advanced to false. If you must read a just-written item right away, use ResourceRead by id (the primary store, with no reflection delay) or query by the sys.id the write returned.

createdBy: ":self" in a where means "only what the current caller created". It cannot be used in a Script that allows anonymous calls (anonymousCallEnabled): there :self resolves to the author rather than the caller, which would silently open the author's own resources, so such a definition is rejected on save (see Anonymous calls).

In where and order, write a content field as fields.<field> (the bare name alone is not recognized). For fields.<field>, the Space default locale is automatically applied, so you do not attach a locale yourself. The fields.status and fields.slug in the examples below are themselves default-locale lookups. Only when you want to query a specific (non-default) locale do you state it explicitly as fields.<field>.<locale> (e.g. fields.title.ko-KR). sys.* (such as sys.createdAt) and createdBy (:self) are written as-is, without fields.. The full rules are in Locales in where and order in Value Expressions.

Reading the member directory (ServiceUser)

ResourceRead, ResourceFind, and ResourceForEach accept "ServiceUser" in resource and read that Space's member directory (ResourceCount does not; see ResourceCount below). Use it to confirm who owns an order, or to find a member by email address and pass their sys.id to the next statement. The rules below are common to all three.

  • Reads only. ResourceCreate, ResourceUpdate, ResourcePatch, ResourceDelete, and the publish and archive statements do not accept "ServiceUser", and such a definition is rejected at save time. This is not something you can open up by adding permissions. There is no path at all for a Script to change a member, which is why it is rejected as a mis-written statement rather than as a permission error.
  • The author needs member-directory permission for the save to pass. It is not checked through a permission map the way Content and Media are. Instead it looks for SETTING_SERVICE_LOGIN (or SETTING_ALL) in the author's SpaceRole settings, because the member directory is a resource governed by Space settings on every other path as well. Without it, the save is rejected (see Security model).
  • from accepts only Current. A member is not a published resource, so passing Published makes execution fail.
  • contentType and advanced are ignored. The member directory is not divided by Content Type (there is one set per Space), and Advanced Search is Content-only.
  • In where, sys.email accepts only the exact-match operators (eq, ne, in, nin). A member's address is stored encrypted, so ordering comparisons and prefix are meaningless there. Passing any other operator makes execution fail rather than quietly returning zero results.
  • The result is the ServiceUser resource itself. Reference it as { /<name>/sys/id }, { /<name>/nickname }, and so on; its structure is covered in the ServiceUser reference. To email a member you found, do not extract the address; pass their sys.id to toServiceUser on EmailSend (the engine resolves the address immediately before sending, so the member's address never enters the Script variable space).
// Find one member by email address. null if there is none
{ "type": "ResourceFind", "resource": "ServiceUser",
  "where": { "sys.email": { "eq": "{ /payload/fields/email }" } }, "name": "member" }

ResourceRead

Reads a single item by id (get-by-id). The result binds the entire resource to the name.

FieldDescription
resource"Content", "Media", or "ServiceUser"
targetThe target ({ sys: { id } }). The id is a value expression
from(Optional) Current (default, the latest draft) or Published (the published snapshot). ServiceUser accepts Current only
  • Result: the resource itself is bound to the statement's name, so you reference it directly as { /<name>/sys/id } and { /<name>/fields/<field>/<locale> } (with the "name": "order" of the example below, that is { /order/sys/id }). It is not a list, so no array index is involved.
  • If the target does not exist, it errors. You can wrap it in Try to handle that.
{ "type": "ResourceRead", "resource": "Content",
  "target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" }

ResourceFind

Reads the first matching single item by filter. If there is none, it is null. Use it to find one record by a unique business key (slug, email, sku).

FieldDescription
resource"Content", "Media", or "ServiceUser"
contentTypeThe Content Type to search within ({ sys: { id } }). Required for Content. Ignored for Media and ServiceUser
whereThe filter ({ "<field>": { "<op>": <value> } }). Operators are those in the operator list (regex/near/within require advanced). createdBy: ":self" supported. For ServiceUser, sys.email accepts only eq, ne, in, and nin (see Reading the member directory)
orderThe sort that determines the "first" one when multiple match (e.g. "-sys.createdAt")
from(Optional) Current (default, the latest draft) or Published (the published snapshot). ServiceUser accepts Current only
advanced(Optional) Run via Advanced Search. Content-only (Media and ServiceUser ignored). Default true. See the Resource reads note above
  • Result: Binds the first matching resource to the statement's name. Reference it directly as { /<name>/fields/<field>/<locale> }. Since it is null when there is none, branch on existence with { "==": [ "{ /<name> }", null ] } (the typical find-then-upsert pattern).
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
  "where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" }

ResourceForEach

Iterates internally over the resources matching a filter and runs onEach for each item. It is a statement for performing an action on each item rather than building a collection to use as a value. Use it for repetitive work such as bulk-publishing drafts, bulk-editing Content that matches a condition, or sending/syncing each item externally. To read just one, use ResourceRead (by id) or ResourceFind (by filter).

FieldDescription
resource"Content", "Media", or "ServiceUser" (required)
contentTypeThe Content Type to iterate within ({ sys: { id } }). Required for Content. Ignored for Media and ServiceUser
whereThe filter ({ "<field>": { "<op>": <value> } }). The meaning is the same as ResourceFind's where (including the ServiceUser sys.email restriction). Operators are those in the operator list (regex/near/within require advanced). createdBy: ":self" supported
orderThe sort (e.g. "sys.createdAt,sys.id"). Without it, the platform default order
fromCurrent (default, the latest draft) or Published (the published snapshot). ServiceUser accepts Current only
advancedIterate via Advanced Search. Content-only (Media and ServiceUser ignored). Default true. See the Resource reads note above
limit(Optional, 1 or more) The cap on the total number processed (not a page size). Without it, iterates up to the platform ceiling (10,000 items)
name(Optional) The name to bind the current item to. It is re-bound on every iteration and referenced inside onEach as { /<name> } (same lifetime as Loop's name; after the iteration ends, the last item stays bound). Omit it if you do not reference the item
onEachThe array of child statements to run for each item (required)
  • It does not bind a collection (it is foreach, not map). There is no { items, next } and no cursor. Rather than returning the iteration result as a value, it runs onEach per item. If you need a list, gather it yourself with SetVar. If all you need is a count, use ResourceCount.
  • It is not an infinite iteration even without limit. Without one, it iterates up to the platform ceiling (10,000 items), and if it hits that ceiling with matches remaining, it fails (so as not to report success while leaving untouched items). Conversely, reaching a declared limit is an intended stop and terminates normally. A limit above the ceiling is rejected on save.
  • There is no cursor. Completing the run is a success; being cut off partway (wall-clock or quota exceeded, or an unhandled failure in onEach) is a failure, and the error points to which item failed and why. Resumption is expressed by the author with their own data (leave where as "unprocessed" and mark completion at the end of onEach, so a re-run continues from what remains).
  • It enters the time budget as a multiplication. The time this statement declares is the time onEach declares multiplied by the number of items processed (limit, or 10,000 when absent) (Time budget). Because it is a composite statement that owns children, it itself does not count toward the external-call leaf budget; the external-call statements inside onEach are what count against the budget.
  • onEach can hold external calls (Http, EmailSend) or Media file ingestion like any other statement (the same as Loop's body). Processing a resource-query result once per item is the reason this statement exists.
// find all draft posts and publish each
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "where": { "fields.status": { "eq": "draft" } }, "order": "sys.createdAt,sys.id",
  "from": "Current", "advanced": false, "name": "post",
  "onEach": [
    { "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /post/sys/id }" } } }
  ] }

ResourceCount

Counts only how many items match a filter. It does not read the items, so use it when you need a number rather than a list. This is where you check the remaining stock, decide whether the same value already exists, or test whether a limit has been exceeded.

FieldDescription
resource"Content" or "ContentType" (required). Media and ServiceUser cannot be counted, and writing them makes the save fail
contentTypeThe Content Type to count within ({ sys: { id } }). Required for Content. Ignored when counting Content Type (there is one set per Space)
whereThe filter. The meaning is the same as ResourceFind's where. Every matching item is counted
from(Optional) Current (default, the latest draft) or Published (the published snapshot)
advanced(Optional) Run via Advanced Search. Content-only (ignored when counting Content Type). Default true. See the Resource reads note above
name(Optional) The name to bind the count to
  • Result: Binds the matched count to the statement's name. Reference it as { /<name> } and use it in comparisons and branches.
  • It does not return the items. If you need them, use ResourceFind (the first matching single item) or ResourceForEach (run per item).
  • Do not iterate with ResourceForEach just to get a count. Iteration takes the time budget multiplied by the number of items (Time budget), and it fails if it hits the platform ceiling with matches remaining. When you only need the count, this statement does it in one go.
  • There is no order and no limit. Counting needs no ordering, and everything that matches is counted.
// count how many comments are on this post
{ "type": "ResourceCount", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
  "where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "name": "commentCount" }

External

Http

Calls an external HTTP endpoint. As an external call it counts toward the per-plan external-call limit, and it enters the time budget as timeoutMs (30 seconds when absent) × (1 + retry).

FieldDescription
method"GET", "POST", "PUT", "PATCH", "DELETE"
urlThe target URL (a value expression; { /ptr } can be interpolated)
headers[{ "key", "value", "secret"? }]. value is a value expression. A secret:true header is treated as CMA (administrator) only: it is not exposed to end users and is decrypted only immediately before the request is sent. Putting Content-Type here makes body serialize into that format (below)
bodyThe request body (a value expression or JSON). The Content-Type header decides which format it goes out in
timeoutMsThe timeout for this call (ms)
retryThe number of retries when the response status is 400 or higher. Default 0; the ceiling is 2
ignoreStatusCodeWhether to treat this call as a failure when the final status (after retries) is 400 or higher. When false (default) it is treated as a failure and becomes a Try/catch target. When true it is not treated as a failure and { status, body } is bound as-is (the caller branches on status itself)
responseTypeWhat to receive the response body as. "Json" (default) parses it into an object or array; "Text" receives it as a string
  • Result: { status, body }, reachable through the statement's name as { /<name>/status } and { /<name>/body/... }. The shape of body is set by responseType.
  • responseType applies to a success response only. The body of a response whose status is 400 or higher is bound for diagnostics regardless of what you declared (the parsed value when it is JSON, a string otherwise).
  • When it is "Json" and the body is not JSON, this call fails (a Try/catch target). For an API that does not return JSON, receive the response as "Text", then parse it with ParseJson when you need it as a value.
  • "Text" is decoded with the charset of the response Content-Type, and treated as UTF-8 when there is no charset. When the body is empty, body is null in both cases.
  • Response size limit: The response body is at most 10MiB. If it exceeds that, this call fails with an exception and can be handled like any other runtime failure with Try/catch (this is a size-based failure, so it is not suppressed by ignoreStatusCode).
{ "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, "retry": 1,
  "responseType": "Json", "name": "resp" }

What format the body goes out in

The Content-Type you put in headers decides the serialization format of body. The comparison ignores letter case and parameters such as ;charset=…, and looks only at the leading part. When the header is absent or its value is empty, the request goes out as application/json. This header is attached only when there is a body, so when there is no body the header you wrote goes out as it is. When the same key is given more than once, only the first value is used and they are merged into one.

A body that cannot be carried in the declared format is corrected to a format that can carry it before it is sent. The header never states something different from the actual body.

These are the combinations that go out exactly as declared.

Declared Content-Typebody shapeBody that goes out
application/jsonAnythingJSON
application/x-www-form-urlencodedAn object or arrayorder[id]=A-2481&order[amount]=34000
text/plainA scalarThe value as it is
Anything else (text/xml and such)AnythingJSON

These are the combinations that get corrected because they cannot be carried in the declared format.

Declared Content-Typebody shapeContent-Type that actually goes outBody that goes out
application/x-www-form-urlencodedA scalartext/plain;charset=UTF-8The value as it is
text/plainAn object or arrayapplication/jsonJSON

These two rows state how the request goes out when the pair does not match, and they are not a way to obtain the format you intended. When body is assembled from a value expression, it can turn out to be a scalar depending on the payload at run time, and the correction then happens without an error. If the receiving side objects to the format, fix either the shape of body or the Content-Type to match your intent.

form-urlencoded expands an object into bracket keys and an array into indexes.

bodyExpanded keys and values
{ "order": { "id": "A-2481", "amount": 34000 } }order[id]=A-2481&order[amount]=34000
{ "tags": ["outerwear", "winter"] }tags[0]=outerwear&tags[1]=winter
{ "items": [{ "sku": "TUMBLER-500" }] }items[0][sku]=TUMBLER-500
{ "memo": null }memo=

Keys and values go out percent-encoded in UTF-8. The table above is decoded so that the key structure is visible. Even when a value contains & or +, it is not mistaken for a pair separator or a space and is delivered as it is.

Expanding nesting into bracket keys is a widely used convention, not a specification of the format itself. Check whether the receiving side restores order[id] into a nested object, and if it does not, build body with flat keys.

{ "type": "Http", "method": "POST", "url": "https://api.example.com/oauth/token",
  "headers": [ { "key": "Content-Type", "value": "application/x-www-form-urlencoded" } ],
  "body": { "grant_type": "client_credentials", "client_id": "{ /vars/clientId }" },
  "name": "token" }

EmailSend

Sends one email through a registered EmailAccount. The fields it takes are only those that map straight to SMTP/MIME. There is no template id, scheduled send, or provider-specific extension (if you need those, call that mail service's API directly with Http). The sender (the from address) is not set here; it comes from the EmailAccount that account points to.

FieldDescription
accountThe reference to the EmailAccount to send through ({ sys: { id } }, required). Usually a literal id. If given as a value expression, it is resolved at send time and so cannot be checked on save
toThe recipient address (a value expression). Use exactly one of to and toServiceUser
toServiceUserSpecifies the recipient as a ServiceUser reference ({ sys: { id } }; its sys.id can be a value expression). The engine resolves the address just before sending, so the member's address never enters the Script variable space
ccThe array of CC addresses (a value expression)
bccThe array of BCC addresses (a value expression)
subjectThe subject (a value expression, required)
bodyThe body (a value expression, required). Always sent as text/html, so write markup rather than plain text (line breaks become whitespace, and < is interpreted as a tag). Interpolated value-expression results are HTML-escaped
replyTo(Optional) The Reply-To header (a value expression). It may differ from the sender (for example, send as no-reply but route replies to a support address)
timeoutMs(Optional, 1 or more) The timeout for this send (ms). Without it, the platform default; a value above the ceiling is rejected on save
  • The recipient total is at most 50. It counts to (1), cc, and bcc all together (the SMTP envelope has no cc/bcc distinction and all go out as recipients, so they are counted together). Exceeding it is rejected on save and at execution. To send to many people, use ResourceForEach + EmailSend to send one message per item.
  • It binds no result. Success only means "the provider accepted the mail," so there is no value to return and it does not take a name. It also does not retry (email is non-idempotent, so retrying after an ambiguous failure would produce a duplicate send; that is why it does not follow Http's retry). A failure is thrown and handled by a Try's catch.
  • It is an external call. It counts toward the per-plan external-call limit, and it enters the time budget as timeoutMs (10 seconds when absent) counted once (it does not retry, so there is no multiplying by a count as with Http). It can be used inside a ResourceForEach's onEach (the standard form for multi-recipient sends).
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
  "to": "{ /order/fields/email/en-US }",
  "subject": "Order received (order no. { /order/sys/id })",
  "body": "<p>Your order has been received. We will let you know again once shipping begins.</p>",
  "replyTo": "support@my-shop.example" }

Variables

SetVar

Declares or updates a script-scoped mutable variable. Reference it as { /vars/<var> } (JsonLogic has no variable declaration, so this is provided as a statement).

FieldDescription
varThe variable name. Referenced as { /vars/<var> }
valueA value expression. It can reference itself to accumulate
{ "type": "SetVar", "var": "total", "value": 0 }
{ "type": "SetVar", "var": "total", "value": { "$+": [ "{ /vars/total }", "{ /row/qty }" ] } }   // accumulate
{ "type": "SetVar", "var": "ids",   "value": { "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] } }  // collect into array

Caching

Cache

Reads from and writes to a short-lived cache private to that Script. It is the place to hold a value that is too costly to fetch again every time, such as the result of an external call, for a few seconds and reuse it on the next call. It is not an external call, so it does not count toward the number of external calls per definition, and it declares no time in the time budget.

FieldDescription
actionOne of "Set" (write), "Get" (read), "Delete" (remove) (required)
keyThe cache key (required). It is a literal, not a value expression (see below). At most 128 characters, and a longer one is rejected on save
valueThe value to store (Set only)
ttlHow long the cache stays alive (Set only, in seconds). Between 1 and 30; when omitted, 5
defaultValueThe value Get binds when there is no cached data (Get only). When omitted, null
nameThe name that holds the result. Required for Get (there is no reason to read a value that has nowhere to go). Set binds the value it stored and Delete binds whether a removal took place; both are optional
  • Write only the fields that belong to the action. A ttl on a Get, or a defaultValue on a Set, is rejected on save.
  • Absent and expired are not distinguished. Both bind defaultValue. The same goes for a value stored as null.
  • The store is scoped to that one Script. Another Script in the same Space cannot see its data even under the same cache key. Updating or deleting that Script removes all of that Script's data.
  • key is a literal. Letting a cache key that came from the request pick the data would let the caller decide what is read, and a Script that holds one value per member would hand one member's value to another. That is why a { /pointer } inside key is neither turned into a value nor taken literally: the save itself is rejected.
  • It cannot go inside an iteration. A Cache inside a Loop or ResourceForEach block is rejected on save. That is because the count limit below cannot limit anything inside an iteration. One piece of data is written per lap, so the number of statements in the definition and the number of cache keys actually used come apart.
  • Up to 5 per definition (nested included, summed regardless of action). Beyond that, the save is rejected.
  • The value you store is at most 10,240 bytes (10KiB). Beyond that, the statement fails (status 422). It behaves like any other runtime failure, so Try/catch can handle it locally.
// Reuse the exchange rates for 30 seconds.
{ "type": "Cache", "action": "Get", "name": "cached", "key": "rates" }
 
// If a value is being held, return it as-is with no external call
{ "type": "If", "condition": { "!!": [ "{ /cached }" ] },
  "then": [ { "type": "Return", "value": "{ /cached }" } ] }
 
{ "type": "Http", "name": "fetched", "method": "GET", "url": "https://api.example.com/rates" }
{ "type": "Cache", "action": "Set", "key": "rates", "value": "{ /fetched/body }", "ttl": 30 }
{ "type": "Return", "value": "{ /fetched/body }" }
 
// Discard the held value before it expires
{ "type": "Cache", "action": "Delete", "key": "rates" }

Value parsing

ParseJson

Parses JSON text into the value it denotes and binds it to a name. Use it for a body received from Http with responseType: "Text", a JSON string that arrived in the payload, or JSON kept as a string in a field. It is not an external call, so it does not count toward the external-call limit, and it declares no time in the time budget.

FieldDescription
nameThe name that holds the parsed value (required). It is optional on other statements but required here. The statement does nothing beyond binding its result, so one without a name has no effect at all
valueThe JSON text to parse (value expression, required). Point at a value from an earlier step, as in { /resp/body }, or write the JSON text out literally (a { inside the literal is not read as a { pointer } template)
  • Result: the parsed value itself. An object stays an object, an array stays an array, and a single value such as 42 or "a" is parsed too. Address the inside with { /<name>/... } afterwards.
  • A value that is already parsed is bound unchanged. When value resolves to something that is not a string, there is no text to parse, so that value is bound as it is.
  • A { /pointer } inside the parsed text is not resolved again. Even when a string received from outside holds an expression such as { /payload/... }, it is not substituted and stays text.
  • null covers two different cases. When the text to parse is the single word null, that is normal and the result is null. But when the place value points at is empty, so there is no value at all, there is nothing to parse and the statement fails.
  • Failure: when value resolves to no value or to whitespace only, or when the text is not JSON. Handle it with Try/catch like any other runtime failure; the error message carries the text it tried to parse.
  • It counts as one statement against the per-definition statement count, but has nothing to do with the external-call limit or the SetVar cap.
// 1) An API that does not return JSON: receive as Text, then parse
{ "type": "Http", "method": "GET", "url": "https://api.partner.example/v1/quote",
  "responseType": "Text", "name": "resp" },
{ "type": "ParseJson", "name": "quote", "value": "{ /resp/body }" },
 
// 2) Parse a JSON string that arrived in the payload
{ "type": "ParseJson", "name": "spec", "value": "{ /payload/fields/specJson }" }

Signature verification and text processing

These statements verify the signature a payment provider sent on a webhook, and take apart the string that signature arrived packed inside. All three are computation rather than an external call, so they do not count toward the external-call limit and declare no time in the time budget; and because they declare no data slot, the $ prefix rules do not apply to them. A complete example combining all three is in Verifying a webhook signature in the cookbook.

All three cap the length of the value they handle. The cap is on what the expression resolves to, not on the length of the expression (the sixteen characters of { /rawPayload } point at tens of kilobytes), and exceeding it fails execution, which Try can handle. The numbers are collected in Value length limits.

Signature

Checks whether the signature code received is the one built with secret, and binds the answer as a Boolean value. Signatures that payment providers (PG, MoR) send on webhooks are verified with this statement.

FieldDescription
nameThe name to bind the verification result to (required). { /<name> } is true or false. A verification whose result is never used is the same as not verifying, so it cannot be omitted
algorithmThe hash the code is built on (required). SHA1, SHA256, SHA384, SHA512
secretThe secret key shared with the other side (value expression, required)
secretEncodingHow secret is written. Utf8 (default, a text key), Hex, Base64. A key issued as hex or base64 but left as text is a different key: it produces a plausible-looking code that never matches
valueThe message the code is computed over (value expression, required). It must be character-for-character what the other side signed, so it is usually { /rawPayload }, or that prefixed with a timestamp the provider packed into its header
expectedThe code the caller sent (value expression, required). For example { /headers/x-signature }
  • Result: a Boolean. Use { /<name> } directly as an If condition afterwards.
  • Take value from /rawPayload, not from the parsed /payload. Turning a parsed payload back into a string normalizes whitespace, number notation, and escaping, so it does not return to the bytes the other side signed (see Context roots).
  • There is no field for the output notation. algorithm fixes the byte length of the code, and for a given length the hex and base64 renderings have different string lengths, so the engine recovers the bytes without being told which one the other side sent. Hex letter case, and base64 versus base64url (padding included), are non-distinctions for the same reason.
  • What fails and what returns false is decided by who supplies the value.
    • If expected is absent or the code does not match, the result is false, not a failure. Reporting a missing header separately from a wrong code would tell the sender which of the two was wrong.
    • If value is empty, it is computed as the empty message. An empty body is signed too.
    • If secret is absent or is not the notation secretEncoding declares, it fails. Of the three, this is the only input that is the author's own. Neither secret nor value appears in the failure message.
  • The cap on value is 65,536 characters (on the resolved value). It is sized for the webhook body sizes real providers send.
  • The comparison decides equality in constant time. How many leading bytes matched does not leak through the response time.
  • secret is not stored encrypted. Unlike secret: true on an Http header (stored encrypted, decrypted immediately before sending), it stays in the definition exactly as written, so the value is visible to any role that can read that Script. Members (ServiceUser) cannot read a Script definition (reading and authoring are CMA-only).
// A provider that signs the whole body
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "whsec_9f2c1b7ae4", "value": "{ /rawPayload }",
  "expected": "{ /headers/x-webhook-signature }" }
 
// A provider that issues its key as base64
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "aGVsbG8td2VlZ2xvbw==", "secretEncoding": "Base64",
  "value": "{ /rawPayload }", "expected": "{ /headers/webhook-signature }" }

Hash

Digests value and binds it as a string in the notation encoding specifies. Use it to reproduce a signature scheme that concatenates a few fields with a secret key and computes SHA256, rather than using HMAC.

FieldDescription
nameThe name to bind the digest to (required)
algorithmMD5, SHA1, SHA256, SHA384, SHA512 (required). MD5 is for reproducing an older scheme that asks for it, not a value to choose for a new signature
valueThe message to digest (value expression, required)
encodingThe result notation. Hex (default), HexUpper, Base64, Base64Url
  • There is no secret field. Schemes put the key in front, behind, or in the middle, so writing it directly inside value expresses every position.
  • Result: a string. To compare it with the code the other side sent, write { "==": [ "{ /<name> }", "{ /headers/... }" ] }. Unlike Signature's constant-time comparison, this is an ordinary equality comparison.
  • If value resolves to nothing or to whitespace only, it fails (it is the author's own expression).
  • The cap on value is 128 characters. It is the place for a few concatenated fields, so it is far narrower than Signature's. If you must compute over an entire webhook body, use Signature.
// SHA256(order number + amount + merchantKey) as uppercase hex
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
  "value": "{ /payload/orderId }{ /payload/amount }9f2c1b7ae4" }

Regex

Applies pattern to value and binds what mode asks for. Value expressions have no way to cut a string up (there is only cat for joining and in for containment), so use this statement when several values arrive packed into one header, as in t=…,v1=….

FieldDescription
nameThe name to bind the result to (required). With Capture, an element is addressed as { /<name>/1 }
mode"Match" binds whether it matched as a Boolean; "Capture" binds the first match as an array (required)
patternThe regular expression (required). It is a literal, not a value expression (see below). Flags go inline in the pattern, as in (?i). At most 128 characters, and a longer one is rejected on save
valueThe text to apply the pattern to (value expression, required). If the resolved value exceeds 10,240 characters (10KiB), execution fails
  • Result: Match is a Boolean; Capture is an array or null. In the array, index 0 is the whole match and 1 onward are the capture groups, and a group that did not participate is null (not an empty string, which would be something that did match). If the pattern does not occur, Capture is null rather than an empty array.
  • Both modes ask whether the pattern occurs anywhere. If the whole text must equal the pattern, anchor it with ^…$. The question is kept the same in both modes so that a Match check and the Capture that follows it cannot disagree.
  • pattern is one of the two fields in this engine that are not value expressions (the other is the key of Cache). Running a pattern that came from the request would let the caller choose the expression being run, and regular-expression backtracking turns that into a denial of service. That is also why a { /pointer } inside a pattern is not substituted; it becomes part of the pattern literally.
  • A pattern is compiled once per run, across the whole definition. It is not recompiled per iteration even inside a Loop or ResourceForEach, and an unusable pattern fails before the first statement has done anything (Try can handle it).
// Take apart "t=1492774577,v1=<64 hex chars>" into { /sig/1 } = timestamp, { /sig/2 } = code
{ "type": "Regex", "name": "sig", "mode": "Capture",
  "pattern": "^t=(\\d+),v1=([0-9a-f]{64})$", "value": "{ /headers/x-provider-signature }" }
 
// Check the format only
{ "type": "Regex", "name": "isOrderId", "mode": "Match",
  "pattern": "^ORD-\\d{8}-\\d{4}$", "value": "{ /payload/orderId }" }

Control flow

If

A conditional branch. condition is JsonLogic, and truthy/falsy follows the Truthiness rules.

FieldDescription
conditionJsonLogic (evaluated as a boolean)
thenThe Statement array to run when true
else(Optional) The Statement array to run when false
{ "type": "If",
  "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" } } ],
  "else": [ /* ... */ ] }

Loop

Iteration. Pick one mode: over (foreach), while (condition), or for (counted). In every mode the engine enforces an iteration ceiling (to prevent infinite loops). You declare the ceiling with maxIterations, and when you do not state it, the platform ceiling applies. External calls (Http, EmailSend) and Media file ingestion can also go inside body, and an external-call statement actually runs on each iteration at execution time. The maximum number of external calls per definition still applies.

It enters the time budget as a multiplication. The time this statement declares is the time body declares multiplied by maxIterations (10,000 when absent) (Time budget). When body makes no external call, its declared time is 0, so the 30-second base budget is the effective limit.

FieldDescription
overforeach: a value expression that resolves to an array
whilecondition: JsonLogic (repeats while true)
forcounted: { "from", "to", "step"? }. From from to to inclusive; step defaults to 1
maxIterationsThe maximum iteration count (optional). When you do not state it, the platform ceiling of 10,000 applies, and a larger value is rejected on save
name(Optional) The name to bind the current item (foreach) or the index (while/for) to ({ /<name> })
bodyThe Statement array for the loop body
// foreach
{ "type": "Loop", "over": "{ /payload/fields/items }", "name": "item", "maxIterations": 100,
  "body": [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_item" } },
             "fields": { "name": { "en-US": "{ /item/name }" } } } ] }
 
// while
{ "type": "Loop", "while": "{ /vars/hasMore }", "maxIterations": 1000, "body": [ /* ... */ ] }
 
// counted (1..10 step 2)
{ "type": "Loop", "for": { "from": 1, "to": 10, "step": 2 }, "name": "i", "maxIterations": 100, "body": [ /* ... */ ] }

Parallel

Runs branches concurrently and continues after they join. References between branches are not allowed (if there is a dependency, place them sequentially).

FieldDescription
branchesStatement[][]. Each element is one branch (an array of 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" } ]
] }

Return

This is the return from ordinary programming. It returns the Script's result to the caller and terminates normally at that point.

FieldDescription
value(Optional) The value expression to return
isErrorDefault false. When true, value comes back as the response's error (otherwise as return)
statusCodeThe response status code. Default 200
  • If Return is never reached, there is no return value. To return a result, specify value explicitly.
  • Because it is a normal termination, not an exception or throw, it is not a catch target (even inside Try it terminates the entire Script, but finally still runs).
  • A guard is also expressed with this statement. Putting a Return in an If's then returns a value when the condition is violated and does not run the statements that follow. This is one of several uses of Return.
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 }
{ "type": "Return", "value": { "reason": "payment failed" }, "isError": true, "statusCode": 402 }

Try

Exception handling.

FieldDescription
bodyThe Statement array to attempt
catch(Optional) Runs when body fails. Exposes { message } at /error (which statement failed is not carried)
finally(Optional) Always runs regardless of success or failure
  • If catch handles it, the Script is not aborted. Only a failure with no catch aborts the Script (including a compensation attempt).
  • What counts as a "failure", and the limits of compensation, are covered in Execution semantics, constraints, and security.
{ "type": "Try",
  "body":    [ { "type": "Http", "method": "POST", "url": "https://primary.api/gen", "name": "resp" },
               { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
                 "fields": { "text": { "en-US": "{ /resp/body/text }" } } } ],
  "catch":   [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
                 "fields": { "text": { "en-US": "Generation failed" }, "error": { "en-US": "{ /error/message }" } } } ],
  "finally": [ /* always runs */ ] }