Skip to main content

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:

WhereEvaluatedIf the expression fails
A job parameterAs the job is dispatchedThe job fails.
An event a job or workflow firesAs the event firesThe event still fires, and the field keeps the expression text.
A notification templateAs the notification is sentThe notification still goes out, and that field keeps the expression text.
An expression dependency on a jobRepeatedly while the job waits, until it renders TrueThe 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.

An expression dependency is read strictly, and against stored values

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 True would 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/False the platform acts on and never stores or sends.
Nothing is written yet

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:

OperandNotes
NumberAlways a floating-point number. "Integer" means a whole number that fits in 32 bits.
StringIn double quotes: "OK".
Booleantrue or false, matched ignoring case.
Property tokenA 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 typeYou 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 ^.

LevelOperators
100every 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.

Three groupings that surprise people

These are the model's own rules, not defects, and each is worth checking in an expression you inherit:

  • % binds tighter than *. 2 * 3 % 2 is 2 * (3 % 2) — that is 2, not 0.
  • && and || share one level, so they associate left to right. a || b && c is (a || b) && c.
  • ^ is left-associative. 2 ^ 3 ^ 2 is (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​

OperatorAcceptsNotes
+two numbers, or two stringsNumbers add; strings concatenate. A number and a string together is a type error.
-two numbers, or two stringsNumbers subtract; strings remove — "abcabc" - "b" is "acac", every occurrence. Removing an empty string is an error.
- (unary)a number
*two numbers
/two numbersDividing by zero fails; it does not produce infinity.
%two integersA non-whole value is a type error, so 4.5 % 2 fails while 4.0 % 2 is fine.
^two numbers
&&, ||two booleans
caution
&& and || do not short-circuit

Both 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​

OperatorAcceptsOn a type mismatch
==numbers, strings, booleansReturns false. Never fails.
<> or !=numbers, strings, booleansReturns true. Never fails. Both spellings are the same operator.
<, >, <=, >=two numbers, or two stringsFails.

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.5 and 0.5000004 — a == b and a != b are 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. The Index function, by contrast, searches by exact characters — so a string < comparison and an Index search 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​

FunctionReturns
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:

  • start must be a whole number. SubStr("abcdef", 1.5, 2) fails; it does not read from position 1.
  • A start at or past the end of the string fails, so SubStr("", 0) is an error rather than the empty string — worth knowing when the first argument is a property that may be empty.
  • start + len past the end of the string fails.

Use SubStrNE when the input may not be long enough; see Falling back instead of failing.

Conversion​

FunctionNotes
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​

FunctionNotes
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:

FormatReturns
signed_secs / unsigned_secsThe difference in seconds, signed or absolute.
signed_string / unsigned_stringThe difference as a duration string.
signed_percent / unsigned_percentThe 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​

FunctionNotes
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.

FunctionArguments
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 NE function is not simply its base function plus a fallback, and two differ on purpose: SubStrNE performs no bounds pre-check, so SubStrNE("", 0, "n/a") returns the empty string where SubStr("", 0) fails; and ToLongNE("2.5", 0) returns 2 where ToLong("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​

ResultRenders as
A stringIts text, without the quotes.
A whole number42
A very large or very small numberScientific notation — 1E-06, 1E+21 — rather than a long run of zeros.
A booleanTrue 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.
note
A degraded expression keeps its =

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 guard is coarse, and position matters

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​

LimitValue
Tokens in one expression100,000
Characters a single substitution may produce256 KB
Characters ToLower / ToUpper will fold256 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