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
statementsrun sequentially from top to bottom. When execution reaches aReturn, 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 resource operation fails (no such target, a version mismatch, an unsupported operation, and so on)
- An
Httpfails (see Two ways an Http fails) - A Media file transformation fails (see When a file transformation fails)
- A runtime limit set for each Statement is exceeded (for example, the value length limits)
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.
| Kind | Handled as a status response | Fails immediately |
|---|---|---|
| When it applies | The 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 |
retry | Retries the specified number of times | Does not retry |
ignoreStatusCode: true | Not treated as a failure; binds { status, body } as the result | Does not apply; treated as a failure |
Try/catch | Can be handled in catch | Can 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.
| Constraint | Value |
|---|---|
Maximum external calls (Http, EmailSend) per definition | Per 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 block | Save rejected |
Cache.key | Literal only, at most 128 characters. A value expression means the save is rejected |
Cache.ttl | Between 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 cap | 2 |
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.multipartBody | At least 1. If empty, the save is rejected |
value and url of a multipartBody part | Exactly one. Writing both or leaving both empty means the save is rejected |
fileName of a multipartBody part | Only on a part that has url. Writing it without url means the save is rejected |
Regex.pattern length | 128 characters |
Maximum secrets per definition | 20 |
key of secrets | 1 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 secrets | 1 to 4,096 characters. Outside that range, the save is rejected |
{ /secrets… } reference | Exactly /secrets/<declared key>. Otherwise, the save is rejected |
| A Statement that creates or deletes a ServiceUser | Save rejected. Only the three read Statements plus ResourceUpdate and ResourcePatch accept this resource |
createdBy: ":self" in a where when anonymousCallEnabled is true | Save 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 text | 1 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
HttpandEmailSend, does not count toward the per-definition external-call limit.
ResourceForEachis a composite Statement that owns children, so it itself does not count toward the number of external calls. The external-call Statements insideonEach(Http,EmailSend) are what count (statically counted as 1, but actually executed per item during iteration).onEachcan hold external calls or Media file ingestion, and the same is true of aLoopbody. 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.
- All four behave like any other runtime failure, so
Try/catchcan 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).Hashis far narrower because it is the place where a handful of concatenated fields go.- The 128 characters of
Regex.patternis 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.
| Statement | Time added |
|---|---|
Http | timeoutMs (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) |
EmailSend | timeoutMs (10 seconds if not specified). It does not retry, so it is added only once |
If | The time of the longer of then and else |
Parallel | The time of the longest of the branches |
Try | The sum of the times of body, catch, and finally |
Loop | The time of body multiplied by maxIterations (10,000 if not specified) |
ResourceForEach | The 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
| Plan | Download time |
|---|---|
| Free | About 5.2 seconds |
| Basic | About 10.5 seconds |
| Pro | About 10.5 seconds |
| Enterprise | About 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.
| Plan | Script count |
|---|---|
| Free | 10 |
| Basic | 30 |
| Pro | 100 |
| Enterprise | Unlimited |
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.
| Plan | Maximum size per file URL |
|---|---|
| Free | 50MiB |
| Basic | 100MiB |
| Pro | 100MiB |
| Enterprise | 1GiB |
- The limit applies to each URL separately. If there are several
urlparts, each file must be within this limit. - Trying to send a file that exceeds the limit makes that
Httpfail (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" }
]| Field | Required | Rule |
|---|---|---|
key | Required | 1 to 64 characters, using ASCII letters (a-z, A-Z), digits, _, and - only. It must be unique within one Script. |
value | Required | A 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. |
description | Required | A description of its purpose, 0 to 128 characters. An empty string "" is also allowed. |
- Storage: WEEGLOO encrypts
valueand stores it securely. A Statement reads the original value as{ /secrets/<key> }. - Reads: API responses that return a Script (read, create, update) return
valueas 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 sendsecretsalong 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 intoSetVar,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
valueofsecretsis masked as******, whilekeyanddescriptionare 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 outsidesecrets(the headers ofHttp, thesecretofSignature, 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. ThecreatedBy/updatedByof any resource that is created or updated is the caller, and acreatedBy: ":self"scope also resolves against the caller. Anonymous calls are the exception. An execution that arrived through/execute/anonymoushas 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.
- 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 inResourceUpdateorResourcePatch, the author's SpaceRolesettingsmust containSETTING_SERVICE_LOGIN(orSETTING_ALL); seesettingson 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 areroleOverrideandenableLoginonResourceUpdateandResourcePatch(see Working with the member directory).
- The member directory (ServiceUser) is checked on the settings axis, not through a permission map. To write
- At call time (
/execute): only the caller's Script Execute permission is checked. Without it, the result is403. 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.
- 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
- Direct-call blocking (
directCallEnabled): if a Script'sdirectCallEnabledisfalse, the/executedirect 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 a403before 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 istrue(direct calls allowed). - Anonymous calls (
anonymousCallEnabled): the default isfalse. Setting it totruemakes 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 awherefilter 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), nowherecarriescreatedBy: ":self", and a Statement that verifies what was received (such asSignature) comes first. - The number of external calls (
Http,EmailSend) and the total number of Statements are within your plan's limits,SetVaris 10 or fewer, andCacheis 5 or fewer. - If you used
Cache, you wrotekeyas a literal and did not place it inside aLoopor aResourceForEach. - If you traverse a large set with
ResourceForEach, you declared alimitor 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
secretsand read as{ /secrets/<key> }. No secret value is written literally into a Statement. - If you save with a full update (PUT), you sent
secretsalong 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_LOGINand 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
versiononResourceUpdateorResourcePatch. - 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.
| Code | Condition |
|---|---|
WGL400066 | A single definition holds more than 5 Cache Statements. |
WGL400068 | A Cache Statement was placed inside a Loop or ResourceForEach block. |
WGL400067 | The key of a Cache Statement was written as a { /pointer } reference instead of a literal. |
WGL400065 | The ttl of a Cache Statement is outside the allowed range. |
WGL400063 | A Cache Statement carries a field that does not belong to its action (ttl on Get, defaultValue on Set). |
WGL400060 | ResourceCreate, 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. |
WGL400077 | The resource of a ResourceCount names a resource that cannot be counted. Media and the member directory (ServiceUser) cannot be counted. |
WGL400037 | An EmailSend carries both to and toServiceUser, or leaves both empty. Write exactly one. |
WGL400054 | The recipient total of an EmailSend (to 1 + cc + bcc) is over 50. |
WGL400039 | The timeoutMs of an EmailSend is over the ceiling. |
WGL400101 | An EmailSend carries both body and templateBody, or leaves both empty. Write exactly one. |
WGL400102 | The 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. |
WGL400061 | A Script that allows anonymous calls (anonymousCallEnabled) has createdBy: ":self" in the where of a read Statement. |
WGL400023 | A single definition holds more than 10 SetVar Statements. |
WGL400026 | The retry on an Http Statement is over the upper bound of 2. |
WGL400087 | Two or more request body fields (body, urlBody, multipartBody) were written on an Http. |
WGL400088 | The multipartBody of an Http has no parts. |
WGL400089 | A multipartBody part has both value and url, or neither. |
WGL400090 | A multipartBody part has fileName without url. |
WGL400036 | A ResourceForEach goes over the upper bound on the number of items it can process. |
WGL400094 | Two or more entries in secrets use the same key. |
WGL400095 | A single definition holds more than 20 secrets. |
WGL400086 | A value in secrets is outside 1 to 4,096 characters. The error does not carry the value. |
WGL400086 | The text of a PdfWatermark with no { /pointer } is outside 1 to 64 characters. |
WGL400096 | The 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. |
WGL429005 | The total number of Statements a single definition holds is over the plan limit. |
WGL429006 | The number of external calls (Http, EmailSend) a single definition holds is over the plan limit. |
WGL403015 | The 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. |
Related documents
- Value expressions: value and condition rules.
- Statement Catalog: the fields and results of each Statement.
- Cookbook: a collection of complete examples.
- Script resources and endpoints: the
Scriptresource structure and HTTP endpoints such as/execute. - Script overview: the top-level structure and the time allowed for a single execution.
