# 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](/api/reference/script/value-expressions.md) (reference, literal, JsonLogic, locale map), with two exceptions: the `pattern` of `Regex` and the `key` of `Cache` (see [Regex](#regex) and [Cache](#cache)).

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

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

## Common fields {#common-fields}

```jsonc
{ "type": "<StatementType>", "name": "<optional, unique within script>", /* ...type-specific fields... */ }
```

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

### Entity reference shape {#entity-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-selector}

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

*Content Type* is accepted by **[`ResourceCount`](#resource-count) only**. Writing it in any other statement makes the save fail. Creating or changing a *Content Type* itself is CMA's job, not a *Script*'s.

A *ServiceUser* (a member who signed up for the product) is **read-only**. Only the three read statements (`ResourceRead`, `ResourceFind`, `ResourceForEach`) accept this value; writing it in a write statement makes the save fail (see [Errors](/api/reference/script/execution-and-limits.md#errors)). The rules are covered in [Reading the member directory](#service-user-reads).

## Resource writes {#resource-writes}

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

### ResourceCreate {#resource-create}

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` |

- **`Media` `file`**: The `fields.file.{locale}` value is an **ingest instruction** `{ "source": <value expression>, "encoding": "url"|"base64" }` (both required). On a write that includes a file, the engine performs the ingest (downloading it for `url`, or decoding it for `base64`, then uploading and processing it). **This ingest declares no time, so it comes out of the 30-second base budget** ([Time budget](/api/reference/script/execution-and-limits.md#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`](#common-fields) binding): The created resource. `{ /<name>/sys/id }`, `{ /<name>/fields/<field>/<locale> }`.

```jsonc
// Content
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "fields": { "title": { "en-US": "{ /payload/fields/title }" } }, "publish": true, "name": "post" }

// Media. file is an ingest instruction
{ "type": "ResourceCreate", "resource": "Media",
  "fields": {
    "title": { "en-US": "{ /payload/fields/prompt }" },
    "file":  { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } }
  }, "name": "img" }
```

### ResourceUpdate {#resource-update}

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

| Field | Description |
|---|---|
| `resource` | `"Content"` or `"Media"` |
| `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) |
| `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`.

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

### ResourcePatch {#resource-patch}

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

| Field | Description |
|---|---|
| `resource` | `"Content"` or `"Media"` |
| `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) |
| `publish` | Republish 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.

```jsonc
// +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 {#resource-delete}

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) |

```jsonc
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
```

### ResourcePublish, ResourceUnpublish, ResourceArchive, ResourceUnarchive {#resource-status-ops}

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` |

```jsonc
{ "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 {#resource-reads}

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

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

In addition, `ResourceFind`, `ResourceForEach`, and `ResourceCount` turn [Advanced Search](/api/reference/common/query-parameters.md#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](/api/reference/script/endpoints.md#anonymous-call)).

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](/api/reference/script/value-expressions.md#where-order-locale).

### Reading the member directory (ServiceUser) {#service-user-reads}

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

- **Reads only.** `ResourceCreate`, `ResourceUpdate`, `ResourcePatch`, `ResourceDelete`, and the publish and archive statements do not accept `"ServiceUser"`, and such a definition is **rejected at save time**. This is not something you can open up by adding permissions. There is no path at all for a *Script* to change a member, which is why it is rejected as a mis-written statement rather than as a permission error.
- **The author needs member-directory permission for the save to pass.** It is not checked through a permission map the way *Content* and *Media* are. Instead it looks for **`SETTING_SERVICE_LOGIN`** (or `SETTING_ALL`) in the author's *SpaceRole* `settings`, because the member directory is a resource governed by *Space* settings on every other path as well. Without it, the save is rejected (see [Security model](/api/reference/script/execution-and-limits.md#execution-identity-and-authorization)).
- **`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](/api/reference/cma/service-user.md). To email a member you found, do not extract the address; pass their `sys.id` to `toServiceUser` on [`EmailSend`](#email-send) (the engine resolves the address immediately before sending, so the member's address never enters the *Script* variable space).

```jsonc
// 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 {#resource-read}

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`](#common-fields), 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.

```jsonc
{ "type": "ResourceRead", "resource": "Content",
  "target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" }
```

### ResourceFind {#resource-find}

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](/api/reference/common/query-parameters.md#operators) (`regex`/`near`/`within` require `advanced`). `createdBy: ":self"` supported. For *ServiceUser*, `sys.email` accepts only `eq`, `ne`, `in`, and `nin` (see [Reading the member directory](#service-user-reads)) |
| `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](#resource-reads) note above |

- **Result**: Binds the first matching resource to the statement's [`name`](#common-fields). 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).

```jsonc
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
  "where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" }
```

### ResourceForEach {#resource-for-each}

**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](/api/reference/common/query-parameters.md#operators) (`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](#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`, 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`](#resource-count).
- **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](/api/reference/script/execution-and-limits.md#time-budget)). Because it is a composite statement that owns children, it itself does not count toward the [external-call leaf budget](/api/reference/script/execution-and-limits.md#static-constraints); 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.

```jsonc
// 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 {#resource-count}

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](#resource-reads) note above |
| `name` | (Optional) The name to bind the count to |

- **Result**: Binds the matched count to the statement's [`name`](#common-fields). 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](/api/reference/script/execution-and-limits.md#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.

```jsonc
// 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 {#external}

### Http {#http}

Calls an external HTTP endpoint. As an external call it counts toward the per-plan [external-call limit](/api/reference/script/execution-and-limits.md#static-constraints), and it enters the [time budget](/api/reference/script/execution-and-limits.md#time-budget) as `timeoutMs` (30 seconds when absent) × (1 + `retry`).

| Field | Description |
|---|---|
| `method` | `"GET"`, `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"` |
| `url` | The target URL (a value expression; `{ /ptr }` can be interpolated) |
| `headers` | `[{ "key", "value", "secret"? }]`. `value` is a value expression. **A `secret:true` header is treated as CMA (administrator) only: it is not exposed to end users and is decrypted only immediately before the request is sent**. Putting `Content-Type` here makes `body` serialize into that format ([below](#http-body-serialization)) |
| `body` | The request body (a value expression or JSON). The `Content-Type` header decides which format it goes out in |
| `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 |

- **Result**: `{ status, body }`, reachable through the statement's [`name`](#common-fields) 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`](#parse-json) 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`).

```jsonc
{ "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
  "headers": [ { "key": "Authorization", "value": "Bearer sk-...", "secret": true } ],
  "body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "retry": 1,
  "responseType": "Json", "name": "resp" }
```

#### What format the body goes out in {#http-body-serialization}

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.

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

### EmailSend {#email-send}

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, required). **Always sent as `text/html`**, so write markup rather than plain text (line breaks become whitespace, and `<` is interpreted as a tag). Interpolated value-expression results are **HTML-escaped** |
| `replyTo` | (Optional) The Reply-To header (a value expression). It may differ from the sender (for example, send as no-reply but route replies to a support address) |
| `timeoutMs` | (Optional, 1 or more) The timeout for this send (ms). Without it, the platform default; a value above the ceiling is rejected on save |

- **The recipient total is at most 50.** It counts `to` (1), `cc`, and `bcc` all together (the SMTP envelope has no cc/bcc distinction and all go out as recipients, so they are counted together). Exceeding it is rejected on save and at execution. To send to many people, use [`ResourceForEach`](#resource-for-each) + `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](/api/reference/script/execution-and-limits.md#static-constraints), and it enters the [time budget](/api/reference/script/execution-and-limits.md#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).

```jsonc
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
  "to": "{ /order/fields/email/en-US }",
  "subject": "Order received (order no. { /order/sys/id })",
  "body": "<p>Your order has been received. We will let you know again once shipping begins.</p>",
  "replyTo": "support@my-shop.example" }
```

## Variables {#variables}

### SetVar {#set-var}

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** |

```jsonc
{ "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 {#caching}

### Cache {#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 `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.

```jsonc
// 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 {#value-parsing}

### ParseJson {#parse-json}

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 `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.

```jsonc
// 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 {#signing-and-text}

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](/api/reference/script/value-expressions.md#dollar-prefix) do not apply to them. A complete example combining all three is in [Verifying a webhook signature in the cookbook](/api/reference/script/cookbook.md#verify-webhook-signature).

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

### Signature {#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) |
| `secretEncoding` | How `secret` is written. `Utf8` (default, a text key), `Hex`, `Base64`. A key issued as hex or base64 but left as text is a **different key**: it produces a plausible-looking code that never matches |
| `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 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](/api/reference/script/value-expressions.md#context-roots)).
- **There is no field for the output notation.** `algorithm` fixes the byte length of the code, and for a given length the hex and base64 renderings have different string lengths, so the engine recovers the bytes without being told which one the other side sent. Hex letter case, and base64 versus base64url (padding included), are non-distinctions for the same reason.
- **What fails and what returns `false` is decided by who supplies the value.**
  - If `expected` is absent or the code does not match, the result is `false`, **not a failure.** Reporting a missing header separately from a wrong code would tell the sender which of the two was wrong.
  - If `value` is empty, it is **computed as the empty message.** An empty body is signed too.
  - If `secret` is absent or is not the notation `secretEncoding` declares, it **fails**. Of the three, this is the only input that is the author's own. Neither `secret` nor `value` appears in the failure message.
- **The cap on `value` is 65,536 characters** (on the resolved value). It is sized for the webhook body sizes real providers send.
- The comparison decides equality in **constant time**. How many leading bytes matched does not leak through the response time.
- **`secret` is not stored encrypted.** Unlike `secret: true` on an `Http` header (stored encrypted, decrypted immediately before sending), it stays in the definition exactly as written, so the value is visible to any role that can read that *Script*. Members (*ServiceUser*) cannot read a *Script* definition (reading and authoring are CMA-only).

```jsonc
// A provider that signs the whole body
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "whsec_9f2c1b7ae4", "value": "{ /rawPayload }",
  "expected": "{ /headers/x-webhook-signature }" }

// A provider that issues its key as base64
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "aGVsbG8td2VlZ2xvbw==", "secretEncoding": "Base64",
  "value": "{ /rawPayload }", "expected": "{ /headers/webhook-signature }" }
```

### Hash {#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 `secret` field.** Schemes put the key in front, behind, or in the middle, so writing it directly inside `value` expresses every position.
- **Result**: a string. To compare it with the code the other side sent, write `{ "==": [ "{ /<name> }", "{ /headers/... }" ] }`. Unlike `Signature`'s constant-time comparison, this is an ordinary equality comparison.
- If `value` resolves to nothing or to whitespace only, it **fails** (it is the author's own expression).
- **The cap on `value` is 128 characters.** It is the place for a few concatenated fields, so it is far narrower than `Signature`'s. If you must compute over an entire webhook body, use `Signature`.

```jsonc
// SHA256(order number + amount + merchantKey) as uppercase hex
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
  "value": "{ /payload/orderId }{ /payload/amount }9f2c1b7ae4" }
```

### Regex {#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**: `Match` is a `Boolean`; `Capture` is an array or `null`. In the array, index `0` is the whole match and `1` onward are the capture groups, and a group that did not participate is `null` (not an empty string, which would be something that did match). If the pattern does not occur, `Capture` is `null` rather than an empty array.
- **Both modes ask whether the pattern occurs anywhere.** If the whole text must equal the pattern, anchor it with `^…$`. The question is kept the same in both modes so that a `Match` check and the `Capture` that follows it cannot disagree.
- **`pattern` is one of the two fields in this engine that are not value expressions** (the other is [the `key` of `Cache`](#cache)). Running a pattern that came from the request would let the caller choose the expression being run, and regular-expression backtracking turns that into a denial of service. That is also why a `{ /pointer }` inside a pattern is not substituted; it becomes part of the pattern literally.
- A pattern is compiled **once per run, across the whole definition**. It is not recompiled per iteration even inside a `Loop` or `ResourceForEach`, and an unusable pattern fails before the first statement has done anything (`Try` can handle it).

```jsonc
// 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 {#control-flow}

### If {#if}

A conditional branch. `condition` is JsonLogic, and truthy/falsy follows the [Truthiness](/api/reference/script/value-expressions.md#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 |

```jsonc
{ "type": "If",
  "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" } } ],
  "else": [ /* ... */ ] }
```

### Loop {#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](/api/reference/script/execution-and-limits.md#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 |

```jsonc
// 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 {#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) |

```jsonc
{ "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 {#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 `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`.

```jsonc
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 }
{ "type": "Return", "value": { "reason": "payment failed" }, "isError": true, "statusCode": 402 }
```

### Try {#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 `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](/api/reference/script/execution-and-limits.md).

```jsonc
{ "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 {#related-documents}

- [Value Expressions](/api/reference/script/value-expressions.md): The value rules that all of the fields above follow.
- [Execution semantics, constraints, and security](/api/reference/script/execution-and-limits.md): Execution order, errors, static constraints, and security.
- [Cookbook](/api/reference/script/cookbook.md): Complete examples that combine these statements.
- [Script overview](/api/reference/script.md): The top-level structure and how much time one execution gets.
