Execution semantics, constraints, and security

This page summarizes how a Script behaves at runtime (order, transactions, errors, locking), what static constraints it is subject to at save time, and its security model. For syntax, see the Statement Catalog and value expressions; for practical combinations, see the cookbook.

Execution order

  • statements run sequentially from top to bottom. When execution reaches a Return, it stops at that point.
  • Execution happens inline, on the path that handles the calling request. The response to the call is the execution result (for the shape of the response, see Request and response in the Script overview), and the time allowed for a single execution is covered in Time budget below.

Execution semantics

Guard (preconditions)

There is no dedicated guard Statement. You express one with If and then:[Return]. When the condition is violated, it returns a result and does not run the subsequent Statements (a Script with no guard is of course also possible).

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

No transactions and best-effort compensation

A Script is not a transaction. On failure, the engine attempts to compensate for the work done so far and returns the cause of the error, but with the following limits (accepted as a design trade-off).

  • Undoing a delete creates a new sys.id, so references that pointed to it break.
  • External effects (Http) are irreversible (a call that has already gone out, and its charges, cannot be undone).
  • The compensation may not run at all, leaving an uncompensated state.

If you need true atomicity, write the compensation into the Script yourself, or put irreversible operations (such as external calls) last. The most dangerous ordering is the kind that "chains through but cannot roll back, yet looks safe."

Optimistic locking

You narrow update/patch contention with the version on ResourceUpdate and ResourcePatch. If you supply version (a value expression, Int), the update proceeds only when it matches the target's current sys.version; a mismatch aborts with a version-conflict error (which you can handle locally with Try/catch). If you omit it, it is last-write-wins with no check. Typically you read first with ResourceRead or ResourceFind and pass that sys.version (see Optimistic-locking CAS in the cookbook).

Writes go to origin

Writes always land in origin (draft), and exposure on delivery (CDA/ACDA) is controlled by publish (the publish on ResourceCreate/ResourceUpdate/ResourcePatch, or ResourcePublish/ResourceUnpublish).

What counts as a failure

When a single Statement fails during execution, execution stops at that point unless a Try handles the failure. The engine then attempts to undo the work performed up to that point. Undoing has limits, which are covered in No transactions and best-effort compensation.

If the failed Statement is inside a Try, you can handle the failure in catch. Inside a catch, you reference the failure details ({ message }) via /error. It does not carry which Statement failed.

The following cases are treated as failures.

A Return is not a failure. It ends execution early in the normal way, so it does not pass to catch.

Two ways an Http fails

An Http failure falls into one of two kinds, depending on whether it is handled as a status response. This difference determines whether retry and ignoreStatusCode apply.

KindHandled as a status responseFails immediately
When it appliesThe target API responds with a status of 400 or higher, the request times out (status 504), the target API cannot be connected to or its response cannot be read (status 502), the response body exceeds 10MiB (status 502)responseType is "Json" but the body of a successful response is not JSON, a file to send exceeds the download limit, a file URL responds with a non-2xx status or cannot be connected to, a part value has an invalid format
retryRetries the specified number of timesDoes not retry
ignoreStatusCode: trueNot treated as a failure; binds { status, body } as the resultDoes not apply; treated as a failure
Try/catchCan be handled in catchCan be handled in catch

The conditions under which sending a file fails are covered in detail in Sending a file as the body.

Server-side aggregation: counts only

Counts are done on the server by ResourceCount. It does not read the items, so it is not subject to the processed-item ceiling.

Sum and group-by have no dedicated server operation. You have to compute such an aggregation yourself, by iterating with ResourceForEach and using SetVar and JsonLogic, so it is bound by the processed-item ceiling (unsuitable for aggregating millions of records). When all you need is a count, use ResourceCount instead of iterating.

No waiting or delay

A Script has no Delay Statement. A Script runs once and is done, and does not wait or poll internally for an external job to finish.

Static constraints (validated at save time)

The following are checked when a Script is saved (on create/update). If any is violated, the save is rejected (this fails at authoring time, not at runtime). Which violation is rejected with which code is covered in Errors.

