Skip to content

Expressions

An expression is a formula that is evaluated when the playbook runs. It is what lets an activity use the result of another one, build dynamic text, do a calculation or make a decision.

If you come from Excel, the idea is similar: a cell can hold the text Total or a formula. In the Designer, a field can hold the fixed text "Hello" or the expression "Hello " + name.

Properties fields marked with ƒx accept an expression with C# syntax. What you type is exactly what gets evaluated, with no prefixes:

You want…You typeResult
Fixed text"Hello world"The text, as is
A fixed number30000The number
A variable’s valuecustomerNameWhatever the variable holds
Built text"Hello " + customerNameThe text with the value inside
A calculationsubtotal * 1.19The result

Fixed text always goes in double quotes. Without quotes, Hello is read as a variable name and the Problems panel tells you it doesn’t exist.

The ƒx marker is dimmed when the field holds fixed text in quotes and lit when it holds an expression, so you can tell at a glance which fields depend on run-time data.

Some fields are not free text:

  • Option lists (for example the Log level): you pick an option from the drop-down. The last one, ‹ expression ›, lets you compute the option with an expression.
  • Yes/no: a toggle.
  • Variable names (the output, the For Each iterator): you type just the name, without quotes.
  • Collections, conditions and selectors: they have their own editor. See Parameter editors.
  • Reuse the result of an activity. An HTTP GET activity stores its response in response; to read the status code you type response.StatusCode.
  • Build dynamic text. "Invoice " + number + " — " + customer.
  • Do calculations. subtotal * 1.19.
  • Evaluate a condition (in If, While…). attempts < 3 && !success.

You don’t need to know how to program for common formulas, but it helps to know the rules that most surprise people coming from Excel or other tools.

Type the variable name as is. It is case-sensitive:

name
total
response

Text always goes in double quotes "...". Single quotes are for one character (see padLeft below).

"Hello world"
"The customer is " + customer // concatenation with +
"Total: " + total + " USD"
"She said \"yes\"" // quotes inside the text, with \"
42
3.14 // the decimal separator is the dot
price * quantity
(subtotal + shipping) * 1.19
total / 12

Use the dot . to read a property of an object, and square brackets [...] to access by key or index:

response.StatusCode // property
response.Headers["Content-Type"] // dictionary value by key
row["ColumnName"] // cell of a DataTable row
list[0] // first element of a list (index 0)
date.Year // property of a date

On dynamic data (variables holding dictionaries or lists, parseJson(...), asDict(...), asList(...)), a missing key or an out-of-range index gives null, with no error: order["discount"] and asDict(item)["discount"] behave the same. Compare it with == null or use str(...), which turns it into "".

A typed dictionary or list (Dictionary<string, string>, List<int>…), on the other hand, does fail when the key does not exist, and the error says which key and in which expression. If the key may be missing, read it with jget(value, "key"), which returns null.

KindOperatorsExample
Arithmetic+ - * / %price * 1.19
Comparison== != < > <= >=total > 1000
Logical&& (and) || (or) ! (not)active && !blocked
Text concatenation+"Hello " + name
Conditional (ternary)condition ? valueIfTrue : valueIfFalsetotal > 0 ? "Charge" : "Free"

The Designer comes with a set of ready-to-use functions. They are all lowercase (upper, not Upper). Autocomplete suggests them with their signature, and the large expression editor lists them in its catalog.

FunctionSignatureDescription
upper(s)(string) → stringConverts to uppercase
lower(s)(string) → stringConverts to lowercase
trim(s)(string) → stringRemoves leading and trailing spaces
length(s)(string) → intText length
replace(s, old, new)(string, string, string) → stringReplaces every occurrence
split(s, separator)(string, string) → List<string>Splits the text into a list
contains(s, sub)(string, string) → booltrue if s contains sub
startsWith(s, prefix)(string, string) → booltrue if it starts with the prefix
endsWith(s, suffix)(string, string) → booltrue if it ends with the suffix
substring(s, start, length)(string, int, int) → stringExtracts a portion from start
padLeft(s, width, char)(string, int, char) → stringPads on the left up to width
padRight(s, width, char)(string, int, char) → stringPads on the right up to width
nl()() → stringA line break
tab()() → stringA tab
upper(name) // "ana" → "ANA"
trim(textWithSpaces)
replace(phone, "-", "") // removes the dashes
contains(subject, "URGENT")
padLeft(toString(number), 6, '0') // 42 → "000042" (note: '0' is a char)
FunctionSignatureDescription
abs(x)(double) → doubleAbsolute value
round(x, d)(double, int) → doubleRounds to d decimals
floor(x)(double) → doubleRounds down
ceil(x)(double) → doubleRounds up
sqrt(x)(double) → doubleSquare root
pow(x, exp)(double, double) → doublePower
min(a, b)(double, double) → doubleThe smaller of two numbers
max(a, b)(double, double) → doubleThe larger of two numbers
mod(a, b)(double, double) → doubleRemainder of the division (modulo)
round(total * 1.19, 2) // tax with 2 decimals
max(stock, 0) // never below 0
FunctionSignatureDescription
toString(v)(any) → stringConverts any value to text
toNumber(v)(any) → doubleConverts to a decimal number
toInt(v)(any) → intConverts to an integer
toBoolean(v)(any) → boolConverts to true/false
parseDouble(s)(string) → doubleReads a decimal using the dot as separator
parseInt(s)(string) → intReads an integer
FunctionSignatureDescription
isNull(v)(any) → booltrue if the value is null
isEmpty(s)(string) → booltrue if null or empty string
isNumber(v)(any) → booltrue if it can be read as a number
isEmpty(email) ? "no email" : email
isNumber(cell) && toNumber(cell) > 0
FunctionSignatureDescription
now()() → DateTimeCurrent local date and time
today()() → DateTimeToday at 00:00:00 (no time)
utcNow()() → DateTimeCurrent date and time in UTC

