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

  • statements run sequentially from top to bottom. When execution reaches a Return, 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 Return value (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 (an Http external call), MediaIngest (Media file ingestion; { source, encoding } under fields.file; common to url and base64), or LongRunning (a large Loop, and so on), then executionMode is forced to Async. 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 Http final status of 400 or higher (4xx·5xx; not a failure when ignoreStatusCode: 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 with Try/catch/finally.
  • A Return is not an error but a normal early exit. It is not a catch target (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).

ConstraintDefault
If there is external I/O, executionMode must be AsyncN/A
Inside a Loop body, Http external calls and Media file ingestion are forbiddenN/A
Maximum Http external calls per definition3 (maxExternalIo)
Maximum SetVar per definition (nested included)5 (maxSetVar)
Maximum total statements per definition (nested included)15 (maxStatements)
Http.retry cap2 (maxHttpRetry)

These limits can be adjusted via server settings (weegloo.core.script.*); the values above are the defaults.

Media file ingestion is the MediaIngest capability and, unlike an Http external call (ExternalIo), does not count toward the maxExternalIo (3) limit. The Async requirement and the Loop-body ban, however, apply to it just as they do to Http.

Time budget (runtime)

ModeDefault budget
Sync10 seconds (syncTimeoutMs)
Async60 seconds (asyncTimeoutMs)

Per-plan count limits

Script is a Billable resource, and the number per Organization is limited by plan.

PlanScript count
Free3
Basic10
Pro50
EnterpriseUnlimited

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. The createdBy/updatedBy of any resource that is created or updated is the caller, and a createdBy: ":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.
    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 (WGL403015). In other words, a Script that contains an unauthorized operation is never saved in the first place.
    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.
  • Ownership scope: createdBy: ":self" in a where filter 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, executionMode is "Async".
  • You have not put an external call inside a Loop body.
  • External calls are 3 or fewer, SetVar 5 or fewer, and total statements 15 or fewer.
  • Secret values were placed only via secret:true in Http.headers.
  • Irreversible operations (external calls) are placed as late as possible.
  • 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.