ConstraintValue
Maximum external calls (Http, EmailSend) per definitionPer plan (see Pricing)
ResourceForEach maximum total items processed (without a declared limit, iterates up to this value; fails if it is hit with matches remaining)10,000
Maximum SetVar per definition (nested included)10
Maximum Cache per definition (nested included, summed regardless of action)5. Beyond that, the save is rejected
A Cache inside a Loop or ResourceForEach blockSave rejected
Cache.keyLiteral only, at most 128 characters. A value expression means the save is rejected
Cache.ttlBetween 1 and 30 seconds; 5 when omitted. Outside that range, the save is rejected
Maximum total Statements per definition (nested included)Per plan (see Pricing)
Http.retry cap2
Request body fields of one Http (body, urlBody, multipartBody)Only one of the three. Writing two or more means the save is rejected
Number of parts in Http.multipartBodyAt least 1. If empty, the save is rejected
value and url of a multipartBody partExactly one. Writing both or leaving both empty means the save is rejected
fileName of a multipartBody partOnly on a part that has url. Writing it without url means the save is rejected
Regex.pattern length128 characters
Maximum secrets per definition20
key of secrets1 to 64 characters, ASCII letters (a-z, A-Z), digits, _, and - only. Two entries with the same key means the save is rejected
value of secrets1 to 4,096 characters. Outside that range, the save is rejected
{ /secrets… } referenceExactly /secrets/<declared key>. Otherwise, the save is rejected
A Statement that creates or deletes a ServiceUserSave rejected. Only the three read Statements plus ResourceUpdate and ResourcePatch accept this resource
createdBy: ":self" in a where when anonymousCallEnabled is trueSave rejected
transformations of a Media ingest instruction (File transformations)Array only. An element's type is PdfWatermark, and color is #RRGGBB. For text, the entire written string (including the path in a { /pointer }) is a single line of allowed characters. Otherwise, the save is rejected
Length of a literal PdfWatermark text1 to 64 characters. Outside that range, the save is rejected. A text containing a { /pointer } is not checked on save; it is checked at execution against the resolved value

The fixed limits in the table above are values the platform sets, so they are the same regardless of plan. The maximum total Statements and maximum external calls per definition, by contrast, are per-plan limits. These two are not validation errors but plan limits, so exceeding them causes the save/update to be rejected as a plan-limit overage (the same definition is allowed on a higher plan) and is lifted by upgrading. The per-plan numbers are in Pricing.

Media file ingestion, unlike external calls such as Http and EmailSend, does not count toward the per-definition external-call limit.

ResourceForEach is a composite Statement that owns children, so it itself does not count toward the number of external calls. The external-call Statements inside onEach (Http, EmailSend) are what count (statically counted as 1, but actually executed per item during iteration). onEach can hold external calls or Media file ingestion, and the same is true of a Loop body. How many laps an iteration actually runs does not enter this count; it enters the time budget below as a multiplication instead.

Value length limits (runtime)

The signing and text-processing Statements, and Cache, cap the size of the value they handle. The cap applies to what the expression resolves to, not to the length of the expression (the sixteen characters of { /rawPayload } can point at tens of kilobytes), which is why it is checked while running rather than at save time.

TargetCapOn exceeding
value of Signature65,536 charactersThat Statement fails (status 422)
value of Hash128 charactersThat Statement fails (status 422)
value of Regex10,240 characters (10KiB)That Statement fails (status 400)
value of Cache10,240 bytes (10KiB)That Statement fails (status 422)
  • All four behave like any other runtime failure, so Try/catch can handle them locally.
  • Signature's cap is sized for the body sizes real providers send (a payment event runs to a few KB, an order webhook to tens of KB). Hash is far narrower because it is the place where a handful of concatenated fields go.
  • The 128 characters of Regex.pattern is the save-time check in Static constraints above. That length is not a safety device ((a+)+$ is dangerous in six characters). What bounds a runaway match is the literal-only rule for patterns plus the time budget below; what a length can honestly promise is a size a person can read and review.

Time budget (runtime)

The time allowed for a single execution is at most 180 seconds. The time actually allowed is determined by the Script definition (and, when files are sent, by the plan as well), and equals a base of 30 seconds plus the time that external calls can use.

This budget applies to the execution as a whole. Separately, a single Http request must finish within that request's timeoutMs.

Base of 30 seconds

All work that is not an external call, such as reading and writing resources or receiving and processing Media files, is handled within these 30 seconds. A Script with no external calls is allowed 30 seconds.

Time each Statement adds

The times of Statements placed in sequence all add up.

