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 three exceptions: the pattern of Regex, the key of Cache, and the color of PdfWatermark (see Regex, Cache, and File transformations).

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 }). Can send a file as the 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 Statement that works on Content without naming 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, secrets), 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.

The Statements that accept a ServiceUser (a member who signed up for the product) are the three read Statements (ResourceRead, ResourceFind, ResourceForEach) plus ResourceUpdate and ResourcePatch. The only fields those two can write are roleOverride and enableLogin. Writing it in any other write Statement makes the save fail (see Errors). The rules are covered in Working with the member directory.

Resource writes

Every write Statement has propagateEvents (default false) (ResourceUpdate and ResourcePatch writing a ServiceUser do not accept this field). 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", "transformations": [ … ] }. source and encoding are required, and you write transformations only when you change the received file before it is stored (File transformations). 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" }

File transformations (transformations)

When you sell an e-book, you may want to hand each buyer a copy with their email stamped on every page of the PDF. Writing transformations in the ingest instruction lets you change the received file before it is stored. The only supported transformation is PdfWatermark.

Ingest instruction fieldRequiredDescription
transformationsOptionalAn array of transformation objects. When omitted or an empty array, the received file goes into the Media unchanged. If it is not an array, the Script save is rejected
  • Transformations are applied in array order. The result of one transformation becomes the input of the next, and the last result becomes the Media's file.
  • You can write it in any ingest instruction of ResourceCreate, ResourceUpdate, or ResourcePatch.
  • The original that source points to is not changed. To stamp text on a file already in the Space, read that Media with ResourceRead and put { /<name>/fields/file/<locale>/url } into source to create a new Media.
  • A transformation is part of the ingest. The time it takes comes out of the 30-second base budget (Time budget).

PdfWatermark: stamping text on every page of a PDF

FieldRequiredDescription
typeRequired"PdfWatermark". Any other value gets the Script save rejected
textRequiredThe text to stamp. A string that can mix in { /pointer } references. JsonLogic operation objects are not accepted. It must be a single line of 1 to 64 characters made of allowed characters
colorOptionalThe text color. Write it as #RRGGBB (six hexadecimal digits); when omitted, it is #808080 (gray). It accepts only a literal, not a value expression, and a value in a different format gets the Script save rejected
  • Fields not in the table are ignored. Font size, position, and opacity cannot be specified.
  • It stamps every page. A single line of text is laid along the diagonal through the center of the page (from bottom left to top right).
  • It is overlaid on top of the page content at 30% opacity. The original content beneath the text remains readable.
  • The font size is set automatically for each page. The text is sized to span 80% of the diagonal's length, but never more than 15% of the page's shorter side. Short text is stamped large and long text small, and the text never runs off the page.
  • The font is Helvetica, a PDF standard font.
  • Whether a file is a PDF is determined by its content, not by its file name or extension.
// Part of a statements array. A new Media with the buyer's email and order number stamped on a purchased e-book PDF
{ "type": "ResourceRead", "resource": "Media",
  "target": { "sys": { "id": "{ /payload/fields/ebookMediaId }" } }, "name": "ebook" },
{ "type": "ResourceCreate", "resource": "Media", "locale": "en-US", "publish": false,
  "fields": {
    "title": "{ /ebook/fields/title/en-US }",
    "file": {
      "source": "{ /ebook/fields/file/en-US/url }", "encoding": "url",
      "transformations": [
        { "type": "PdfWatermark", "text": "{ /payload/fields/email } · { /payload/fields/orderId }", "color": "#B22222" }
      ]
    }
  }, "name": "stamped" }

Because this is a copy for a single buyer, publish is set to false so that it is not delivered through CDA/ACDA. If you omit it, the default true applies and the copy is published.

Limits on PdfWatermark text

text can contain only characters that the PDF standard font can draw (the WinAnsi character set).

KindCharacters
ASCII printable charactersSpace, uppercase and lowercase English letters, digits, and basic symbols such as @ . # { }
Latin-1 charactersWestern European letters and symbols such as é ü ñ ß · © °. The no-break space (U+00A0) and the soft hyphen (U+00AD) are excluded
Additional symbols€ ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • ˜ ™ š › œ ž Ÿ, and the two dash characters (U+2013, U+2014)

No other characters can be used. This includes extended Latin characters not in the table (such as ł, ő, and ș), non-Latin characters, emoji, and line breaks and tabs.

When the Script is saved, the text is checked exactly as written. For a text that contains a { /pointer }, the resolved value is not known until execution, so the resolved value is checked again at execution. The codes for errors raised on save are in Errors.

