Understanding Expressions
Expressions are how data moves between nodes in a Vesbite workflow. They let you reference trigger data, access results from previous actions, transform values, and make decisions – all inline, without writing a separate script. This document explains why expressions exist, how they work, and how to use them effectively.
Why Expressions Exist
A workflow is only useful if its nodes can share data. Consider a simple Flow: an RFID reader scans a tag, and you want to create an Airtable record with the tag’s EPC code.
The trigger node has the EPC. The Airtable node needs it. Something has to bridge that gap.
You could hard-code every value, but then every Flow would only work for one specific scenario. Expressions solve this by letting you write dynamic references that are resolved at runtime:
{{ variables.epc }}This single expression means “whatever EPC value the trigger provided for this particular Flow instance.” When 100 tags are scanned, 100 instances run, and each one resolves variables.epc to a different value.
Expressions are what make Flows reusable and data-driven instead of static and single-purpose.
The Vex Language
Vesbite expressions use Vex (Vesbite Expressions), a template language based on Scriban. Scriban is a fast, safe, sandboxed template engine – which makes it well-suited for evaluating user-defined expressions in a multi-tenant cloud environment.
Vex expressions are enclosed in double curly braces:
{{ expression }}You can embed expressions in plain text:
Tag {{ variables.epc }} was scanned at {{ variables.timestamp }}Or use a pure expression when the entire input should resolve to a value:
{{ variables.rssi > -50 }}The curly-brace syntax should feel familiar if you have used any template language (Jinja, Handlebars, Liquid). If you have not, the key idea is simple: everything inside {{ }} is evaluated and replaced with its result.
How Expressions Are Evaluated
Expressions are evaluated at runtime, in the context of the current workflow instance. This has important implications:
Each instance has its own data. When a trigger fires, it populates variables for that specific instance. Expressions in that instance resolve against that data – they cannot see data from other instances.
Evaluation is sequential. Expressions in a node are evaluated when that node executes. This means you can reference outputs from nodes that have already run, but not from nodes that have not executed yet.
Expressions can return any type. A Vex expression can evaluate to a string, number, boolean, object, array, or null. The result type depends on what the expression computes. Vesbite handles type coercion automatically when possible (for example, a number expression used in a string context is converted to its string representation).
Evaluation is sandboxed. Expressions cannot access the file system, make network calls, or affect other instances. They can only read from the workflow context and apply transformations.
The Expression Editor
Vesbite includes a Monaco-based expression editor built into the Flow builder. When you click on an input field that supports expressions, the editor opens with features designed to make expression writing fast and error-free.
Autocomplete
As you type, the editor suggests available variables, outputs, and filters based on the current workflow context. Type variables. and see every variable that triggers and upstream actions have set. Type output(" and see a list of action IDs from the current Flow.
Chip Visualization
Completed expression references are displayed as visual chips – colored badges that show the source and property at a glance. Instead of reading raw {{ output("abc-123").statusCode }}, you see a labeled chip like [HTTP Request > statusCode]. This makes complex expressions easier to read and verify.
Syntax Validation
The editor highlights syntax errors in real time. If you reference a variable that does not exist or use an unsupported filter, you see the error before you save – not when the Flow runs.
Variable Scope
Variables live in the workflow context – a shared key-value store that persists for the lifetime of a Flow instance. All nodes in the instance can read variables, and certain nodes can write them.
How variables are set
Triggers populate the initial set of variables. A Device Event trigger, for example, takes the event payload and makes each field available as a variable:
variables.epc → "E2801160600002095012E177"
variables.rssi → -45
variables.antenna → 1
variables.timestamp → "2026-03-20T14:30:00Z"Set Variable actions let you explicitly create or update a variable:
Name: processedEpc
Value: {{ variables.epc | string.upcase }}After this action runs, {{ variables.processedEpc }} is available to all downstream nodes.
Variable lifetime
Variables exist for the duration of the instance. They are not shared between instances and they are not persisted after the instance completes. If you need to store data permanently, use an integration action to write it to a database or external service.
Output Access
Every action node can produce an output – a structured value (object, array, string, etc.) that represents the result of its operation. You access outputs using the output() function or the activities object.
The output() function
{{ output("node-activity-id") }}
{{ output("node-activity-id").propertyName }}
{{ output("node-activity-id").nested.property }}The argument is the activity ID of the node whose output you want. You can find a node’s activity ID in its properties panel in the Flow builder.
The activities object
An alternative syntax that some users find more readable:
{{ activities["node-activity-id"].output }}
{{ activities["node-activity-id"].output.propertyName }}Both forms are equivalent. Use whichever you prefer.
What outputs look like
Each action type defines its own output structure. Some examples:
| Action | Output |
|---|---|
| HTTP Request | { statusCode: 200, body: { ... }, headers: { ... } } |
| Airtable Create | { id: "rec123", fields: { ... } } |
| Device Action | The device’s response payload |
| Set Variable | The value that was set |
| Branch | The boolean result of the condition |
The expression editor’s autocomplete knows these structures and offers property suggestions after you type output("...")..
Filters
Filters transform data inline using the pipe (|) syntax. They are applied after a value is resolved and before the result is used:
{{ variables.name | string.upcase }}
{{ variables.items | array.size }}
{{ variables.price | math.round 2 }}Vex supports the full set of Scriban built-in filters, organized by category:
String Filters
| Filter | Example | Result |
|---|---|---|
string.upcase | {{ "hello" | string.upcase }} | HELLO |
string.downcase | {{ "HELLO" | string.downcase }} | hello |
string.strip | {{ " hello " | string.strip }} | hello |
string.contains | {{ "hello" | string.contains "ell" }} | true |
string.starts_with | {{ "hello" | string.starts_with "he" }} | true |
string.replace | {{ "hello" | string.replace "l" "r" }} | herro |
string.slice | {{ "hello" | string.slice 0 3 }} | hel |
string.split | {{ "a,b,c" | string.split "," }} | ["a","b","c"] |
Math Filters
| Filter | Example | Result |
|---|---|---|
math.round | {{ 3.14159 | math.round 2 }} | 3.14 |
math.floor | {{ 3.7 | math.floor }} | 3 |
math.ceil | {{ 3.2 | math.ceil }} | 4 |
math.abs | {{ -5 | math.abs }} | 5 |
Array Filters
| Filter | Example | Result |
|---|---|---|
array.size | {{ myArray | array.size }} | Length of array |
array.first | {{ myArray | array.first }} | First element |
array.last | {{ myArray | array.last }} | Last element |
array.join | {{ myArray | array.join ", " }} | Comma-separated string |
array.sort | {{ myArray | array.sort }} | Sorted copy |
array.reverse | {{ myArray | array.reverse }} | Reversed copy |
array.uniq | {{ myArray | array.uniq }} | Deduplicated copy |
Filters can be chained:
{{ variables.name | string.strip | string.upcase }}Control Flow in Expressions
Vex supports conditional logic and loops directly within expressions. While most control flow should be handled by Flow nodes (Branch, ForEach), inline control flow is useful for formatting output or computing derived values.
Conditionals
{{ if variables.rssi > -30 }}Strong{{ else if variables.rssi > -60 }}Medium{{ else }}Weak{{ end }}This evaluates to a string based on the signal strength. Use this pattern when you need a derived value in a single input field without adding a separate Branch node.
Loops
{{ for item in variables.items }}
- {{ item.name }}: {{ item.value }}
{{ end }}Loops iterate over arrays and produce output for each element. They are most useful in text-formatting scenarios (building a message body, generating a report).
Ternary-style expressions
For simple either/or values, you can use a compact conditional:
{{ if variables.success; "OK"; else; "FAIL"; end }}Type System
Vex works with a small, predictable set of types:
| Type | Examples | Notes |
|---|---|---|
| String | "hello", "E280116" | Text values, the most common type |
| Number | 42, 3.14, -17 | Integers and decimals |
| Boolean | true, false | Logical values, used in conditions |
| Object | { "key": "value" } | Structured data with named properties |
| Array | [1, 2, 3] | Ordered collections |
| Null | null | Absence of a value |
Vesbite coerces types automatically when possible. For example, if an action input expects a string but your expression evaluates to a number, the number is converted to its string representation. Similarly, objects and arrays are serialized to JSON strings when used in a string context.
If a type cannot be coerced (for example, a string that does not represent a number being used where a number is required), the expression evaluation fails and the node follows its error handle.
Best Practices
Keep expressions simple
Expressions are best when they do one thing: reference a value, apply a filter, or make a simple comparison. If you find yourself writing multi-line logic in an expression field, consider using a Set Variable action instead.
Instead of this (complex inline expression):
{{ if output("api-call").body.items | array.size > 0; output("api-call").body.items | array.first | string.upcase; else; "NONE"; end }}Do this (Set Variable + simple reference):
- Set Variable node:
firstItem={{ output("api-call").body.items | array.first | string.upcase }} - Branch node:
{{ output("api-call").body.items | array.size > 0 }} - Downstream node references:
{{ variables.firstItem }}
Use Set Variable for reusable values
If you reference the same computed value in multiple nodes, use a Set Variable action to compute it once and store it. This avoids duplicating expressions and makes your Flow easier to read.
Name variables descriptively
Variable names like processedEpc, signalStrength, or recordCount are far more readable than x, temp, or val. Since variables appear in autocomplete and chips, good names make the entire Flow self-documenting.
Test expressions with the Manual trigger
Before publishing a Flow, use the Manual trigger to test it with sample data. The instance log shows the evaluated result of each expression, so you can verify that your references resolve correctly.
Handle missing data
Not every event payload will have every field. Use conditionals or default values to handle cases where a variable might be null:
{{ variables.tid ?? "unknown" }}
{{ if variables.metadata; variables.metadata.location; else; "not set"; end }}Next Steps
- Understanding Workflows – How Flows execute and manage state
- Your First Workflow – Build a complete automation with expressions
- How to Build a Flow – Practical guide to the canvas and node configuration