StatementTime added
HttptimeoutMs (30 seconds if not specified). When files are sent, the download time for each file (table below) is added, and if retry is specified, the whole is multiplied by the number of attempts (1 + retry)
EmailSendtimeoutMs (10 seconds if not specified). It does not retry, so it is added only once
IfThe time of the longer of then and else
ParallelThe time of the longest of the branches
TryThe sum of the times of body, catch, and finally
LoopThe time of body multiplied by maxIterations (10,000 if not specified)
ResourceForEachThe time of onEach multiplied by limit (10,000 if not specified)
Other Statements (those that do not contain other Statements)Not added. Handled within the base 30 seconds

If there is no external call inside an iteration, the time added is 0 even after multiplying by the number of iterations. Such an iteration must therefore finish within the base 30 seconds.

Download time per file

PlanDownload time
FreeAbout 5.2 seconds
BasicAbout 10.5 seconds
ProAbout 10.5 seconds
EnterpriseAbout 107.4 seconds

This value is the time it takes to receive the plan's download limit at 10,000,000 bytes per second.

Calculation example

A Script that calls one Http sending two photos (timeoutMs 30 seconds, retry 1) on the Pro plan is calculated as follows.

  • Http: (30 seconds + 10.5 seconds × 2) × 2 = about 102 seconds
  • Whole execution: 30 seconds + 102 seconds = about 132 seconds

132 seconds is shorter than 180 seconds, so this Script is allowed 132 seconds.

When it exceeds 180 seconds

Even when the computed value exceeds 180 seconds, the Script is saved and runs. However, once execution reaches 180 seconds, it stops at that point.

Per-plan count limits

For Script, the number per Organization is limited by plan.

PlanScript count
Free10
Basic30
Pro100
EnterpriseUnlimited

Separately from this, the number of Statements and the number of external calls (Http, EmailSend) a single Script definition can contain are also limited by plan. When you save or update a definition, exceeding that plan's limit rejects it; for the concrete numbers, see Pricing.

When the limit is reached, creating a new Script is rejected.

Http file download size

When Http fetches a file to send as the request body from a URL (urlBody, or a url part of multipartBody), the size that can be received per file URL is limited by plan. It follows the plan of the Organization to which the Space that owns the Script belongs.

PlanMaximum size per file URL
Free50MiB
Basic100MiB
Pro100MiB
Enterprise1GiB
  • The limit applies to each URL separately. If there are several url parts, each file must be within this limit.
  • Trying to send a file that exceeds the limit makes that Http fail (see Two ways an Http fails).
  • This limit is also the basis for the download time added to the time budget.

Security model

secrets

Secret values such as external API keys, a payment provider's (PG) secret key, webhook signing keys, and precomputed Basic … credentials are not written literally into a Statement. Declare them in the definition's secrets, and read them in a Statement as { /secrets/<key> }. Published test keys go in the same place too. Then, when you switch to the real key, you leave the Statements as they are and change only the value in secrets.

"secrets": [
  { "key": "LLM_API_KEY",            "value": "sk-...",    "description": "API key for LLM calls" },
  { "key": "WEBHOOK_SIGNING_SECRET", "value": "whsec_...", "description": "" }
],
"statements": [
  { "type": "Http", "method": "POST", "url": "https://api.openai.com/v1/chat/completions",
    "headers": [ { "key": "Authorization", "value": "Bearer { /secrets/LLM_API_KEY }" } ],
    "body": { /* ... */ }, "name": "llm" }
]
FieldRequiredRule
keyRequired1 to 64 characters, using ASCII letters (a-z, A-Z), digits, _, and - only. It must be unique within one Script.
valueRequiredA string of 1 to 4,096 characters. It is a static value and is not interpreted as a template, so a { /… } inside it is used literally, character for character.
descriptionRequiredA description of its purpose, 0 to 128 characters. An empty string "" is also allowed.
  • Storage: WEEGLOO encrypts value and stores it securely. A Statement reads the original value as { /secrets/<key> }.
  • Reads: API responses that return a Script (read, create, update) return value as the original value. Any role that can read that Script can see the value, so keep read permission narrow. Members (ServiceUser) cannot read a Script definition (reading and authoring are CMA-only, and ACMA has no Script API).
  • Updates: A full update (PUT) replaces the entire definition. If you send it without secrets, the secret values you declared are deleted, and if a Statement still references one of them as { /secrets/… }, the save is rejected. So send secrets along with every update. Sending back the values from the read response as they are is enough.
  • Values during execution: What happens to a value once it is read is decided by the Statements. If you return it with Return, it remains in the execution response and the ScriptLog, and if you put it into SetVar, Cache, or a Content, it remains there as the original value. Send secret values only to the external API that needs them.
  • App Bundle: When a Script is packed into an App Bundle, the value of secrets is masked as ******, while key and description are carried as they are. A Script created by installing that App Bundle has its secret values set to ******, so the Statements that read them work only after the installing side puts in the real values. Values written in places outside secrets (the headers of Http, the secret of Signature, and so on) are not masked and are carried as they are.