LimitLiteral texttext containing a { /pointer }
A single line of allowed charactersScript save rejectedThe entire written string (pointer paths included) is checked on save, and the Script save is rejected. The resolved value is checked again at execution and fails with status 400
1 to 64 charactersScript save rejectedNot checked on save. The length of the resolved value is checked at execution and fails with status 400
  • Work out the length of the fixed text when you write it. In "Purchased by { /payload/fields/email } · Order { /payload/fields/orderId }", the fixed text outside the templates is 22 characters, which leaves 42 characters for the email and order number combined. If longer real values come in, the Script is saved, but that execution fails.
  • Stamp identifiers rather than names. A buyer name that contains characters not in the table fails at execution. An email or order number made of ASCII can be stamped.

When a file transformation fails

In the cases below, that Statement fails. As with other runtime failures, you can handle it with Try/catch. The transformation happens before the Media is written, so on failure no new Media is created, and the target of a ResourceUpdate or ResourcePatch is not changed either.

SituationResult
The file at source is not a PDFFails with status 422
The PDF cannot be read, or it is encrypted (including a PDF that only restricts permissions without asking for a password to open it)Fails with status 422
The resolved text is empty or whitespace onlyFails with status 400
The resolved text is outside 1 to 64 characters or contains characters that cannot be usedFails with status 400

ResourceUpdate

Fully replaces the fields of the target Content, Media, or ServiceUser (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", "Media", or "ServiceUser". For the fields you can write on a ServiceUser and the constraints, see Working with the member directory
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). A ServiceUser does not accept it
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, Media, or ServiceUser (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", "Media", or "ServiceUser". For the fields you can write on a ServiceUser and the constraints, see Working with the member directory
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). A ServiceUser does not accept it
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 Working with 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.

Working with 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 five Statements that work with the member directory.

  • Only two fields can be changed. ResourceUpdate and ResourcePatch accept "ServiceUser" and change roleOverride and enableLogin. Passing null for roleOverride on ResourcePatch clears the role that was assigned. ResourceCreate, ResourceDelete, and the publish and archive Statements do not accept "ServiceUser", and such a definition is rejected at save time. It is not something you can open up by adding permissions; a Script cannot create or delete a member, so it is rejected as a mis-written Statement rather than as a permission error.
  • A member has no version and no propagateEvents. Writing either one makes execution fail. ResourceUpdate replaces every writable field, so enableLogin is required, and leaving out roleOverride clears the role that was assigned. To change only one of them, use ResourcePatch, where at least one of the two fields must be present.
  • 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 Working with 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. timeoutMs is added to the time budget, and when files are sent, each file's download time is added along with it. When retry is set, that sum is multiplied by the number of attempts (1 + retry).

FieldDescription
method"GET", "POST", "PUT", "PATCH", "DELETE"
urlThe target URL (a value expression; { /ptr } can be interpolated)
headers[{ "key", "value" }]. value is a value expression. Do not write a secret value such as an API key literally; declare it in secrets and read it as { /secrets/<key> } (for example, "Bearer { /secrets/LLM_API_KEY }"). 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
urlBodyThe URL of the file to send as the request body (a value expression). The platform fetches the file from this URL and sends its bytes as they are (Sending a file as the body)
multipartBodyThe array of parts to send as multipart/form-data (at least one). Text parts and file parts can be mixed (Sending a file as the body)
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
  • Use only one of body, urlBody, and multipartBody. Writing two or more is rejected on save. When none of the three is present, the request is sent without a body.
  • 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 { /secrets/LLM_API_KEY }" } ],
  "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" }

Sending a file as the body

Sometimes you need to pass a file uploaded to Media on to an external API. A file's bytes cannot be held in a value expression. Instead, when you specify the URL where the file is, the platform fetches the file from that URL and sends it as the request body. To send one file as the entire body, use urlBody; to send files together with text fields as a form, use multipartBody.

urlBody: sending one file as the entire body

Sends the file fetched from the specified URL as the request body as it is, without converting it. It suits APIs that take a file uploaded to an upload URL with PUT. Below is an example that reads a contract PDF (Media) and sends it to an e-signature service's upload URL.

