Skip to main content

Expressions and Functions

Anywhere a flow computes rather than stores โ€” an If condition, a Loop bound, an edge condition, a Variable assignment, the text an Input Text node types โ€” the same small language is doing the work. This page is its reference.

It is deliberately small. It has no loops, no assignment, no way to reach the filesystem or the network. An expression reads values and returns one.

Referring to a valueโ€‹

{{gem_count}} the variable gem_count
${gem_count} identical โ€” both spellings are accepted
vars.gem_count the same variable, written out
result.output.text an upstream node's output

See Variables for where the values come from.

Operatorsโ€‹

Highest precedence first โ€” the order the parser applies them:

LevelOperatorsNotes
Access. [ ]Property and index: split(x, ",")[0]
Unary! -Not, negate
Multiplicative* / %
Additive+ -
Comparison< <= > >=
Equality== !=
Coalesce??Left side unless it is null
And&&Short-circuits
Or||Short-circuits

Parentheses group as you would expect.

Arithmetic is strictly typed, on purpose

+ joins two strings, and adds two numbers. It refuses a mixed pair โ€” "5" + 1 raises rather than guessing which one you meant. Booleans are never operands. Division by zero raises.

This is not an oversight. Loosening + would silently change the result of every flow already shipped, so conversion is a function you can see in the expression โ€” number(), text(), int() โ€” never a hidden coercion. The Variable node's intent rows write these calls for you: pick "Add 1 to counter" and you get a correct conversion without writing one.

The functionsโ€‹

29 names, all listed below. Every one is total โ€” it answers rather than raising on odd input, wherever answering is possible โ€” and every one except random is deterministic, so a condition replays identically.

Text predicates coerce null to the empty string, so contains(result.output.text, "Start") is safe even when the detection above it read nothing: you get false, not a dead run.

Testing textโ€‹

FunctionReturns
contains(text, part)Whether part appears in text
not_contains(text, part)The opposite
starts_with(text, part)
ends_with(text, part)
equals_ignore_case(a, b)Equality, ignoring case
is_empty(value)Empty string, empty list, empty object, or null
is_not_empty(value)The opposite
regex_match(text, pattern)Whether the pattern is found anywhere in text

Shaping textโ€‹

FunctionReturns
lower(text) / upper(text)Case conversion
trim(text)Surrounding whitespace removed
concat(a, b, โ€ฆ)Every argument joined as text โ€” the mixed-type join + refuses
replace(text, find, replacement)Every literal occurrence. Not a regex. An empty find returns text unchanged
split(text, separator)The parts, as a list โ€” index it: split(x, ",")[0]. An empty separator raises
substring(text, start)From start to the end. A negative start counts back from the end
substring(text, start, length)At most length characters
extract(text, pattern)The first capture group, or the whole match when the pattern has none โ€” or null when nothing matched
extract(text, pattern, group)A specific group
default(value, fallback)fallback when value is null or empty
len(value) / length(value)Character count, list length, or object size. null is 0

substring clamps rather than faults. A start past the end gives "". An OCR read that came back shorter than usual is an ordinary Tuesday, not a reason to kill a run.

extract returns null on no match, which is what makes it compose: extract({{ocr}}, "[0-9]+") ?? "0".

default is wider than ??. ?? only covers null; default() also covers the empty string, which is the everyday shape of a failed read.

Numbersโ€‹

FunctionReturns
number(value)The numeric value โ€” raises if it is not a number
number(value, fallback)โ€ฆor the fallback instead of raising
text(value)The value as text
is_number(value)Whether number() would succeed
int(value)Truncated toward zero โ€” int(-2.7) is -2
round(value) / round(value, digits)Ties go away from zero
floor(value) / ceil(value)
abs(value)
min(a, b, โ€ฆ) / max(a, b, โ€ฆ)
clamp(value, low, high)Bounded. Raises if low > high
random(low, high)A whole number in [low, high], both ends included
number() does not guess separators

"1,234" is genuinely ambiguous โ€” a comma means thousands in English and decimal in Vietnamese. number() accepts a plain numeric literal only. Strip separators yourself with replace(), so the locale decision is visible in the flow instead of assumed by it.

round rounds what you wrote. round(1.005, 2) is 1.01. Rounding the binary approximation instead would give 1.0 โ€” defensible, and impossible to explain in a support thread.

random is the one non-deterministic function, and that is its whole purpose: a flow that always uses the same caps repeats itself exactly. It is re-drawn on every evaluation โ€” nothing caches it. Bounds are accepted in either order, and are rounded inward so a fractional argument can never widen the range.

random(1, 100) <= 20 one run in five

Legacy spellingsโ€‹

Older flows and anything the no-code builder compiled may use camelCase: notContains, startsWith, endsWith, equalsIgnoreCase, isEmpty, isNotEmpty. They still work and are identical to the snake_case names. Write the snake_case form in anything new.

Worked patternsโ€‹

Read a number out of a noisy OCR result.

number(extract({{ocr_text}}, "[0-9]+") ?? "0")

extract pulls the digits out of "LEVEL 5", ?? supplies a value when the read found nothing, and number makes it arithmetic. Without the fallback, a blank read would fault.

Take the nth field of the nth record from one big clipboard string.

split({{noidung}}, "\n")[{{i}} * 5 + 1]

This is the core of the desktop data-entry example: one Ctrl+C brings the whole file back as text, and a Loop counter walks it five lines at a time.

A percentage that never goes out of bounds.

clamp(round({{done}} / {{total}} * 100), 0, 100)

Common mistakesโ€‹

MistakeWhat happensFix
{{ocr}} + 1 on an OCR readRaises โ€” a string and a numbernumber({{ocr}}, 0) + 1
number() on "1,234"Raisesnumber(replace({{x}}, ",", ""))
replace() with a regexNothing matches โ€” it is literalUse extract() for patterns
Using ?? on an empty string?? only covers nulldefault(value, fallback)
Assuming extract returns ""It returns nullFollow it with ?? or default()
random in a condition you expect to replayIt is re-drawn every evaluationStore it in a variable first
  • Variables, Inputs and Memory โ€” where the values live
  • If โ€” the node that most often holds one
  • Variable โ€” assignment, and the intent rows that write these calls for you
  • Loop โ€” counters and bounds