A single Script can hold up to 20 secrets. The pointer rules are covered in /secrets in Value expressions.

Execution identity and authorization

  • Execution identity: during execution, every resource operation is performed under the identity of the user who called /execute. The createdBy/updatedBy of any resource that is created or updated is the caller, and a createdBy: ":self" scope also resolves against the caller. Anonymous calls are the exception. An execution that arrived through /execute/anonymous has no caller, so both resolve against the author instead (Anonymous calls).
  • There are two authorization boundaries, and at runtime the engine does not re-check resource permissions per Statement.
    1. At authoring time (save): when a Script is saved, it checks whether the author actually holds the resource and action permissions that its Statements use. If even one is missing, the save is rejected. In other words, a Script that contains an unauthorized operation is never saved in the first place. Every Statement that selects a resource is subject to this check, whether it is a leaf or the block-owning ResourceForEach. A definition you already saved is checked again when you update it, so once a permission has been revoked you can no longer save an edit to that definition.
      • The member directory (ServiceUser) is checked on the settings axis, not through a permission map. To write resource: "ServiceUser" in one of the three read Statements or in ResourceUpdate or ResourcePatch, the author's SpaceRole settings must contain SETTING_SERVICE_LOGIN (or SETTING_ALL); see settings on SpaceRole. The member directory is a Space setting on every other path through the system as well.
      • A Statement that creates or deletes a member cannot be saved under any role. A Script cannot create or delete a member, so it is rejected as a mis-written Statement (400) rather than as missing permission (403). That is to say it is not a gap the author could close by adding a role. The only changes allowed on a member are roleOverride and enableLogin on ResourceUpdate and ResourcePatch (see Working with the member directory).
    2. At call time (/execute): only the caller's Script Execute permission is checked. Without it, the result is 403. Once it passes, per-Statement resource permissions are not checked again at runtime; execution proceeds. This works like function-execution permission in programming. If you have permission to run the function, the permission for each individual operation inside it is not asked again. The anonymous call path has no such check. There is no caller to check, which is why opening that path amounts to publishing one Script without authentication.
  • Direct-call blocking (directCallEnabled): if a Script's directCallEnabled is false, the /execute direct call itself is rejected. This gate is applied after the Execute permission check has passed, so the call is blocked even when the caller holds the Execute permission. A caller without that permission receives a 403 before reaching this gate. The gate exists only on that endpoint, so a Webhook's linked action (script) and a Scheduler run it as before. The default is true (direct calls allowed).
  • Anonymous calls (anonymousCallEnabled): the default is false. Setting it to true makes that one Script runnable through a dedicated unauthenticated path (/execute/anonymous) as well, and there the execution identity is the author, not the caller. Of the two boundaries above, the call-time check (Execute permission) does not exist on that path, so the real authentication is what the Script does for itself (verifying the signature on what it received). The conditions for turning it on and its save-time rules are covered in Anonymous calls.
  • Ownership scope: createdBy: ":self" in a where filter means "only what the current caller created" (for example, reading only your own wallet). This filter cannot be used in a Script that allows anonymous calls. With no caller it resolves to the author, so its original meaning as an ownership scope does not hold.
  • Delegated permissions (author beware): combining the two boundaries above, running a Script is equivalent to acting with the author's permissions delegated to it. The caller needs only Execute, and the Statements inside the Script run exactly within the scope the author was authorized for at save time. As a result, a resource operation the caller could not perform on their own can still happen through the Script. Because the permissions granted to the author are the effective reach of that Script, decide carefully what operations you put in a Script.

Summary checklist

