Expressions in property tokens
Written from the Properties and tags token grammar. Read that page first
— an expression is a property token with a = after the opening delimiter, and everything it says
about scopes, system names, delimiters, and when tokens resolve applies here unchanged.
A property token normally names a value to look up. An expression token computes one instead:
[[= [[JI.RetryCount]] + 1 ]]
[[= ToUpper([[OI.Environment]]) ]]
[[= SubStr([[JI.$JOB NAME]], 0, 8) ]]
[[= [[OI.LastRunStatus]] == "OK" ]]
Write it as [[= … ]] or {{= … }}. The one-delimiter-pair-per-string rule
still decides which pair is live, so an expression written with {{= in a string that contains [[
anywhere is left as written.
Where an expression is evaluated
The three moments a plain token resolves, under the same rules — plus one place an expression is the whole point rather than part of a string:
| Where | Evaluated | If the expression fails |
|---|---|---|
| A job parameter | As the job is dispatched | The job fails. |
| An event a job or workflow fires | As the event fires | The event still fires, and the field keeps the expression text. |
| A notification template | As the notification is sent | The notification still goes out, and that field keeps the expression text. |
| An expression dependency on a job | Repeatedly while the job waits, until it renders True | The job is put On Hold and the cause is recorded against it. |
An expression is charged against the same work budget as the rest of the string it sits in, so two expressions in one job parameter share one budget rather than each getting their own.
Two of this page's rules are reversed at that gate, and both follow from its answer being a decision rather than a value:
- No degraded reading. The fallback described in
When an expression fails is what the first three rows do. Source text
that happened to read
Truewould open the gate, so an unresolvable token there holds the job instead. - No masking. An encrypted property compares as its stored value rather than as a mask, since a
mask would answer a comparison that has no relation to the value. The result is a
True/Falsethe platform acts on and never stores or sends.
The language parses assignment — [[= [[OI.Flag]] = "x" ]] — and then refuses it. No expression
can change a property, a threshold, or a resource count. See
Assignment is refused.
Operands
An expression is built from four kinds of operand:
| Operand | Notes |
|---|---|
| Number | Always a floating-point number. "Integer" means a whole number that fits in 32 bits. |
| String | In double quotes: "OK". |
| Boolean | true or false, matched ignoring case. |
| Property token | A full [[Name]] / [[Scope.Name]] token, resolved exactly as it would be on its own — including its scope routing, date formats, and offsets. |
Whitespace between operands and operators is ignored. Operator and function names are matched
ignoring case, so SubStr, substr and SUBSTR are one function.
A property token inside an expression goes through the ordinary resolution path, so an encrypted property is masked rather than substituted, a cycle is still detected, and a name that cannot be resolved fails the expression rather than silently becoming empty.
Writing a quote or a backslash in a string
| You type | You get |
|---|---|
"C:\\logs" | C:\logs — a doubled backslash is one backslash. |
"say \\\"hi\\\"" | say "hi" — three backslashes before the quote. |
"say \"hi\"" | Refused: Escaped quotes are not supported inside an expression string literal; double the escape to write a quote. |
One or two backslashes before a quote are refused by name rather than producing a mangled token.
Precedence
Operators are applied by precedence, highest first. Everything is left-associative, including
^.
| Level | Operators |
|---|---|
| 100 | every named function, and unary - |
| 90 | ^ |
| 80 | % |
| 70 | * / |
| 50 | + - |
| 20 | == <> != < > <= >= |
| 10 | && || |
| 7 | = (assignment — parsed, then refused) |
| 5 | , (the argument separator is an operator, not punctuation) |
Use parentheses when you want a different grouping. Unbalanced parentheses are a syntax error.
These are the model's own rules, not defects, and each is worth checking in an expression you inherit:
%binds tighter than*.2 * 3 % 2is2 * (3 % 2)— that is 2, not 0.&&and||share one level, so they associate left to right.a || b && cis(a || b) && c.^is left-associative.2 ^ 3 ^ 2is(2 ^ 3) ^ 2— that is 64, not 512.
- is read as subtraction when it follows a ) or an operand, and as negation otherwise. Two
adjacent minus signs are read as one subtraction operator, so 5--2 is 3.
Arithmetic and logic
| Operator | Accepts | Notes |
|---|---|---|
+ | two numbers, or two strings | Numbers add; strings concatenate. A number and a string together is a type error. |
- | two numbers, or two strings | Numbers subtract; strings remove — "abcabc" - "b" is "acac", every occurrence. Removing an empty string is an error. |
- (unary) | a number | |
* | two numbers | |
/ | two numbers | Dividing by zero fails; it does not produce infinity. |
% | two integers | A non-whole value is a type error, so 4.5 % 2 fails while 4.0 % 2 is fine. |
^ | two numbers | |
&&, || | two booleans |
&& and || do not short-circuitBoth sides are evaluated before the operator is applied, so a || cannot guard the expression on
its right. [[= [[OI.Divisor]] == 0 \|\| 100 / [[OI.Divisor]] > 1 ]] still fails when the divisor is
zero. There is also no truthiness: 1 && true is a type error, not true.
Comparison
| Operator | Accepts | On a type mismatch |
|---|---|---|
== | numbers, strings, booleans | Returns false. Never fails. |
<> or != | numbers, strings, booleans | Returns true. Never fails. Both spellings are the same operator. |
<, >, <=, >= | two numbers, or two strings | Fails. |
Two things about comparison decide answers a reader would otherwise have to guess at:
==compares numbers with a tolerance;!=compares them exactly. So for two values that differ only in the last few digits —0.5and0.5000004—a == banda != bare both true. Pick one operator and stay with it rather than assuming the other is its negation.- String ordering is linguistic, not character-by-character.
<and its siblings order strings the way a person sorting a list would, in the culture the platform is rendering dates in. TheIndexfunction, by contrast, searches by exact characters — so a string<comparison and anIndexsearch can disagree about whether two strings are "the same".
AreEqual(a, b, …) compares an argument list. It compares adjacent pairs, uses the same
tolerance == does for numbers, returns false on the first type mismatch — and returns true
for a single argument, because there is no pair to compare.
Functions
Every function is written Name(arguments), binds tighter than any operator, and rejects the wrong
number of arguments rather than guessing.
Strings
| Function | Returns |
|---|---|
SubStr(s, start[, len]) | The slice of s beginning at start (0-based). |
Index(hay, needle) | The position of needle in hay, or -1 if it is not there. |
Replace(s, find, with) | s with every occurrence of find replaced. |
ReplaceBackslashes(s, with) | s with every backslash replaced. |
Length(s) | The number of characters in s. |
ToLower(s), ToUpper(s) | s case-folded. |
SubStr is strict about its bounds, and this is the function most likely to fail on real data:
startmust be a whole number.SubStr("abcdef", 1.5, 2)fails; it does not read from position 1.- A
startat or past the end of the string fails, soSubStr("", 0)is an error rather than the empty string — worth knowing when the first argument is a property that may be empty. start + lenpast the end of the string fails.
Use SubStrNE when the input may not be long enough; see Falling back instead of failing.
Conversion
| Function | Notes |
|---|---|
ToStr(x) | Renders x as a string. A boolean renders as True or False. |
ToInt(x) | Truncates toward zero, so ToInt(-2.7) is -2, not -3. A string is read as a number first, so ToInt("2.7") is 2. A boolean is refused. |
ToFloat(x) | A number, or a string read as one. |
ToLong(x) | A number is truncated. A string must be a whole number — ToLong("2.5") fails, where ToInt("2.5") is 2. |
ToBool(x) | Accepts only the text true or false, in any case, with surrounding spaces allowed. A number is refused — ToBool(1) fails. |
Dates and durations
| Function | Notes |
|---|---|
ToOaTime("[[dd:]hh:]mm") | Converts a duration into a serial date-time number. One, two, or three colon-separated parts — minutes, hh:mm, or dd:hh:mm — each a non-negative whole number. A leading + is accepted; a leading - is not. |
TimeDiff(t1, t2, format) | The difference between two durations. Both times must be strings in exactly hh:mm:ss. |
TimeDiff's format is one of six names:
| Format | Returns |
|---|---|
signed_secs / unsigned_secs | The difference in seconds, signed or absolute. |
signed_string / unsigned_string | The difference as a duration string. |
signed_percent / unsigned_percent | The difference as a percentage of the second duration. |
The two percent formats divide by the second duration, so they fail when it is 00:00:00.
Maths and nested evaluation
| Function | Notes |
|---|---|
LogToBase(x, base) | The logarithm of x in the given base. |
Expr(s) | Evaluates the string s as an expression. |
Expr always returns a string, whatever the inner expression computed. So Expr("1+1") + 1 is
a type error, and Expr("1+1") + "x" is "2x". A chain of nested Expr calls is bounded by the
same depth limit as the rest of the resolution, so it fails with a depth error rather than running
away.
Falling back instead of failing
Six functions take one extra trailing argument and return it when the operation fails. This is the only error handling the language has.
| Function | Arguments |
|---|---|
SubStrNE(s, start, fallback) or SubStrNE(s, start, len, fallback) | |
ToStrNE(x, fallback) | |
ToIntNE(x, fallback) | |
ToFloatNE(x, fallback) | |
ToLongNE(x, fallback) | |
TimeDiffNE(t1, t2, format, fallback) |
Three things to know about them:
- The fallback covers the operation, not the call. Calling one with the wrong number of
arguments is still an error —
SubStrNE("abc", 0)fails rather than returning something. - An
NEfunction is not simply its base function plus a fallback, and two differ on purpose:SubStrNEperforms no bounds pre-check, soSubStrNE("", 0, "n/a")returns the empty string whereSubStr("", 0)fails; andToLongNE("2.5", 0)returns2whereToLong("2.5")fails. ToStrNE's fallback can never fire, because nothing in the conversion it performs can fail. It exists for compatibility.
The fallback covers a syntax, type, or divide-by-zero failure. It does not swallow a budget limit, a cycle, or a refused assignment — those are reported rather than hidden.
What a result looks like
| Result | Renders as |
|---|---|
| A string | Its text, without the quotes. |
| A whole number | 42 |
| A very large or very small number | Scientific notation — 1E-06, 1E+21 — rather than a long run of zeros. |
| A boolean | True or False |
A result outside the range a number can represent — 10 ^ 400, for instance — fails rather than
rendering as infinity, because an infinity spliced into a job's command line is worse than an error.
When an expression fails
Four kinds of read failure are reported separately, so a log entry says which one happened: a syntax error, a type error, a divide by zero, and a wrong argument count.
What that costs depends on where the expression is:
- In a job parameter, the job fails. A parameter is an instruction to a machine.
- In an event field or a notification template, the message still goes out and that field keeps the expression's own text. The unit is the field: one bad expression degrades on its own and every other substitution in the same field still resolves.
=A degraded token renders the text inside its delimiters — which for an expression includes the
leading marker. [[= 1 @@ 2 ]] renders as = 1 @@ 2, not as 1 @@ 2. That is what to search the
rendered output for when you are hunting one down.
Assignment is refused
= is the assignment operator, and the language parses it so that an assignment can be recognised
and refused specifically, rather than every expression being turned away. Any attempt to assign
fails with a refusal that says so.
That refusal is never degraded, anywhere — unlike a read failure:
- In a job parameter, the job fails.
- In a notification template, the send fails. A template that assigns has to be edited; it will not start working on its own.
This is deliberate: an attempted write that degraded quietly would be a write nobody ever learned was tried.
A run's own facts cannot be used as expression source
The values the platform captures from a run — a job name, a termination description, a schedule
date, and the other names an event can resolve
— are facts, not templates. Several of them arrive from an agent on your own infrastructure, so
they are not allowed to build a token name or to be handed to Expr. A job renamed
[[OI.Secret]] = "x" would otherwise become an expression authored by whoever can rename a job.
Reading such a value is fine — [[= ToUpper([[JI.$JOB NAME]]) ]] works. Handing one to Expr is
refused.
The refusal triggers when any captured run fact was read earlier in the same expression, not
only when one is Expr's own argument. So these two are not equivalent:
[[= [[JI.$JOB NAME]] + " " + Expr("1+1") ]] refused
[[= Expr("1+1") + " " + [[JI.$JOB NAME]] ]] evaluates
Same names, same operators, different order, different answer. If an Expr call is refused and you
cannot see why, look for a run fact read ahead of it in the same expression.
Limits
| Limit | Value |
|---|---|
| Tokens in one expression | 100,000 |
| Characters a single substitution may produce | 256 KB |
Characters ToLower / ToUpper will fold | 256 KB |
Depth, total expansions, and total output length are the resolution budgets
of the string the expression sits in — the expression does not get its own. Exceeding one is reported
as a budget failure and is never degraded or swallowed by an NE fallback.
Contact support when
- An expression that worked in Classic returns a different answer here, and it is not one of the rules on this page.
- You need an expression to set a property, a threshold, or a resource count.
- A notification stops sending after a template edit and the template contains
=inside an expression.
Related topics
- Properties and tags — the token grammar, scopes, and when tokens resolve
- Notifications — template tokens and what is frozen at event time
- Workflow events — the Completion Expression trigger, which is a separate mechanism and is not yet active