To add time or format, use the methods of the DateTime type (see Data types):

now()
today().AddDays(-1) // yesterday
now().AddHours(2)
now().ToString("yyyy-MM-dd") // "2026-06-06"
now().ToString("dd/MM/yyyy HH:mm")
date.Year
FunctionSignatureDescription
count(e)(IEnumerable) → intNumber of elements
join(e, sep)(IEnumerable, string) → stringJoins the elements into a text
first(e)(IEnumerable) → anyFirst element
last(e)(IEnumerable) → anyLast element
newList()() → ListCreates an empty list
newDict()() → DictionaryCreates an empty dictionary
arr(a, b, …)(any…) → ListCreates a list with the given values
listAdd(list, v)(List, any) → ListAdds an element and returns the list
listGet(list, i)(List, int) → anyElement at position i
listCount(list)(List) → intNumber of elements
dictSet(d, key, v)(Dict, string, any) → DictSets a key and returns the dictionary
dictGet(d, key)(Dict, string) → anyValue of a key
count(emails) // how many emails arrived?
join(names, ", ") // ["Ana","Luis"] → "Ana, Luis"
first(rows)
arr("pending", "in review")
FunctionSignatureDescription
parseJson(s)(string) → objectTurns JSON text into an object you read with brackets
toJson(v)(any) → stringTurns a value into JSON text
toJsonPretty(v)(any) → stringThe same, indented
jget(o, key) / jget(o, i)(any, string|int) → anyValue of a key or a position
jpath(o, path)(any, string) → anyValue at a path
asDict(o) / asList(o)(any) → Dictionary / ListTreats the value as a dictionary or a list. With [...], a missing key or an out-of-range index gives null
parseJson(responseText)["customer"]["name"]
toJson(data)

When you work with tables (from Excel, CSV, databases or Google Sheets) there are specific functions. str, num and eq are important because they safely handle empty cells (DBNull).

FunctionSignatureDescription
rowCount(dt)(DataTable) → intNumber of rows
colCount(dt)(DataTable) → intNumber of columns
firstRow(dt)(DataTable) → DataRow?First row, or null if empty
asRows(dt)(DataTable) → List<DataRow>Turns the table into a list (to use with LINQ)
selectRows(dt, pred)(DataTable, Func) → List<DataRow>Filters rows with a condition
str(v)(any) → stringSafe text: "" if the cell is null
num(v)(any) → doubleSafe number: 0 if the cell is null or empty; reads text with a decimal comma or point (see below)
num(v, culture)(any, string) → doubleReads the text with that culture’s separators: "es-CO", "en-US", "invariant"
eq(a, b)(any, any) → boolSafe equality between cells (handles nulls)
rowCount(table)
str(row["Customer"]) // text of a cell, no errors if empty
num(row["Total"]) > 1000
selectRows(table, r => str(r["Status"]) == "Pending")

Since version 1.2.29. A number that arrives as text (from a CSV, a web page or an Excel cell formatted as text) can use a comma or a point as the decimal separator. num works out the decimal separator from its position, so the amount is always the right one:

Textnum(...)Why
"1500,5"1500.5A single separator not followed by three digits: it is the decimal
"1500.5"1500.5Same, with a point
"1.500,5"1500.5Both a point and a comma: the last one is the decimal
"1,500.5"1500.5Same, the other way round
"1.500.000"1500000The same separator repeated can only be thousands
"1 500,5"1500.5Spaces are always thousands
"1.500,50-"-1500.5Trailing sign, as in some accounting reports
"1.500"errorAmbiguous: it can be one thousand five hundred or one point five

When the text is ambiguous (a single separator followed by exactly three digits, such as "1.500" or "12,345"), num does not guess: the activity fails with a message that explains why. Give the source culture as the second parameter and its separators are used, with nothing deduced:

num(row["Total"], "es-CO") // "1.500" → 1500 (decimal comma, thousands point)
num(row["Total"], "en-US") // "1.500" → 1.5 (decimal point, thousands comma)

