Skip to content

Expressions

An expression is a formula that the Designer evaluates at the time of running the playbook, instead of using a fixed value. It is what allows one activity to use the result of another, build dynamic text, make a calculation or make a decision.

If you’re coming from Excel, the idea is the same: a cell can contain the text Total or the formula =A1+A2. In the Designer, a parameter can contain the text Hola or the expression = "Hola " + nombre.

Each parameter field in the properties panel works in one of two modes. What decides the mode is the first character of the field.

ModeWhen is it activatedWhat the Designer does
LiteralThe field does not begin with =Use the text exactly as you wrote it
ExpressionThe field starts with =Evaluate the content as a formula and use the result
Modo literal Modo expresión
───────────── ──────────────
Hola mundo → = "Hola mundo"
https://acme.com → = "https://" + dominio
30000 → = timeoutBase * 2
  • Reuse the result of an activity. An HTTP GET activity stores its response in a variable respuesta; to read the status code you type = respuesta.StatusCode.
  • Build dynamic text. = "Factura " + numero + " — " + cliente.
  • Do calculations. = subtotal * 1.19.
  • Evaluate a condition (in If, While, Retry…). = intentos < 3 &&!exito.

If the value never changes (a fixed URL, a fixed route, a fixed number), use literal mode: it’s simpler and faster to read.

The Zoan Automation expression engine uses C# syntax. You don’t need to know how to program for common formulas, but it’s a good idea to know the rules that most surprise those who come from Excel or other tools.

Write the name of the variable as is:

= nombre
= total
= respuesta

The literal text is always in double quotes "...". Single quotes are reserved for a single character (see padLeft below).

= "Hola mundo"
= "El cliente es " + cliente // concatenación con +
= "Total: " + total + " COP"
= 42
= 3.14 // el separador decimal es el punto
= precio * cantidad
= (subtotal + envio) * 1.19
= total / 12

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

