TransformScript Syntax
This page is the reference for what is and isn't valid TransformScript. It covers the building blocks you write in a script: comments, literals, operators, and the forms that combine them into expressions. Read it when you need to confirm the exact syntax for a value or operator, or when you want to know why a piece of input is rejected.
For a guided introduction to writing scripts, see Writing a TransformScript.
Comments and Whitespace
Whitespace is insignificant. A ; is skipped and acts as a harmless separator. Comments come in three forms:
// line comment
# line comment
/* block comment (no nesting) */
A block comment must be closed. An unterminated block comment is invalid.
Literals
Strings
Strings use any of three interchangeable quote characters — "…", '…', or `…` — with identical meaning. Supported escapes are \n \t \r, \uXXXX (exactly four hex digits), and \$ \" \' \ \`. Any other escaped character passes through literally.
A string can interpolate an expression with $( … ):
"Hello, $(payload.name)!"
"Total: $(qty * price)"
The interpolated expression is parsed as ordinary TransformScript. An empty $() is invalid. A $ that is not followed by ( is a literal dollar sign.
Numbers
A number is digits or digits.digits. A value with a . is a decimal; otherwise it's an integer. TransformScript does not support radix (0xFF), exponent (1e3), or digit-grouping (1_000) forms. Negative numbers are not literals: -5 is the unary minus operator applied to 5.
Booleans and Null
true, false, null.
Temporal Literals
Dates, times, and durations are written between pipe characters, | … |, and classified by shape:
| Example | Type |
|---|---|
|2024-01-02| | Date |
|13:30:00|, |13:30:00.5| | Time / LocalTime |
|2024-01-02T13:30:00| | LocalDateTime (no zone) |
|2024-01-02T13:30:00Z|, |2024-01-02T13:30:00.5+05:00| | DateTime (zoned) |
|P1Y2M3DT4H|, |PT30M|, |P1W| | Period (ISO-8601 duration) |
Text between pipes that doesn't match one of these shapes is invalid.
Because | always opens a temporal literal, there is no binary | operator in the language. To thread a value through a series of steps, use the then function — see Functions and Lambdas.
Regular Expressions
Regular expressions are written /pattern/flags, for example /ab+/i. A / is read as a regular expression only where a value is expected: at the start of an expression, after an operator, an opening bracket, a comma, matches, and similar positions. Elsewhere, / is division.
Identifiers and Placeholders
Identifiers are [A-Za-z][A-Za-z0-9_]*. The placeholder tokens $, $$, and $$$ (runs of $) are the implicit lambda parameters (see Functions and Lambdas). A single _ is a standalone placeholder. @ is its own token, used for XML attribute access and as the current-element reference inside filter selectors.
Some identifiers are reserved and cannot be used as names: async, case, default, do, enum, for, import, input, ns, output, private, throw, type, unless, yield.
Keywords
true false null and or not default if else match case do var fun is to using as. The words mod, matches, at, and update are soft keywords that are meaningful only in position.
Operators and Precedence
The table lists TransformScript operators from lowest precedence (binds loosest) to highest (binds tightest). Binary operators are left-associative unless noted.
| Precedence (low → high) | Operators / forms |
|---|---|
using(…) bindings | using(a = …) body |
| conditional | if cond a else b |
default | a default b (use b when a is null) |
| ternary | cond ? a : b |
| type cast | expr as Type { options } |
match | subject match { … } |
update | subject update { … } |
| logical or | or |
| logical and | and |
| logical not (prefix) | not x, !x |
| comparison | == != < <= > >= ~=, and x is Type |
| additive / shift / concat | + - << >> ++ |
| multiplicative | * / % mod |
| unary minus (prefix) | -x |
| postfix | selector chains (.field, […]), function calls |
| primary | literals, ( ) grouping, identifiers, objects, arrays, lambdas, do |
Operator notes:
++is strict concatenation: string with string, array with array, or object with object (merge).+also concatenates when either operand is a string (1 + "a"produces"1a"), concatenates or pushes for arrays, and adds for numbers.<<and>>append and prepend (and merge objects). They are not bit shifts.~=is coercive equality. It coerces the operands to a common shape before comparing.==and!=are strict.defaultsubstitutes a fallback fornull:payload.nickname default "n/a".- The lambda arrow
->is the two tokens-then>.~alone appears only in destructuring patterns.
See Operators and Precedence for the full operator reference, and Types and Coercion for how operators coerce their operands and what is, as, and ~= mean.
Expression Forms
Objects
{
id: payload.id,
"display name": payload.name,
(dynamicKeyExpr): value,
total: qty * price,
note: payload.note if payload.note, // entry included only if condition is truthy
element @(lang: "en"): "Hi" // XML attributes via @( … )
}
Keys may be bare identifiers, plain strings (no interpolation in a plain key), or dynamic (expr). Any entry may carry a trailing if cond to include it conditionally. A single key: value in value position is also a valid (braceless) one-entry object.
Arrays
[ 1, 2, 3, extra if includeExtra ]
Array elements may also carry a trailing if cond.
Ranges
Ranges exist only inside selector brackets and use the to keyword: payload.items[1 to 3]. There is no a..b or [a:b] range syntax.
Conditionals
There are three forms: the ternary cond ? a : b; if cond a else b, where the else is mandatory (there is no bare if and no then keyword); and match expressions (see Pattern Matching and Updates). Parentheses around an if predicate are optional.
Calls and Chaining
Functions are called in two equivalent ways: fn(receiver, args…) and the postfix (UFCS) form receiver fn(args…), which means the same call:
upper(payload.name)
payload.name upper()
payload.items map (row) -> row.id
There is no pipe operator and no .method() chaining. See Functions and Lambdas.
Blocks and Bindings
using(x = …, y = …) bodyExpr introduces local bindings for one expression. do { var x = …, fun f(a) = … --- body } is a block with its own bindings.
What Isn't Valid
The forms below are rejected. Each row shows the bad input and a plain description of why.
| Input | Why it's rejected |
|---|---|
0xFF, 1e3, 1_000 | Numbers can't use hex, exponent, or digit-grouping forms |
"abc | Unterminated string |
a | b | There is no | operator; | always opens a temporal literal, so this reads as an unterminated temporal literal |
input (as a name) | input is a reserved word and can't be used as a name |
|2024-13-99| | Invalid temporal literal (no such month or day) |
(1 + 2 | Missing closing parenthesis |
1 + | Expression ends with nothing after the + |
a[1:3] | [a:b] range syntax isn't supported; use [1 to 3] |
x match {} | A match needs at least one case or an else |
Related
- Writing a TransformScript — how to put a script together.
- Types and Coercion — the type set and how operators coerce.
- Selecting and Navigating Data — the full selector syntax.
- Functions and Lambdas — calls, UFCS, lambdas, and
$shorthand. - Operators and Precedence — the full operator reference.
- Pattern Matching and Updates —
matchandupdateexpressions.