// part of the statements array
{ "type": "ResourceRead", "resource": "Media",
  "target": { "sys": { "id": "{ /payload/fields/contractMediaId }" } }, "name": "contract" },
{ "type": "Http", "method": "PUT",
  "url": "https://api.esign.example/v1/envelopes/{ /payload/fields/envelopeId }/document",
  "headers": [ { "key": "Authorization", "value": "Bearer { /secrets/ESIGN_API_KEY }" },
               { "key": "Content-Type", "value": "application/pdf" } ],
  "urlBody": "{ /contract/fields/file/en-US/url }", "name": "upload" }

The request's Content-Type is decided in the following order.

  1. If you specified Content-Type in headers, that value is used as it is. The correction described in body serialization does not apply.
  2. If you did not specify it, or its value is empty, the Content-Type the file URL responded with is used.
  3. If that is absent too, application/octet-stream is used.

multipartBody: sending files and text as a form

Sends the request as multipart/form-data. Each element of the array becomes one part, and the parts are sent in array order. The array must hold at least one element, and an empty array is rejected on save. The Content-Type is always multipart/form-data (the boundary string is generated automatically), and a Content-Type written in headers is ignored.

Part fieldDescription
nameThe form field name (required). It is a literal, not a value expression. When the same name is used more than once, each goes out as a separate part
valueThe content of a text part (a value expression). If the result is empty, it is sent as an empty value
urlThe URL to fetch the file part's bytes from (a value expression). For example, the url of a Media file
fileNameThe file name carried in the part's Content-Disposition (a value expression). It can be used only on a url part
contentTypeThe part's Content-Type (a value expression)
  • Write exactly one of value and url. Writing both, or neither, is rejected on save. Writing fileName on a value part is also rejected on save.
  • When fileName is omitted, the last path segment of url is decoded and used (the query string is excluded). If that segment is empty or contains control characters, the part is sent as file.
  • When contentType is omitted, a value part goes out without a Content-Type, and a url part uses the Content-Type the file URL responded with (application/octet-stream when absent). If you specify a value that is not in media-type format, that Http fails with status 400 at execution.
  • If the value-expression result of fileName or contentType contains a line break character, that Http fails with status 400 before the request is sent.

Below is an example that sends a product photo (Media) together with analysis options to an image-analysis service. The image part does not set fileName, so the last segment of the Media file URL (e.g. tumbler.png) goes out as the file name.

// part of the statements array
{ "type": "ResourceRead", "resource": "Media",
  "target": { "sys": { "id": "{ /payload/fields/photoMediaId }" } }, "name": "photo" },
{ "type": "Http", "method": "POST", "url": "https://api.vision.example/v2/analyze",
  "headers": [ { "key": "x-api-key", "value": "{ /secrets/VISION_API_KEY }" } ],
  "multipartBody": [
    { "name": "image", "url": "{ /photo/fields/file/en-US/url }" },
    { "name": "language", "value": "ko" },
    { "name": "features", "value": "labels" },
    { "name": "features", "value": "text" }
  ], "timeoutMs": 20000, "name": "analysis" }

Failures in an Http that sends files also fall into two kinds. A timeout is handled as a status 504 response, so retry and ignoreStatusCode apply to it. Neither applies to the file-related failures in the table below. The classification of all Http failures is laid out in Two ways an Http fails.

With ignoreStatusCode: true, when the time runs out, { "status": 504, "body": "timed out after …ms" } is bound as the result.

A single request must finish within timeoutMs, and the time spent waiting for the file URL's response is included in it. When a file is sent, the time limit is extended by the time it takes to send that file at 10,000,000 bytes per second. The extension does not exceed the plan's download time multiplied by the number of files. However, if the file URL does not report the file size, the limit is extended only by the amount actually sent, so if the transfer is slow or stalls partway, the time can run out sooner.

In the cases below, that Http fails immediately. It is not retried by retry, and it is treated as a failure even when ignoreStatusCode: true is specified. Like any other runtime failure, it can be handled with Try/catch.

