Value expressions
Everywhere a value is needed in a Script (a URL, a request body, a field value, a condition, a filter value, a target id, and so on) takes one of the three forms below. There are exactly two exceptions: the pattern of Regex and the key of Cache are written as literals only, and a { /pointer } inside them is not turned into a value. This document explains those three forms, where values come from (the context roots), and the locale-map rules specific to WEEGLOO data. Every field in the Statement catalog follows these rules.
The three forms
| Form | Rule | Example |
|---|---|---|
| Reference | Resolves a { /json-pointer } inside a string against the context. | "{ /payload/fields/title }" |
| Literal | A value with no { /ptr } (a string, number, boolean, object, or array). Used as-is. | "draft", 42, true, { "a": 1 } |
| Operations and conditions (JsonLogic) | An object whose single key is an operator. Its operands are themselves value expressions (references, literals, or nested operations). Depending on the slot, the operator needs a $. | { "$+": [ "{ /vars/n }", 1 ] } |
The three forms nest: you put a reference in a JsonLogic operand, then feed that reference's result back into another operation.
Data slots and expression slots: when to prefix with $
The same JSON is read differently depending on the slot. What decides it is who owns the keys in that slot. The keys of fields are a Content Type's field ids, and the keys of Http.body are the schema of the API you are calling, so in slots like those, cat or in has to be a field name, not an operator.
| Slot | Applicable fields | How it is read |
|---|---|---|
| Data slot | fields (ResourceCreate, ResourceUpdate, ResourcePatch), Http.body, Return.value, SetVar.value, Cache.value, Cache.defaultValue | A key without $ is always a field name. To use an operation, prefix it with $. |
| Expression slot | If.condition, Loop.while, version | The whole value is an expression. An operator can be written as either cat or $cat. |
| Template slot | Everything else (url, method, headers[].value, locale, order, over, target.sys.id, the fields of EmailSend, and the value fields of Signature, Hash, and Regex) | These are strings, so only { /pointer } goes in. |
| Literal only | Regex.pattern, Cache.key | Not a value expression. A { /pointer } written in Regex.pattern is not substituted; it becomes part of the pattern. |
The rules fit in two lines.
- In a data slot, a key without
$is always a field name. To use an operation, prefix the operator with$. - Once
$takes you into an expression, everything inside it is an expression. Nested operators do not need$(you may still add it).
If in doubt, prefix every operator with
$. That is correct in every slot.
// data slot: cat is a Content Type field name (not the concatenation operation)
"fields": { "cat": { "en-US": "hello" } }
// computing in a data slot: $ only at the boundary, everything inside stays as-is
"fields": { "tier": { "en-US": { "$if": [ { ">=": [ "{ /p/score }", 700 ] }, "gold", "silver" ] } } }
// expression slot: write it as-is
"condition": { "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }When you need a field name that starts with $: $$
When the key really does have to start with $, as with JSON Schema's $ref and $schema, write $ twice. "$$ref" means the data key $ref. Only one leading $ is stripped ($$$ref gives $$ref), and this applies to keys only (a $ inside a value is left alone).
"body": { "$$ref": "#/components/schemas/Item", "topK": { "$min": [ "{ /payload/fields/k }", 50 ] } }The two things that are rejected
The two cases below are not quietly interpreted as something else; they are rejected as errors.
- A
$key alongside another key in the same object is an error. An operation must be that object's only key, and you can move the sibling data one level out. - An unknown
$key is an error.$cattis not a field named$catt. The$namespace is reserved for operators.
In an expression slot, an operator name sitting alongside a sibling key is an error too ({ "and": […], "or": […] }). That slot has no reading in which the object is data, and every object is judged true, so left alone the condition would quietly always be true.
Reference: { /json-pointer }
Put an RFC 6901 JSON Pointer (which must start with /) inside the braces. Whitespace around the braces is allowed ({ /a/b } is the same as {/a/b}).
Single pointer vs. mixed template: type rules
- When the entire string is a single pointer, the value keeps its original type (a number stays a number, an object stays an object, an array stays an array).
- When it is mixed with literal text, the result is string concatenation.
"{ /payload/fields/count }" // a number value stays a number (e.g. 42)
"{ /payload/fields/tags }" // an array stays an array
"page-{ /payload/fields/n }-of-10" // string concatenation → "page-42-of-10"
"Bearer { /payload/fields/token }" // string concatenation → "Bearer abc123"Missing values
- When the path is absent or the value is empty, a single pointer becomes
nulland a mixed template becomes an empty string.
Context roots: where values come from
The top-level segment of a { /pointer } is one of the seven below.
| Root | Contents |
|---|---|
/payload | The JSON payload (the input) passed on the call. Example: { /payload/fields/email } |
/rawPayload | The same input, held exactly as the body string the caller sent (before parsing). Example: { /rawPayload } |
/headers | The request HTTP headers passed on the call. Keys are lowercase, with a single value per name. Example: { /headers/authorization } |
/now | The time at which execution started. { /now/seconds }, { /now/millis }, { /now/iso } |
/<name> | The result of an earlier statement that carries that name. Example: { /order/sys/id } |
/vars/<name> | A script-scoped mutable variable declared with SetVar. Example: { /vars/total } |
/error | Used only inside a Try block's catch. The caught error, { message }. Example: { /error/message } |
The six names other than /<name> (payload, rawPayload, headers, now, vars, error) are reserved and cannot be used as a statement's name. Using the same name would overwrite that root, so it is rejected at save time (see the binding-name rules under Common fields).
/rawPayload: the body exactly as it was sent
/payload is the parsed value; /rawPayload is the raw string of that same body. The two point at the same thing without being the same thing. Turning a parsed value back into a string normalizes whitespace, number notation, escapes, and duplicate keys, so you do not get back the bytes that were sent.
That is why a value computed over the bytes as sent can be handled only through /rawPayload. The representative case is verifying the signature on a payment provider's webhook (see Signature). For the everyday references where you pull a value out and use it, use /payload.
The call body accepts a JSON object only. An empty body is treated as no body, and a body that is not a JSON object (broken JSON, an array, a scalar, a literal null) is rejected without being executed (see Errors).
/now: the time execution started
/now holds the time this execution started, in three forms.
| Pointer | Value |
|---|---|
{ /now/seconds } | Epoch seconds (integer) |
{ /now/millis } | Epoch milliseconds (integer) |
{ /now/iso } | A time string in the platform's notation, the same as sys.createdAt (UTC) |
- One execution has one time only. It is not a statement that reads a clock but a value planted when execution starts, so two statements never see different values. Each branch of a
Parallelinherits the same time. Because it is not a statement, it does not count toward the statement count either. - There is no field for choosing a time zone. An epoch value is the same number everywhere, and
isois UTC notation. - You use it to verify a webhook's replay window (how far from now the timestamp carried in the signature is, in seconds). The timestamp usually arrives as a string, but the arithmetic operation converts it to a number, so you compare it as-is.
// is the timestamp carried in the signature within 5 minutes (300 seconds)?
{ "<": [ { "-": [ "{ /now/seconds }", "{ /sig/1 }" ] }, 300 ] }The shape of a statement's result
The result shape of a statement that carries a name differs by type.
| Statement | Result shape | Reference example |
|---|---|---|
Http | { status, body } | { /resp/status }, { /resp/body/choices/0/message/content } |
ResourceCreate, ResourceRead (single), ResourceFind (single) | The resource itself | { /post/sys/id }, { /post/fields/title/en-US } |
ResourceForEach | (During iteration) name is the current item = the resource itself. Referenced only inside onEach | { /post/sys/id }, { /post/fields/title/en-US } |
ResourceCount | The matched count (an integer) | { /commentCount } |
ParseJson | The parsed value itself (object, array, scalar) | { /quote/items/0/price } |
Signature | Boolean (whether verification passed) | { /verified } |
Hash | A string (the digest in the notation you declared) | { /expectedSign } |
Regex | Match is a Boolean. Capture is an array (0 = the whole match, the capture groups from 1) or null when there is no match | { /isOrderId }, { /sig/1 } |
ResourceFindbindsnullwhen there is no match. Branch on existence with{ "==": [ "{ /found }", null ] }.ResourceRead(single) is an error when the target does not exist (you can handle it withTry). See Reading resources in the Statement catalog for details.- Reading a ServiceUser gives the member resource itself as the result (
{ /member/sys/id }). Unlike Content and Media, its fields are not locale maps but the values themselves. The rules are covered in Reading the member directory.
Operations and conditions: JsonLogic
When you need a calculation or a condition, use an operator object from the jsonlogic.com specification.
- Data access is standardized on
{ /ptr }references, not the vanillavar(dot-path). The engine resolves the operands' pointers first, then applies the operator. - An operator must be that object's only key. In a data slot only a key prefixed with
$is an operation; in an expression slot it is an operation whether or not it has$(see Data slots and expression slots).
Operator table
The names in the table are operator tokens. When you use one in a data slot, prefix it with
$(catbecomes$cat). In an expression slot, either form works.
| Category | Operator | Meaning and example |
|---|---|---|
| Condition | if (alias ?:) | { "if": [cond, then, cond2, then2, …, default] }. The value of the first true condition, or the last default if none. |
| Logic | and, or | Short-circuit evaluation. and returns the first falsy operand (or the last), or the first truthy operand (or the last), as a value. |
| Logic | ! (not), !! (to-bool) | { "!": x } negates truthiness; { "!!": x } gives the truthiness. !! is often used for existence checks. |
| Equality | ==, != | Loose comparison (compares after numeric coercion; "1"==1 is true). |
| Equality | ===, !== | Strict comparison (type included). |
| Comparison | <, <=, >, >= | Chainable: { "<": [1,2,3] } means 1<2 AND 2<3. If a value cannot be made numeric (NaN), it is false. |
| Arithmetic | + | The sum of all operands. |
| Arithmetic | - | With one operand, negation; with two, subtraction. |
| Arithmetic | *, /, % | Multiplication, division, remainder. |
| Aggregation | min, max | The minimum and maximum of the operands. |
| String | cat | Concatenates all operands as strings. |
| Membership | in | { "in": [needle, haystack] }. If the haystack is a string, substring; if a collection, element membership. |
| Array | merge | Flattens multiple arrays or values into a single array (used for accumulation). |
| Date | date | { "date": [value, output unit] }. Normalizes the value into a comparable instant. The output unit is millis (the default), seconds, iso, or day. See Date normalization. |
The array iteration operators (map, filter, reduce, all, some, none) are not supported. Script iterates over an array with Loop (see Loop in the Statement catalog). Picking out only the items in a list that meet a date condition is also the job of the read statements, not of iteration. Give the condition to the where of ResourceFind or ResourceForEach, and the server filters them and returns them (the operators you can use are in the operator list).
Numeric coercion and examples
The numeric coercion rules are as follows. A number is left as-is, true becomes 1, false becomes 0, a string is parsed (if it cannot be parsed, the calculation fails), and null becomes 0.
A date string is not a number. "2026-10-03" does not parse as a number, so a comparison operator returns false every time, without raising an error. To compare dates, normalize them first with date.
The snippets below assume an expression slot. When you put one in a data slot (fields, Http.body, Return.value, SetVar.value), prefix the top-level operator with $ and leave the inner operands as they are.
{ "-": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] } // balance - cost
{ "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] } // balance < cost → boolean
{ "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }
{ "cat": [ "id-", "{ /payload/sys/id }" ] } // "id-<uuid>"
{ "!!": "{ /found/sys/id }" } // true if it exists
{ "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] } // array accumulation: SetVar.value is a data slot, so $
{ "if": [ "{ /payload/fields/next }", "{ /payload/fields/next }", "END" ] } // next if present, otherwise "END"Date normalization (date)
A comparison operator turns its operands into numbers and then compares them. A date string is not a number, so that comparison is always false, without raising an error. Switching to == does not solve it either. When neither side is a number the text itself is compared, so "2026-10-03" and "2026-10-03T00:00:00.000Z", two spellings of the same instant, come out as different values. Normalize a date with date before comparing it.
{ "date": [ value, output unit ] } // the output unit can be omitted
{ "date": "2026-10-03" } // when you pass a single value, you may drop the arrayThere are no dedicated before, after, or equal operators. A normalized value is a number, so you use the comparison, arithmetic, and aggregation operators that already exist.
| What you want to decide | The expression to use |
|---|---|
| a is before b | { "<": [ { "date": a }, { "date": b } ] } |
| a is after b | { ">": [ { "date": a }, { "date": b } ] } |
| The same instant | { "==": [ { "date": a }, { "date": b } ] } |
| The same day (the time ignored) | { "==": [ { "date": [a, "day"] }, { "date": [b, "day"] } ] } |
| Between from and to | { "<=": [ { "date": from }, { "date": x }, { "date": to } ] } (a chained comparison) |
| One week later | { "date": [ { "+": [ { "date": x }, 604800000 ] }, "iso" ] } |
| The number of days between two dates | { "/": [ { "-": [ { "date": a }, { "date": b } ] }, 86400000 ] } |
| The earliest of several dates | { "min": [ { "date": a }, { "date": b } ] } |
An arithmetic result is a millisecond number again, so you can put it through date once more to output it as iso or day ("One week later" in the table above).
// expression slot: is the coupon inside its valid period? the three values may be written in different notations
{ "<=": [
{ "date": "{ /coupon/fields/startsAt/en-US }" },
{ "date": "{ /now/iso }" },
{ "date": "{ /coupon/fields/endsAt/en-US }" }
] }
// expression slot: is the HTTP Date header within 5 minutes (300 seconds) of now?
{ "<": [ { "-": [ "{ /now/seconds }", { "date": [ "{ /headers/date }", "seconds" ] } ] }, 300 ] }The inputs it reads
All of the values below are read as the same instant.
| Format | Example |
|---|---|
| ISO-8601, RFC 3339 | 2026-10-03T00:00:00Z, 2026-10-03T00:00:00.000Z, 2026-10-03T09:00:00+09:00 |
| A time with the seconds or the fraction omitted | 2026-10-03T00:00 |
A time with a space where the T goes | 2026-10-03 00:00:00 |
| A date only (read as midnight UTC) | 2026-10-03 |
RFC 1123 (the notation of the HTTP Date header) | Sat, 03 Oct 2026 00:00:00 GMT |
| An epoch number or a numeric string | 1790985600, 1790985600000, "1790985600" |
- With no offset, the value is read as UTC. The offset accepts
+09:00,+0900,+09, andZ. - Parsing is strict. A date that does not actually exist (
2026-13-45) fails even when the digit count is right. - The unit of an epoch is told apart by the magnitude of its absolute value. Below 100,000,000,000 it is seconds, and at or above that it is milliseconds. That is why whichever of
{ /now/seconds }and{ /now/millis }you put in, each one is read correctly. - The range accepted as an epoch is an absolute value of 100,000,000 or more and less than 100,000,000,000,000. Because the unit has to be told apart by magnitude, the range is bounded at both ends. A number outside it fails instead of being read as the year 1970. The separator-less date
20261003, the year2026, and the0that arrives to mean "no value" fall into this.
The output unit
The second operand decides the output form. Unit names are case-insensitive.
| Value | Result | Where you use it |
|---|---|---|
Omitted, millis | Epoch milliseconds (a number) | Comparison and arithmetic |
seconds | Epoch seconds (a number). Anything below a second is dropped | External APIs that take epoch seconds |
iso | 2026-10-03T00:00:00.000Z | Writing to a Content Date field |
day | 2026-10-03 (in UTC) | Same-day comparison, display |
A name that is not in the list fails, and the error message lists the names you can use.
The iso output and writing to a Date field
A Content Date field accepts a single format on write, yyyy-MM-ddTHH:mm:ss[.fraction]Z. The T, the seconds, and the trailing Z all have to be there, the fraction is optional, and the value is read as UTC. So a 2026-10-03 or a 2026-10-03T09:00:00+09:00 that arrived in the payload is rejected as an invalid value if you put it in as-is. The iso output of date is exactly this format, so put a date you received through date once before you write it to the field.
// data slot: write the payload's "2026-10-03" to the coupon's end date
"fields": { "endsAt": { "en-US": { "$date": [ "{ /payload/fields/endsAt }", "iso" ] } } }Values it cannot read
In the three cases below, that statement fails (status 400). It is a failure that happens during execution, so you can handle it locally with the catch of Try.
- The first operand is missing, or a reference did not find a value.
- The value cannot be read as a date. An empty string, a whitespace-only string, a string that is not a date, a date that does not exist, a boolean, an object, and a number outside the accepted range fall into this.
- The output unit name is not in the list.
Not returning null when the value is missing is a deliberate contract. null becomes 0 under numeric coercion and is therefore compared against the year 1970, so a check with a missing date does not fail but comes out inverted. A coupon that is past its valid period getting through is worse than execution coming to a stop.
Truthiness
if, and, or, !, !!, along with If.condition and Loop.while, decide true and false by the following rules.
- falsy:
null,false, the number0, the empty string"", and an empty collection (an empty array). - truthy: everything else (nonzero numbers, non-empty strings and arrays, and every object).
Keys can be references too
The keys of a map such as fields also support { /ptr } references. The key is resolved at runtime.
"fields": { "{ /payload/fields/fieldName }": { "en-US": "{ /payload/fields/fieldValue }" } }If two keys resolve to the same value, they collide and it is an engine error.
Locale map: rules specific to Content and Media
In WEEGLOO, each field of a Content or Media is not a value but a per-locale map (for example, balance is { "en-US": 1, "ko-KR": 10 }). So you must handle the locale together when you read and write. For Media as well, title and description (scalars) and file (the ingest directive) are locale maps. JSON that is not a Content or Media, such as /payload or an HTTP response, is unaffected by this rule (it keeps the structure the schema defines, and a scalar stays a scalar).
Reading
- To get a scalar, specify down to the locale:
{ /<name>/fields/<field>/<locale> }(for example,{ /post/fields/title/en-US }). - Without a locale,
{ /<name>/fields/<field> }returns the whole locale-map object. - A
localized:falsefield lives only in the default-locale bucket, so read it with that default-locale code.
Writing (the fields of ResourceCreate, ResourceUpdate, and ResourcePatch)
The value is a locale map, { "<locale>": <scalar value expression> }. It is symmetric with reading.
"fields": {
"title": { "en-US": "Hello", "ko-KR": "안녕" }, // list buckets for multiple locales
"status": { "en-US": "paid" }
}ResourceCreatemust include the Space default-locale bucket in every field it populates (the default-locale rule).ResourceUpdateis a full replacement. Fields and locales absent fromfieldsare removed (including the file).ResourcePatchupdates only the specified fields and buckets (other fields and locales are kept).- Delete with a literal
null: when the value is a literalnull, that (field, locale) bucket is deleted (the standard way to clear a specific locale in a Patch).""(an empty string) is not a deletion but sets an empty value. When a value expression ({ /ptr }) evaluates to null at runtime, it is not a deletion but an error (a missing payload is not silently swallowed). Only a literalnulldeletes. Mediafile: the value is not a scalar but an ingest directive,{ "source": …, "encoding": "url"|"base64" }. What the ingest actually does is covered in ResourceCreate in the Statement catalog.- Put a
localized:falsefield only in the default-locale bucket. - A locale code (a map key) can also be a
{ /ptr }reference (see Keys can be references too above). Use this to build a dynamic locale.
The locale convenience field
When you give locale to ResourceCreate, ResourceUpdate, or ResourcePatch, the engine automatically wraps each value in fields in a { <locale>: value } bucket. In other words, you only need to give scalars.
// the two below are equivalent
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
"locale": "en-US", "fields": { "title": "Hello" } }
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
"fields": { "title": { "en-US": "Hello" } } }If you give locale while the value already nests a locale map ({ "en-US": … }), it becomes doubly nested as { <locale>: { "en-US": … } } (an authoring mistake). Standardize on one style: with locale, use scalars only; without it, use explicit locale maps only.
Locales in where and order
- In
whereandorder, forfields.Xthe engine automatically applies the Space default locale (the same as a CMA query). - To specify a particular locale, state it explicitly as
fields.X.<locale>.
"where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } } // default-locale slug
"where": { "fields.title.ko-KR": { "prefix": "안" } } // specific localeErrors
These are the codes that come back when a value expression breaks its rules. The check happens at save time, and the codes for breaking the other static constraints on a definition are in the errors of Execution semantics, constraints, and security, while the codes that come back when you call a Script are in the errors of Script resource and endpoints. For codes that are common to every resource, see common errors.
| Code | Condition |
|---|---|
WGL400056 | A $ operation key was placed in a data slot alongside another key in the same object. |
WGL400055 | A $ key that is not defined as an operator was written in a data slot. |
Related documents
- Statement catalog: The fields and results of the 25 statement types that use value expressions.
- Execution semantics, constraints, and security: Execution order, errors, optimistic locking, and static constraints.
- Cookbook: Complete examples that combine value expressions.
- Script overview: The top-level structure and the time given to a single execution.
