Execution semantics, constraints, and security
Last updated: July 21, 2026
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 and mode
statementsrun sequentially from top to bottom. When execution reaches aReturn, it stops at that point.- Sync runs on the request-handling path; Async runs in the background. This is only a distinction of where execution happens; either way the result is the
Returnvalue (for the shape of the call response, see Request and response in the Script overview and Execution modes). - From capability to mode: if the statement tree contains any of
ExternalIo(anHttpexternal call),MediaIngest(Media file ingestion;{ source, encoding }underfields.file; common to url and base64), orLongRunning(a large Loop, and so on), thenexecutionModeis forced toAsync. These three are distinct capabilities, and they differ in which limit they count against in Static constraints 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 (rolling back a create is easy; an update needs a before-image). - External effects (
Http) are irreversible (a call that has already gone out, and its charges, cannot be undone). - On a process crash, an uncompensated state (an orphan) may remain.
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 ResourcePageRead 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
- A real failure is a statement runtime error: an
Httpfinal status of 400 or higher (4xx·5xx; not a failure whenignoreStatusCode: true) or a timeout, a response body exceeding 10MiB, or a failed resource operation (no such target, a version conflict, an unsupported operation, and so on). For such failures the engine aborts and compensates, and you can handle them locally withTry/catch/finally. - A
Returnis not an error but a normal early exit. It is not acatchtarget (there is no user-throw concept). - Inside a
catch, you reference{ message, statement }via/error.
No server-side aggregation
There are no dedicated server operations for count, sum, or group-by. You compute by iterating with ResourcePageRead and using SetVar/JsonLogic, so you are bound by fetch size and maxIterations (unsuitable for aggregating millions of records).
No waiting or delay
A Script has no Delay statement. A Script runs once and is done, on the request path (Sync) or in the background (Async), and does not wait or poll internally for an external job to finish (the Async result is separate: the caller polls with the requestId received in the 202 to obtain the Return value).
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).
| Constraint | Default |
|---|---|
If there is external I/O, executionMode must be Async | N/A |
Inside a Loop body, Http external calls and Media file ingestion are forbidden | N/A |
Maximum Http external calls per definition | 3 (maxExternalIo) |
Maximum SetVar per definition (nested included) | 5 (maxSetVar) |
| Maximum total statements per definition (nested included) | 15 (maxStatements) |
Http.retry cap | 2 (maxHttpRetry) |
These limits can be adjusted via server settings (weegloo.core.script.*); the values above are the defaults.
Media file ingestion is the
MediaIngestcapability and, unlike anHttpexternal call (ExternalIo), does not count toward themaxExternalIo(3) limit. TheAsyncrequirement and theLoop-body ban, however, apply to it just as they do toHttp.
Time budget (runtime)
| Mode | Default budget |
|---|---|
| Sync | 10 seconds (syncTimeoutMs) |
| Async | 60 seconds (asyncTimeoutMs) |
Per-plan count limits
Script is a Billable resource, and the number per Organization is limited by plan.
| Plan | Script count |
|---|---|
| Free | 3 |
| Basic | 10 |
| Pro | 50 |
| Enterprise | Unlimited |
When the limit is reached, creating a new Script is rejected (the same path as other Billable resources).
Security model
Secret headers
An item in Http.headers with secret:true is CMA (administrator) only: it is not exposed to the end user (ServiceUser) and is decrypted only immediately before transmission. Put secrets such as an LLM API key here (even when packed into an App Bundle, the secret value is masked and never leaves the source Space).
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. - 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 (
WGL403015). In other words, a Script that contains an unauthorized operation is never saved in the first place. - 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.
- 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 (
- Ownership scope:
createdBy: ":self"in awherefilter means "only what the current caller created" (for example, reading only your own wallet). - 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 there is an external call (
Http) or Media file ingestion,executionModeis"Async". - You have not put an external call inside a
Loopbody. - External calls are 3 or fewer,
SetVar5 or fewer, and total statements 15 or fewer. - Secret values were placed only via
secret:trueinHttp.headers. - Irreversible operations (external calls) are placed as late as possible.
- If you are worried about update/patch contention, use the
versiononResourceUpdateorResourcePatch. - If you want to return a result, you specified
Return.value.
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 execution modes.