SituationResult
The file to fetch is larger than the plan's download limitFails with status 413. If the file URL reports the size in advance, it fails before fetching; if not, it fails the moment the limit is exceeded during the fetch
The file URL responds with a non-2xx status (redirects included), or the file URL cannot be reachedFails with status 502
A part's contentType is not in media-type formatFails with status 400
A part's fileName or contentType value contains a line break characterFails with status 400. It fails before the request is sent
  • The download limit applies per file URL. When multipartBody has several file parts, the limit applies to each file separately.
  • The bytes sent, files included, are added to "External calls" under data transfer in the pricing plans.

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). 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 unless autoEscape is false. Use exactly one of body and templateBody
templateBodyBuilds the body from a template. It has the shape { source, data }, and you use exactly one of it and body. See Building the body from a template below
autoEscape(Optional, true by default) Whether to HTML-escape the values interpolated into the body. It applies to the results of body's { /pointer } and to templateBody.data values. Leave it false and markup inside a value renders as markup. Markup the author wrote directly and the templateBody.source template are not subject to it in the first place, and header fields are unaffected
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
  • Write the body with either body or templateBody. Writing both, or leaving both empty, is rejected on save.
  • A value sent by the caller renders as text by default. With autoEscape on, markup such as an <a> inside a value appears as text rather than as a tag. It is the mechanism that keeps a value someone else sent from planting a link or a tag in another person's mail. Leave it false only when the markup has to survive, such as the body of a newsletter an administrator wrote directly.
  • A header field cannot take a value containing a line break. This covers subject, replyTo, and every address. A line break makes execution fail. In the body a line break is ordinary content, so it is not checked.
  • 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" }

Building the body from a template (templateBody)

Writing the mail body straight into the Script definition means editing the Script every time the wording changes. templateBody keeps the body template in a Content and only fills its blanks, so the wording is edited in the content studio and the Script stays as it is.

KeyDescription
sourceA single pointer to where the template lives (required). It must point at a Content that this Script's ResourceRead or ResourceFind read, and no other characters may sit before or after that pointer. The value has to be a string, so a multi-locale Field needs the locale as well (/tpl/fields/html/en-US)
dataThe values that fill the template's blanks (optional). A key name is the blank's name. It is a data slot, so an operator needs a $ prefix ($cat), and a blank whose name starts with $ is written $$ (see Data slots and expression slots)

A blank inside the template is written { /<key> }. These blanks read data and nothing else; a template cannot read /payload or /secrets directly. A blank pointing at a key that is not in data renders as empty text. The same goes for a key whose value is null. That is the opposite of a pointer in body that points at null and prints the text null, so take care when moving between the two forms.

Where the template has a { followed by a / (including with whitespace in between, for example CSS's a{/*comment*/}), it is read as a blank, so write it \{.

There are three places where the save passes but execution stops. It fails when source resolves to an object (when the locale was left off a multi-locale Field). It fails when source is empty (either ResourceFind found nothing, or the Statement that reads the template sits after EmailSend). It also fails when filling every blank leaves nothing but whitespace.

The template that source points at is not escaped itself. What gets escaped is the data values. So to carry HTML sent by the caller into the mail, put that value in data and leave autoEscape as false.

{ "type": "ResourceRead", "resource": "Content", "name": "tpl",
  "target": { "sys": { "id": "cnt_mail_shipping" } } },
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
  "toServiceUser": { "sys": { "id": "{ /order/fields/buyer/en-US/sys/id }" } },
  "subject": "Your order has shipped",
  "templateBody": {
    "source": "{ /tpl/fields/html/en-US }",
    "data": { "name": "{ /order/fields/buyerName/en-US }",
              "tracking": "{ /order/fields/trackingNo/en-US }" } } }

The template (the html Field of cnt_mail_shipping) looks like this.

<p>{ /name }, the item you ordered has shipped.</p>
<p>Tracking number: { /tracking }</p>

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 Statements 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). Declare the key in secrets and write it as { /secrets/<key> }
secretEncodingThe notation of the key string that secret points to. 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.
  • Do not write the signing key literally in secret. Declare it in the definition's secrets and write "{ /secrets/<key> }". WEEGLOO encrypts the values in secrets and stores them securely. secretEncoding refers to the notation of the key string stored in secrets.
// A provider that signs the whole body
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "{ /secrets/WEBHOOK_SIGNING_SECRET }", "value": "{ /rawPayload }",
  "expected": "{ /headers/x-webhook-signature }" }
 
// A provider that issues its key as base64 (the base64 string is stored in secrets as it is)
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "{ /secrets/WEBHOOK_SIGNING_KEY }", "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. Inside value, write { /secrets/<key> } in the position where that scheme puts the key. Schemes put the key in front, behind, or in the middle, and written this way it can go in any of those positions. Declare the key in secrets.
  • 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 + MERCHANT_KEY) as uppercase hex
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
  "value": "{ /payload/orderId }{ /payload/amount }{ /secrets/MERCHANT_KEY }" }

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 the two Statements, a Match check and the Capture that follows it, cannot disagree.
  • pattern is one of the three fields in this engine that are not value expressions (the others are the key of Cache and the color of PdfWatermark). 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 */ ] }