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.
One field, one C# expression
Section titled “One field, one C# expression”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 type | Result |
|---|---|---|
| Fixed text | "Hello world" | The text, as is |
| A fixed number | 30000 | The number |
| A variable’s value | customerName | Whatever the variable holds |
| Built text | "Hello " + customerName | The text with the value inside |
| A calculation | subtotal * 1.19 | The 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.
When do I need an expression?
Section titled “When do I need an expression?”- Reuse the result of an activity. An HTTP GET activity stores its response in
response; to read the status code you typeresponse.StatusCode. - Build dynamic text.
"Invoice " + number + " — " + customer. - Do calculations.
subtotal * 1.19. - Evaluate a condition (in If, While…).
attempts < 3 && !success.
Basic syntax
Section titled “Basic syntax”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.
Referencing variables
Section titled “Referencing variables”Type the variable name as is. It is case-sensitive:
nametotalresponseText (strings)
Section titled “Text (strings)”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 \"Numbers and arithmetic
Section titled “Numbers and arithmetic”423.14 // the decimal separator is the dotprice * quantity(subtotal + shipping) * 1.19total / 12Accessing properties and elements
Section titled “Accessing properties and elements”Use the dot . to read a property of an object, and square brackets [...] to access by key or index:
response.StatusCode // propertyresponse.Headers["Content-Type"] // dictionary value by keyrow["ColumnName"] // cell of a DataTable rowlist[0] // first element of a list (index 0)date.Year // property of a dateOn 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.
Operators
Section titled “Operators”| Kind | Operators | Example |
|---|---|---|
| Arithmetic | + - * / % | price * 1.19 |
| Comparison | == != < > <= >= | total > 1000 |
| Logical | && (and) || (or) ! (not) | active && !blocked |
| Text concatenation | + | "Hello " + name |
| Conditional (ternary) | condition ? valueIfTrue : valueIfFalse | total > 0 ? "Charge" : "Free" |
Built-in functions
Section titled “Built-in functions”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.
| Function | Signature | Description |
|---|---|---|
upper(s) | (string) → string | Converts to uppercase |
lower(s) | (string) → string | Converts to lowercase |
trim(s) | (string) → string | Removes leading and trailing spaces |
length(s) | (string) → int | Text length |
replace(s, old, new) | (string, string, string) → string | Replaces every occurrence |
split(s, separator) | (string, string) → List<string> | Splits the text into a list |
contains(s, sub) | (string, string) → bool | true if s contains sub |
startsWith(s, prefix) | (string, string) → bool | true if it starts with the prefix |
endsWith(s, suffix) | (string, string) → bool | true if it ends with the suffix |
substring(s, start, length) | (string, int, int) → string | Extracts a portion from start |
padLeft(s, width, char) | (string, int, char) → string | Pads on the left up to width |
padRight(s, width, char) | (string, int, char) → string | Pads on the right up to width |
nl() | () → string | A line break |
tab() | () → string | A tab |
upper(name) // "ana" → "ANA"trim(textWithSpaces)replace(phone, "-", "") // removes the dashescontains(subject, "URGENT")padLeft(toString(number), 6, '0') // 42 → "000042" (note: '0' is a char)Numbers
Section titled “Numbers”| Function | Signature | Description |
|---|---|---|
abs(x) | (double) → double | Absolute value |
round(x, d) | (double, int) → double | Rounds to d decimals |
floor(x) | (double) → double | Rounds down |
ceil(x) | (double) → double | Rounds up |
sqrt(x) | (double) → double | Square root |
pow(x, exp) | (double, double) → double | Power |
min(a, b) | (double, double) → double | The smaller of two numbers |
max(a, b) | (double, double) → double | The larger of two numbers |
mod(a, b) | (double, double) → double | Remainder of the division (modulo) |
round(total * 1.19, 2) // tax with 2 decimalsmax(stock, 0) // never below 0Type conversion
Section titled “Type conversion”| Function | Signature | Description |
|---|---|---|
toString(v) | (any) → string | Converts any value to text |
toNumber(v) | (any) → double | Converts to a decimal number |
toInt(v) | (any) → int | Converts to an integer |
toBoolean(v) | (any) → bool | Converts to true/false |
parseDouble(s) | (string) → double | Reads a decimal using the dot as separator |
parseInt(s) | (string) → int | Reads an integer |
Validation
Section titled “Validation”| Function | Signature | Description |
|---|---|---|
isNull(v) | (any) → bool | true if the value is null |
isEmpty(s) | (string) → bool | true if null or empty string |
isNumber(v) | (any) → bool | true if it can be read as a number |
isEmpty(email) ? "no email" : emailisNumber(cell) && toNumber(cell) > 0Date and time
Section titled “Date and time”| Function | Signature | Description |
|---|---|---|
now() | () → DateTime | Current local date and time |
today() | () → DateTime | Today at 00:00:00 (no time) |
utcNow() | () → DateTime | Current date and time in UTC |
To add time or format, use the methods of the DateTime type (see Data types):
now()today().AddDays(-1) // yesterdaynow().AddHours(2)now().ToString("yyyy-MM-dd") // "2026-06-06"now().ToString("dd/MM/yyyy HH:mm")date.YearCollections (lists and dictionaries)
Section titled “Collections (lists and dictionaries)”| Function | Signature | Description |
|---|---|---|
count(e) | (IEnumerable) → int | Number of elements |
join(e, sep) | (IEnumerable, string) → string | Joins the elements into a text |
first(e) | (IEnumerable) → any | First element |
last(e) | (IEnumerable) → any | Last element |
newList() | () → List | Creates an empty list |
newDict() | () → Dictionary | Creates an empty dictionary |
arr(a, b, …) | (any…) → List | Creates a list with the given values |
listAdd(list, v) | (List, any) → List | Adds an element and returns the list |
listGet(list, i) | (List, int) → any | Element at position i |
listCount(list) | (List) → int | Number of elements |
dictSet(d, key, v) | (Dict, string, any) → Dict | Sets a key and returns the dictionary |
dictGet(d, key) | (Dict, string) → any | Value of a key |
count(emails) // how many emails arrived?join(names, ", ") // ["Ana","Luis"] → "Ana, Luis"first(rows)arr("pending", "in review")| Function | Signature | Description |
|---|---|---|
parseJson(s) | (string) → object | Turns JSON text into an object you read with brackets |
toJson(v) | (any) → string | Turns a value into JSON text |
toJsonPretty(v) | (any) → string | The same, indented |
jget(o, key) / jget(o, i) | (any, string|int) → any | Value of a key or a position |
jpath(o, path) | (any, string) → any | Value at a path |
asDict(o) / asList(o) | (any) → Dictionary / List | Treats 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)DataTable and rows
Section titled “DataTable and rows”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).
| Function | Signature | Description |
|---|---|---|
rowCount(dt) | (DataTable) → int | Number of rows |
colCount(dt) | (DataTable) → int | Number 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) → string | Safe text: "" if the cell is null |
num(v) | (any) → double | Safe number: 0 if the cell is null or empty; reads text with a decimal comma or point (see below) |
num(v, culture) | (any, string) → double | Reads the text with that culture’s separators: "es-CO", "en-US", "invariant" |
eq(a, b) | (any, any) → bool | Safe equality between cells (handles nulls) |
rowCount(table)str(row["Customer"]) // text of a cell, no errors if emptynum(row["Total"]) > 1000selectRows(table, r => str(r["Status"]) == "Pending")How num reads a number written as text
Section titled “How num reads a number written as text”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:
| Text | num(...) | Why |
|---|---|---|
"1500,5" | 1500.5 | A single separator not followed by three digits: it is the decimal |
"1500.5" | 1500.5 | Same, with a point |
"1.500,5" | 1500.5 | Both a point and a comma: the last one is the decimal |
"1,500.5" | 1500.5 | Same, the other way round |
"1.500.000" | 1500000 | The same separator repeated can only be thousands |
"1 500,5" | 1500.5 | Spaces are always thousands |
"1.500,50-" | -1500.5 | Trailing sign, as in some accounting reports |
"1.500" | error | Ambiguous: 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.
Files and paths
Section titled “Files and paths”| Function | Signature | Description |
|---|---|---|
fileExists(path) | (string) → bool | true if the file exists |
dirExists(path) | (string) → bool | true if the folder exists |
createDir(path) | (string) → string | Creates the folder and returns its full path |
pathJoin(a, b) | (string, string) → string | Joins two path parts |
fileName(path) | (string) → string | File name of a path |
dirName(path) | (string) → string | Containing folder of a path |
asset(rel) | (string) → string | Absolute path of a file inside your project |
pathJoin(outputFolder, "report.xlsx")asset("templates/invoice.docx") // path to a file included in the projectfileName(fullPath) // "C:\x\data.csv" → "data.csv"Environment variables and credentials
Section titled “Environment variables and credentials”| Function | Signature | Description |
|---|---|---|
env(name) | (string) → string? | Reads a system environment variable |
credential(name) | (string) → Credential | Gets a credential from Zoan Cloud or from the Windows Credential Manager |
env("USERNAME")credential("smtp")["password"] // accesses a field of the credentialenv() returns nothing for variables that start with ZOAN_: that way a playbook can never read the platform’s internal data.
Regular expressions (regex)
Section titled “Regular expressions (regex)”To extract or validate unstructured data (an invoice number inside a text, an email, a date). They use the .NET dialect.
| Function | Signature | Description |
|---|---|---|
regexMatch(text, pattern) | (string, string) → bool | true if the pattern appears in the text |
regexFind(text, pattern) | (string, string) → string | First match, or "" |
regexFindAll(text, pattern) | (string, string) → List | Every match |
regexExtract(text, pattern) | (string, string) → List | The captured groups of the first match |
regexGroup(text, pattern, name) | (string, string, string) → string | A named capture group |
regexReplace(text, pattern, repl) | (string, string, string) → string | Replaces every match (supports $1, ${name}) |
regexFind(emailText, @"\d{4}-\d{2}-\d{2}") // first yyyy-mm-dd dateregexMatch(email, @"^[^@]+@[^@]+\.[^@]+$") // is it a valid email?regexGroup(text, @"(?<invoice>INV-\d+)", "invoice")Working with lists and tables: LINQ
Section titled “Working with lists and tables: LINQ”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 listnumbers.Where(n => n > 100).ToList()
// Sort and take the first onecustomers.OrderBy(c => c.Balance).First()
// Filter table rows and count themasRows(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.
Autocomplete, large editor and validation
Section titled “Autocomplete, large editor and validation”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,TaborEnteraccept,Esccloses it andCtrl+Spaceopens 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+Vdeclares 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 and warnings
Section titled “Errors and warnings”- 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.
Common mistakes
Section titled “Common mistakes”| Symptom | Cause | Solution |
|---|---|---|
| ”The name ‘Hello’ does not exist in the current context” | You typed fixed text without quotes, or the variable is not declared | Put it in quotes: "Hello" |
Upper(...) / FormatDate(...) doesn’t exist | You are using names with an initial capital | Functions are lowercase: upper, and for date formatting use date.ToString("...") |
| A text comparison is always false | You compared DataTable cells with == | Use str(cell) == "value" or eq(a, b) |
| A decimal is read wrong | Data with a decimal comma | Replace the comma with a dot before parseDouble |
\n shows up literally in the text | The backslash is taken literally | Use 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 text | Close the text; for a path ending in a backslash write @"C:\folder\" or use pathJoin(folder, name) |
| Incompatible type | The expression returns a different type from what the field expects | Convert the value (toString, toInt…) or check the variable’s type |
| The key “x” does not exist in the dictionary | A typed dictionary does not have that key | If it may be missing, use jget(value, "x"), which returns null |
Next steps
Section titled “Next steps”- Variables: declare and capture data between activities.
- Data types: what properties and methods each type has.
- Debugging: try your expressions by running the playbook.