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
| Category | type | One-line summary |
|---|---|---|
| Resource writes | ResourceCreate | Create Content/Media (optionally publish) |
ResourceUpdate | Full replacement of Content/Media fields (any field/locale not provided is deleted) | |
ResourcePatch | Partial merge of Content/Media fields (only the specified fields/locales; a literal null deletes) | |
ResourceDelete | Delete (Draft/Archived only; if Published, unpublish first) | |
ResourcePublish / ResourceUnpublish | Publish / unpublish | |
ResourceArchive / ResourceUnarchive | Archive / unarchive | |
| Resource reads | ResourceRead | Read a single item by id |
ResourceFind | First matching single item by filter (null if none) | |
ResourceForEach | Iterate internally over the resources matching a filter, running onEach per item | |
ResourceCount | Count only the number of items matching a filter (the items are not read) | |
| External | Http | External HTTP call ({ status, body }). Can send a file as the body |
EmailSend | Send one email through a registered EmailAccount | |
| Variables | SetVar | Declare / update a script-scoped variable |
| Caching | Cache | Read, write, or remove a short-lived cache private to that Script |
| Value parsing | ParseJson | Parse JSON text into a value (object, array, scalar) and bind it |
| Signing and text | Signature | Verify that a received signature code matches the one built with a secret key (Boolean) |
Hash | Compute an unkeyed digest (string) | |
Regex | Apply a regular expression. Whether it matched (Boolean) or the captured groups (array) | |
| Control flow | If | Conditional branch |
Loop | Iterate (foreach / while / counted) | |
Parallel | Run branches concurrently | |
Return | Return a result and terminate early | |
Try | Exception 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, andResourceCount,contentTypeis required whenresourceis"Content". There is no Content query that spans a whole Space.ResourceCreatestates 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) havetarget, 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:
nameis 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.idis usually a literal (e.g."ct_post").target.sys.idis 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.
| Field | Applies to | Description |
|---|---|---|
resource | Common | "Content" or "Media" (required) |
contentType | Content | The Content Type to create ({ sys: { id } }). Required for Content |
fields | Common | Field 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) |
locale | Common | (Convenience) When provided, each value in fields is auto-wrapped as { <locale>: value } |
publish | Common | Publish after the write (exposed on CDA/ACDA). Default true |
Mediafile: Thefields.file.{locale}value is an ingest instruction{ "source": <value expression>, "encoding": "url"|"base64", "transformations": [ … ] }.sourceandencodingare required, and you writetransformationsonly 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 forurl, or decoding it forbase64, 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. Ifpublish:truebut there is no file or processing is incomplete, the publish step errors; ifpublish:false, it staysDraft.- Result (
namebinding): 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 field | Required | Description |
|---|---|---|
transformations | Optional | An 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, orResourcePatch. - The original that
sourcepoints to is not changed. To stamp text on a file already in the Space, read that Media withResourceReadand put{ /<name>/fields/file/<locale>/url }intosourceto 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
| Field | Required | Description |
|---|---|---|
type | Required | "PdfWatermark". Any other value gets the Script save rejected |
text | Required | The 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 |
color | Optional | The 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).
| Kind | Characters |
|---|---|
| ASCII printable characters | Space, uppercase and lowercase English letters, digits, and basic symbols such as @ . # { } |
| Latin-1 characters | Western 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.
| Limit | Literal text | text containing a { /pointer } |
|---|---|---|
| A single line of allowed characters | Script save rejected | The 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 characters | Script save rejected | Not 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.
| Situation | Result |
|---|---|
The file at source is not a PDF | Fails 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 only | Fails with status 400 |
The resolved text is outside 1 to 64 characters or contains characters that cannot be used | Fails 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.
| Field | Description |
|---|---|
resource | "Content", "Media", or "ServiceUser". For the fields you can write on a ServiceUser and the constraints, see Working with the member directory |
target | The target ({ sys: { id } }, required). The id is usually { /ptr } |
fields | The 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 |
publish | Republish 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.
| Field | Description |
|---|---|
resource | "Content", "Media", or "ServiceUser". For the fields you can write on a ServiceUser and the constraints, see Working with the member directory |
target | The target ({ sys: { id } }, required). The id is usually { /ptr } |
fields | The 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 |
publish | Republish after the update. Default true |
- Delete a specific locale or file: Give a literal
nullas 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
filereplaces 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).
| Field | Description |
|---|---|
resource | "Content" or "Media" |
target | The 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.
| Field | Description |
|---|---|
resource | "Content" or "Media" |
target | The 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.
ResourceUpdateandResourcePatchaccept"ServiceUser"and changeroleOverrideandenableLogin. PassingnullforroleOverrideonResourcePatchclears 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
versionand nopropagateEvents. Writing either one makes execution fail.ResourceUpdatereplaces every writable field, soenableLoginis required, and leaving outroleOverrideclears the role that was assigned. To change only one of them, useResourcePatch, 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(orSETTING_ALL) in the author's SpaceRolesettings, 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). fromaccepts onlyCurrent. A member is not a published resource, so passingPublishedmakes execution fail.contentTypeandadvancedare 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.emailaccepts only the exact-match operators (eq,ne,in,nin). A member's address is stored encrypted, so ordering comparisons andprefixare 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 theirsys.idtotoServiceUseronEmailSend(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.
| Field | Description |
|---|---|
resource | "Content", "Media", or "ServiceUser" |
target | The 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
Tryto 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).
| Field | Description |
|---|---|
resource | "Content", "Media", or "ServiceUser" |
contentType | The Content Type to search within ({ sys: { id } }). Required for Content. Ignored for Media and ServiceUser |
where | The 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) |
order | The 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 isnullwhen 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).
| Field | Description |
|---|---|
resource | "Content", "Media", or "ServiceUser" (required) |
contentType | The Content Type to iterate within ({ sys: { id } }). Required for Content. Ignored for Media and ServiceUser |
where | The 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 |
order | The sort (e.g. "sys.createdAt,sys.id"). Without it, the platform default order |
from | Current (default, the latest draft) or Published (the published snapshot). ServiceUser accepts Current only |
advanced | Iterate 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 |
onEach | The array of child Statements to run for each item (required) |
- It does not bind a collection (it is
foreach, notmap). There is no{ items, next }and no cursor. Rather than returning the iteration result as a value, it runsonEachper item. If you need a list, gather it yourself withSetVar. If all you need is a count, useResourceCount. - 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 declaredlimitis an intended stop and terminates normally. Alimitabove 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 (leavewhereas "unprocessed" and mark completion at the end ofonEach, so a re-run continues from what remains). - It enters the time budget as a multiplication. The time this Statement declares is the time
onEachdeclares 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 insideonEachare what count against the budget. onEachcan hold external calls (Http,EmailSend) or Media file ingestion like any other Statement (the same asLoop'sbody). 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.
| Field | Description |
|---|---|
resource | "Content" or "ContentType" (required). Media and ServiceUser cannot be counted, and writing them makes the save fail |
contentType | The Content Type to count within ({ sys: { id } }). Required for Content. Ignored when counting Content Type (there is one set per Space) |
where | The 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) orResourceForEach(run per item). - Do not iterate with
ResourceForEachjust 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
orderand nolimit. 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).
| Field | Description |
|---|---|
method | "GET", "POST", "PUT", "PATCH", "DELETE" |
url | The 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) |
body | The request body (a value expression or JSON). The Content-Type header decides which format it goes out in |
urlBody | The 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) |
multipartBody | The 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) |
timeoutMs | The timeout for this call (ms) |
retry | The number of retries when the response status is 400 or higher. Default 0; the ceiling is 2 |
ignoreStatusCode | Whether 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) |
responseType | What 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, andmultipartBody. 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'snameas{ /<name>/status }and{ /<name>/body/... }. The shape ofbodyis set byresponseType. responseTypeapplies 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 (aTry/catchtarget). For an API that does not return JSON, receive the response as"Text", then parse it withParseJsonwhen you need it as a value. "Text"is decoded with the charset of the responseContent-Type, and treated as UTF-8 when there is no charset. When the body is empty,bodyisnullin 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 byignoreStatusCode).
{ "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-Type | body shape | Body that goes out |
|---|---|---|
application/json | Anything | JSON |
application/x-www-form-urlencoded | An object or array | order[id]=A-2481&order[amount]=34000 |
text/plain | A scalar | The value as it is |
Anything else (text/xml and such) | Anything | JSON |
These are the combinations that get corrected because they cannot be carried in the declared format.
Declared Content-Type | body shape | Content-Type that actually goes out | Body that goes out |
|---|---|---|---|
application/x-www-form-urlencoded | A scalar | text/plain;charset=UTF-8 | The value as it is |
text/plain | An object or array | application/json | JSON |
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.
body | Expanded 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.
- If you specified
Content-Typeinheaders, that value is used as it is. The correction described in body serialization does not apply. - If you did not specify it, or its value is empty, the
Content-Typethe file URL responded with is used. - If that is absent too,
application/octet-streamis 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 field | Description |
|---|---|
name | The 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 |
value | The content of a text part (a value expression). If the result is empty, it is sent as an empty value |
url | The URL to fetch the file part's bytes from (a value expression). For example, the url of a Media file |
fileName | The file name carried in the part's Content-Disposition (a value expression). It can be used only on a url part |
contentType | The part's Content-Type (a value expression) |
- Write exactly one of
valueandurl. Writing both, or neither, is rejected on save. WritingfileNameon avaluepart is also rejected on save. - When
fileNameis omitted, the last path segment ofurlis decoded and used (the query string is excluded). If that segment is empty or contains control characters, the part is sent asfile. - When
contentTypeis omitted, avaluepart goes out without aContent-Type, and aurlpart uses theContent-Typethe file URL responded with (application/octet-streamwhen absent). If you specify a value that is not in media-type format, thatHttpfails with status 400 at execution. - If the value-expression result of
fileNameorcontentTypecontains a line break character, thatHttpfails 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.
| Situation | Result |
|---|---|
| The file to fetch is larger than the plan's download limit | Fails 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 reached | Fails with status 502 |
A part's contentType is not in media-type format | Fails with status 400 |
A part's fileName or contentType value contains a line break character | Fails with status 400. It fails before the request is sent |
- The download limit applies per file URL. When
multipartBodyhas 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.
| Field | Description |
|---|---|
account | The 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 |
to | The recipient address (a value expression). Use exactly one of to and toServiceUser |
toServiceUser | Specifies 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 |
cc | The array of CC addresses (a value expression) |
bcc | The array of BCC addresses (a value expression) |
subject | The subject (a value expression, required) |
body | The 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 |
templateBody | Builds 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
bodyortemplateBody. Writing both, or leaving both empty, is rejected on save. - A value sent by the caller renders as text by default. With
autoEscapeon, 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 itfalseonly 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, andbccall 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, useResourceForEach+EmailSendto 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 followHttp'sretry). A failure is thrown and handled by aTry'scatch. - 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 withHttp). It can be used inside aResourceForEach'sonEach(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.
| Key | Description |
|---|---|
source | A 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) |
data | The 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).
| Field | Description |
|---|---|
var | The variable name. Referenced as { /vars/<var> } |
value | A 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 arrayCaching
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.
| Field | Description |
|---|---|
action | One of "Set" (write), "Get" (read), "Delete" (remove) (required) |
key | The 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 |
value | The value to store (Set only) |
ttl | How long the cache stays alive (Set only, in seconds). Between 1 and 30; when omitted, 5 |
defaultValue | The value Get binds when there is no cached data (Get only). When omitted, null |
name | The 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
ttlon aGet, or adefaultValueon aSet, is rejected on save. - Absent and expired are not distinguished. Both bind
defaultValue. The same goes for a value stored asnull. - 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.
keyis 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 }insidekeyis neither turned into a value nor taken literally: the save itself is rejected.- It cannot go inside an iteration. A
Cacheinside aLooporResourceForEachblock 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/catchcan 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.
| Field | Description |
|---|---|
name | The 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 |
value | The 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
42or"a"is parsed too. Address the inside with{ /<name>/... }afterwards. - A value that is already parsed is bound unchanged. When
valueresolves 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. nullcovers two different cases. When the text to parse is the single wordnull, that is normal and the result isnull. But when the placevaluepoints at is empty, so there is no value at all, there is nothing to parse and the Statement fails.- Failure: when
valueresolves to no value or to whitespace only, or when the text is not JSON. Handle it withTry/catchlike 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
SetVarcap.
// 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.
| Field | Description |
|---|---|
name | The 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 |
algorithm | The hash the code is built on (required). SHA1, SHA256, SHA384, SHA512 |
secret | The secret key shared with the other side (value expression, required). Declare the key in secrets and write it as { /secrets/<key> } |
secretEncoding | The 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 |
value | The 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 |
expected | The code the caller sent (value expression, required). For example { /headers/x-signature } |
- Result: a
Boolean. Use{ /<name> }directly as anIfcondition afterwards. - Take
valuefrom/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.
algorithmfixes 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
falseis decided by who supplies the value.- If
expectedis absent or the code does not match, the result isfalse, not a failure. Reporting a missing header separately from a wrong code would tell the sender which of the two was wrong. - If
valueis empty, it is computed as the empty message. An empty body is signed too. - If
secretis absent or is not the notationsecretEncodingdeclares, it fails. Of the three, this is the only input that is the author's own. Neithersecretnorvalueappears in the failure message.
- If
- The cap on
valueis 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'ssecretsand write"{ /secrets/<key> }". WEEGLOO encrypts the values insecretsand stores them securely.secretEncodingrefers to the notation of the key string stored insecrets.
// 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.
| Field | Description |
|---|---|
name | The name to bind the digest to (required) |
algorithm | MD5, 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 |
value | The message to digest (value expression, required) |
encoding | The result notation. Hex (default), HexUpper, Base64, Base64Url |
- There is no
secretfield. Insidevalue, 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 insecrets. - Result: a string. To compare it with the code the other side sent, write
{ "==": [ "{ /<name> }", "{ /headers/... }" ] }. UnlikeSignature's constant-time comparison, this is an ordinary equality comparison. - If
valueresolves to nothing or to whitespace only, it fails (it is the author's own expression). - The cap on
valueis 128 characters. It is the place for a few concatenated fields, so it is far narrower thanSignature's. If you must compute over an entire webhook body, useSignature.
// 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=….
| Field | Description |
|---|---|
name | The 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) |
pattern | The 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 |
value | The text to apply the pattern to (value expression, required). If the resolved value exceeds 10,240 characters (10KiB), execution fails |
- Result:
Matchis aBoolean;Captureis an array ornull. In the array, index0is the whole match and1onward are the capture groups, and a group that did not participate isnull(not an empty string, which would be something that did match). If the pattern does not occur,Captureisnullrather 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, aMatchcheck and theCapturethat follows it, cannot disagree. patternis one of the three fields in this engine that are not value expressions (the others are thekeyofCacheand thecolorofPdfWatermark). 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
LooporResourceForEach, and an unusable pattern fails before the first Statement has done anything (Trycan 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.
| Field | Description |
|---|---|
condition | JsonLogic (evaluated as a boolean) |
then | The 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.
| Field | Description |
|---|---|
over | foreach: a value expression that resolves to an array |
while | condition: JsonLogic (repeats while true) |
for | counted: { "from", "to", "step"? }. From from to to inclusive; step defaults to 1 |
maxIterations | The 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> }) |
body | The 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).
| Field | Description |
|---|---|
branches | Statement[][]. 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.
| Field | Description |
|---|---|
value | (Optional) The value expression to return |
isError | Default false. When true, value comes back as the response's error (otherwise as return) |
statusCode | The response status code. Default 200 |
- If
Returnis never reached, there is no return value. To return a result, specifyvalueexplicitly. - Because it is a normal termination, not an exception or throw, it is not a
catchtarget (even insideTryit terminates the entire Script, butfinallystill runs). - A guard is also expressed with this Statement. Putting a
Returnin anIf'sthenreturns a value when the condition is violated and does not run the Statements that follow. This is one of several uses ofReturn.
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 }
{ "type": "Return", "value": { "reason": "payment failed" }, "isError": true, "statusCode": 402 }Try
Exception handling.
| Field | Description |
|---|---|
body | The 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
catchhandles it, the Script is not aborted. Only a failure with nocatchaborts 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 */ ] }Related documents
- Value Expressions: The value rules that all of the fields above follow.
- Execution semantics, constraints, and security: Execution order, errors, static constraints, and security.
- Cookbook: Complete examples that combine these Statements.
- Script overview: The top-level structure and how much time one execution gets.