With a culture, text that does not fit its separators (for example "1500.5" with "es-CO", where the point is the thousands separator) is also an error, never a different number. If the cell is already a number (like the ones Read Excel Sheet returns), num returns it as is, whatever the culture.

FunctionSignatureDescription
fileExists(path)(string) → booltrue if the file exists
dirExists(path)(string) → booltrue if the folder exists
createDir(path)(string) → stringCreates the folder and returns its full path
pathJoin(a, b)(string, string) → stringJoins two path parts
fileName(path)(string) → stringFile name of a path
dirName(path)(string) → stringContaining folder of a path
asset(rel)(string) → stringAbsolute path of a file inside your project
pathJoin(outputFolder, "report.xlsx")
asset("templates/invoice.docx") // path to a file included in the project
fileName(fullPath) // "C:\x\data.csv" → "data.csv"
FunctionSignatureDescription
env(name)(string) → string?Reads a system environment variable
credential(name)(string) → CredentialGets a credential from Zoan Cloud or from the Windows Credential Manager
env("USERNAME")
credential("smtp")["password"] // accesses a field of the credential

env() returns nothing for variables that start with ZOAN_: that way a playbook can never read the platform’s internal data.

To extract or validate unstructured data (an invoice number inside a text, an email, a date). They use the .NET dialect.

FunctionSignatureDescription
regexMatch(text, pattern)(string, string) → booltrue if the pattern appears in the text
regexFind(text, pattern)(string, string) → stringFirst match, or ""
regexFindAll(text, pattern)(string, string) → ListEvery match
regexExtract(text, pattern)(string, string) → ListThe captured groups of the first match
regexGroup(text, pattern, name)(string, string, string) → stringA named capture group
regexReplace(text, pattern, repl)(string, string, string) → stringReplaces every match (supports $1, ${name})
regexFind(emailText, @"\d{4}-\d{2}-\d{2}") // first yyyy-mm-dd date
regexMatch(email, @"^[^@]+@[^@]+\.[^@]+$") // is it a valid email?
regexGroup(text, @"(?<invoice>INV-\d+)", "invoice")

For more powerful operations on lists and tables (filter, sort, transform) you can use LINQ, chaining methods with a dot. End with .ToList() when you need an actual list.

// Filter a list
numbers.Where(n => n > 100).ToList()
// Sort and take the first one
customers.OrderBy(c => c.Balance).First()
// Filter table rows and count them
asRows(table).Where(r => str(r["Status"]) == "Pending").ToList()
count(asRows(table).Where(r => num(r["Total"]) > 1000))

The available LINQ methods (Where, Select, OrderBy, Any, All, First, ToList…) are listed by type in Data types.

While you type an expression, the Designer helps you in several ways:

  • Autocomplete. As you start typing, a list appears with the playbook variables, the built-in functions and the system variables, each with its signature. After a dot (variable.) it suggests the valid members for that variable’s type. ↑/↓ move through the list, Tab or Enter accept, Esc closes it and Ctrl+Space opens it whenever you want.
  • Create the variable without leaving the field. If you type the name of a variable that doesn’t exist yet, Ctrl+Shift+V declares it in the playbook with the type that field expects.
  • Expression editor. The field’s { } button opens a large editor, with a catalog of Variables, Functions and System variables to insert with one click. At the bottom it confirms Valid expression and the type it returns.
  • Live validation. The Designer compiles each expression while you edit. If something doesn’t fit, the field is marked and the error shows up in the Problems panel, with a link to the activity and the field.
  • Errors (red): syntax mistakes, an incompatible type or a name that doesn’t match any declared variable. They block running and publishing, because the playbook would fail halfway on the robot.
  • Warnings (amber): things worth checking that don’t block, such as a fragile selector or a template to-do.
SymptomCauseSolution
”The name ‘Hello’ does not exist in the current context”You typed fixed text without quotes, or the variable is not declaredPut it in quotes: "Hello"
Upper(...) / FormatDate(...) doesn’t existYou are using names with an initial capitalFunctions are lowercase: upper, and for date formatting use date.ToString("...")
A text comparison is always falseYou compared DataTable cells with ==Use str(cell) == "value" or eq(a, b)
A decimal is read wrongData with a decimal commaReplace the comma with a dot before parseDouble
\n shows up literally in the textThe backslash is taken literallyUse nl() (and tab() for a tab)
“Newline in constant” / “Unterminated string literal”The closing quote is missing, or a backslash right before it makes it part of the textClose the text; for a path ending in a backslash write @"C:\folder\" or use pathJoin(folder, name)
Incompatible typeThe expression returns a different type from what the field expectsConvert the value (toString, toInt…) or check the variable’s type
The key “x” does not exist in the dictionaryA typed dictionary does not have that keyIf it may be missing, use jget(value, "x"), which returns null
  • Variables: declare and capture data between activities.
  • Data types: what properties and methods each type has.
  • Debugging: try your expressions by running the playbook.