= respuesta.StatusCode // propiedad
= respuesta.Headers["Content-Type"] // valor de un diccionario por clave
= fila["NombreColumna"] // celda de una fila de DataTable
= list[0] // primer elemento de una list (índice 0)
= fecha.Year // propiedad de una fecha
TypeOperatorsExample
Arithmetic+ - * / %= precio * 1.19
Comparison== != < > <= >== total > 1000
Logical&& (y) `\\
Text concatenation+= "Hola " + nombre
Condicional (ternario)condición? valorSi: valorNo= total > 0 ? "Cobrar" : "Gratis"

The Designer comes with a set of functions ready to use. All are written in lower case (upper, not Upper). When you type = and start typing, the Designer’s autocomplete suggests them with its signature.

FunctionFirmaDescription
upper(s)(string) → stringConvert to uppercase
lower(s)(string) → stringConvert to lowercase
trim(s)(string) → stringQuita espacios al inicio y al final
length(s)(string) → intLongitud del texto
replace(s, viejo, nuevo)(string, string, string) → stringReplaces all occurrences
split(s, separador)(string, string) → ListSplit text into a list
contains(s, sub)(string, string) → booltrue si s contiene sub
startsWith(s, prefijo)(string, string) → booltrue if it starts with the prefix
endsWith(s, sufijo)(string, string) → booltrue if it ends with the suffix
substring(s, inicio, largo)(string, int, int) → stringExtract a portion from inicio
padLeft(s, ancho, car)(string, int, char) → stringFill left to ancho
padRight(s, ancho, car)(string, int, char) → stringFill right to ancho
= upper(nombre) // "ana" → "ANA"
= trim(textoConEspacios)
= replace(telefono, "-", "") // quita los guiones
= contains(asunto, "URGENTE")
= padLeft(toString(numero), 6, '0') // 42 → "000042" (nota: '0' es un char)
FunctionFirmaDescription
abs(x)(double) → doubleabsolute value
round(x, d)(double, int) → doubleRedondea a d decimales
floor(x)(double) → doubleRedondea hacia abajo
ceil(x)(double) → doubleRedondea hacia arriba
sqrt(x)(double) → doubleSquare root
pow(x, exp)(double, double) → doublePotencia
min(a, b)(double, double) → doubleThe lesser of two numbers
max(a, b)(double, double) → doubleThe greater of two numbers
mod(a, b)(double, double) → doubleRemainder of division (module)
= round(total * 1.19, 2) // IVA con 2 decimales
= max(stock, 0) // nunca por debajo de 0
FunctionFirmaDescription
toString(v)(any) → stringConvert any value to text
toNumber(v)(any) → doubleConvert to decimal number
toInt(v)(any) → intConvierte a entero
toBoolean(v)(any) → boolConvierte a verdadero/falso
parseDouble(s)(string) → doubleRead a decimal using the period as a separator
parseInt(s)(string) → intRead an integer
FunctionFirmaDescription
isNull(v)(any) → booltrue if value is null
isEmpty(s)(string) → booltrue if null or empty string
isNumber(v)(any) → booltrue if it can be interpreted as a number
= isEmpty(correo) ? "sin correo" : correo
= isNumber(celda) && toNumber(celda) > 0
FunctionFirmaDescription
now()() → DateTimeFecha y hora local actual
today()() → DateTimeToday at 00:00:00 (no time)
utcNow()() → DateTimeFecha y hora actual en UTC

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

= now()
= today().AddDays(-1) // ayer
= now().AddHours(2)
= now().ToString("yyyy-MM-dd") // "2026-06-06"
= now().ToString("dd/MM/yyyy HH:mm")
= fecha.Year
FunctionFirmaDescription
count(e)(IEnumerable) → intNumber of elements
join(e, sep)(IEnumerable, string) → stringJoin the elements in a text
first(e)(IEnumerable) → anyPrimer elemento
last(e)(IEnumerable) → anyLast item
newList()() → ListCreate an empty list
newDict()() → DictionaryCreate an empty dictionary
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, clave, v)(Dict, string, any) → DictAssigns a key and returns the dictionary
dictGet(d, clave)(Dict, string) → anyValue of a key
= count(correos) // ¿cuántos correos llegaron?
= join(nombres, ", ") // ["Ana","Luis"] → "Ana, Luis"
= first(filas)

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

FunctionFirmaDescription
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>Convert table to list (for use with LINQ)
selectRows(dt, pred)(DataTable, Func) → List<DataRow>Filter rows with a condition
str(v)(any) → stringSafe text: "" if cell is null
num(v)(any) → doubleSafe number: 0 if cell is null
eq(a, b)(any, any) → boolSafe equality between cells (handles nulls)
= rowCount(tabla)
= str(row["Cliente"]) // texto de una celda, sin errores si está vacía
= num(fila["Total"]) > 1000
= selectRows(tabla, r => str(r["Estado"]) == "Pendiente")
FunctionFirmaDescription
fileExists(ruta)(string) → booltrue if file exists
dirExists(ruta)(string) → booltrue if folder exists
createDir(ruta)(string) → stringCreate the folder and return its full path
pathJoin(a, b)(string, string) → stringJoin two parts of the route
fileName(ruta)(string) → stringFile name of a path
dirName(ruta)(string) → stringFolder containing a route
asset(rel)(string) → stringAbsolute path of a file within your project
= pathJoin(carpetaSalida, "reporte.xlsx")
= asset("plantillas/factura.docx") // ruta a un archivo incluido en el proyecto
= fileName(rutaCompleta) // "C:\x\datos.csv" → "datos.csv"
FunctionFirmaDescription
env(nombre)(string) → string?Read a system environment variable
credential(nombre)(string) → CredentialGet a credential from Zoan Cloud or Windows Credential Manager
= env("USERNAME")
= credential("smtp")["password"] // accede a un campo de la credencial

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

FunctionFirmaDescription
regexMatch(texto, patrón)(string, string) → booltrue if the pattern appears in the text
regexFind(texto, patrón)(string, string) → stringPrimera coincidencia, o ""
regexFindAll(texto, patrón)(string, string) → List<string>All matches
regexExtract(texto, patrón)(string, string) → List<string>The groups captured from the first match
regexGroup(texto, patrón, nombre)(string, string, string) → stringA named capture group
regexReplace(texto, patrón, repl)(string, string, string) → stringReemplaza cada coincidencia (admite $1, ${nombre})
= regexFind(correoTexto, "\d{4}-\d{2}-\d{2}") // primera fecha aaaa-mm-dd
= regexMatch(email, "^[^@]+@[^@]+\.[^@]+$") // is this a valid email?
= regexGroup(texto, "(?<factura>FAC-\d+)", "factura")

For more powerful operations on lists and tables (filter, sort, transform) you can use LINQ, the.NET query syntax, chaining methods with dot. Remember to end with .ToList() when you need a specific list.

// Filtrar una list
= numeros.Where(n => n > 100).ToList()
// Ordenar y tomar el primero
= clientes.OrderBy(c => c.Saldo).First()
// Filtrar filas de una tabla y contar
= asRows(tabla).Where(r => str(r["Estado"]) == "Pendiente").ToList()
= count(asRows(tabla).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 write an expression, the Designer helps you in two ways:

  • Autocomplete. When you type = and start typing, a list appears with the playbook variables and built-in functions, each with its signature. After a dot (variable.), suggests valid members for that variable’s type.
  • Live validation. The Designer checks the syntax and type of the result. If an expression returns a type other than what the parameter expects (for example, text where a number is required), the field is marked orange as a warning.

If an expression references a name that does not correspond to any variable, at execution it evaluates to null instead of stopping the playbook. This prevents crashes due to iteration variables not yet created, but also means that a typo in a variable name can happen silently. Check the variables panel if an expression returns null unexpectedly.

SymptomCausaSolution
The field saves the text =a+b without calculatingThe = is not the first character, or the field is in literal modeMake sure = is at the start of the field
Upper(...) / FormatDate(...) no existeYou are using capitalized names.Functions are in lower case: upper, and for date format use fecha.ToString("...")
A text comparison always returns falseYou compared DataTable cells with ==Use str(celda) == "valor" or eq(a, b)
A decimal is misinterpretedData with decimal pointReplace the comma with a period before parseDouble
\n appears literally in the textC# escapes don’t work in expressionsGenerate the character from a variable/activity
Expression returns null for no reasonNombre de variable mal escrito (identificador desconocido)Check the exact name in the variables panel
  • Variables — declare and capture data between activities.
  • Data types — what properties and methods each type has.
  • Debugging — test your expressions by running the playbook locally.