Before saving, verify the following.

  • If you turned on anonymous calls (anonymousCallEnabled), no where carries createdBy: ":self", and a Statement that verifies what was received (such as Signature) comes first.
  • The number of external calls (Http, EmailSend) and the total number of Statements are within your plan's limits, SetVar is 10 or fewer, and Cache is 5 or fewer.
  • If you used Cache, you wrote key as a literal and did not place it inside a Loop or a ResourceForEach.
  • If you traverse a large set with ResourceForEach, you declared a limit or verified the size can complete.
  • If you included an iteration (Loop, ResourceForEach), you checked that it enters the time budget as a multiplication (with no external call, the 30-second base budget is the limit).
  • Secret values such as external API keys and signing keys are declared in secrets and read as { /secrets/<key> }. No secret value is written literally into a Statement.
  • If you save with a full update (PUT), you sent secrets along with it.
  • The message for signature verification is taken from { /rawPayload }, not from /payload.
  • If a Statement works with members (ServiceUser), the author holds SETTING_SERVICE_LOGIN and you included no Statement that creates or deletes that resource.
  • Irreversible operations (external calls) are placed as late as possible.
  • If you send files with Http, you wrote only one request body field (body, urlBody, multipartBody) and checked that each file is within the per-plan download limit.
  • If you are worried about update/patch contention, use the version on ResourceUpdate or ResourcePatch.
  • If you want to return a result, you specified Return.value.

Errors

These are the codes that come back when the shape of a definition breaks a static constraint and the save is rejected. The codes for breaking the value expression rules are in the errors of Value expressions, and the codes that come back when you call or delete a Script are in the errors of Script resource and endpoints. For codes that are common to every resource, see common errors.

CodeCondition
WGL400066A single definition holds more than 5 Cache Statements.
WGL400068A Cache Statement was placed inside a Loop or ResourceForEach block.
WGL400067The key of a Cache Statement was written as a { /pointer } reference instead of a literal.
WGL400065The ttl of a Cache Statement is outside the allowed range.
WGL400063A Cache Statement carries a field that does not belong to its action (ttl on Get, defaultValue on Set).
WGL400060ResourceCreate, ResourceDelete, or a publish or archive Statement has "ServiceUser" in its resource. The member directory is accepted only by the three read Statements and by ResourceUpdate and ResourcePatch.
WGL400077The resource of a ResourceCount names a resource that cannot be counted. Media and the member directory (ServiceUser) cannot be counted.
WGL400037An EmailSend carries both to and toServiceUser, or leaves both empty. Write exactly one.
WGL400054The recipient total of an EmailSend (to 1 + cc + bcc) is over 50.
WGL400039The timeoutMs of an EmailSend is over the ceiling.
WGL400101An EmailSend carries both body and templateBody, or leaves both empty. Write exactly one.
WGL400102The templateBody.source of an EmailSend is not a single pointer to a Content that this Script's ResourceRead or ResourceFind read. Other characters before or after the pointer are rejected too.
WGL400061A Script that allows anonymous calls (anonymousCallEnabled) has createdBy: ":self" in the where of a read Statement.
WGL400023A single definition holds more than 10 SetVar Statements.
WGL400026The retry on an Http Statement is over the upper bound of 2.
WGL400087Two or more request body fields (body, urlBody, multipartBody) were written on an Http.
WGL400088The multipartBody of an Http has no parts.
WGL400089A multipartBody part has both value and url, or neither.
WGL400090A multipartBody part has fileName without url.
WGL400036A ResourceForEach goes over the upper bound on the number of items it can process.
WGL400094Two or more entries in secrets use the same key.
WGL400095A single definition holds more than 20 secrets.
WGL400086A value in secrets is outside 1 to 4,096 characters. The error does not carry the value.
WGL400086The text of a PdfWatermark with no { /pointer } is outside 1 to 64 characters.
WGL400096The transformations of a Media ingest instruction is not an array, an element's type is not PdfWatermark, a PdfWatermark has no text or its text is an object or array, color is not in #RRGGBB format, or the text as written contains a character that cannot be used or a line break.
WGL429005The total number of Statements a single definition holds is over the plan limit.
WGL429006The number of external calls (Http, EmailSend) a single definition holds is over the plan limit.
WGL403015The author does not hold the permissions for the resources and actions that the Statements in the definition operate on. Even when the author holds the permission, a contentType, createdBy, or tag filter attached to that allowance gets the save rejected. The allowance has to be an unconditional one. The single exception is Content Create, where an allowance scoped to a contentType also counts and is checked against the contentType written in the Statement (Media Create has no such exception). A read Statement with "ServiceUser" in its resource while the author lacks SETTING_SERVICE_LOGIN also falls under this code.