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:

  1. 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.

  2. 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.

  3. 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).

  4. 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:

ActionOutput
HTTP Request{ statusCode: 200, body: { ... }, headers: { ... } }
Airtable Create{ id: "rec123", fields: { ... } }
Device ActionThe device’s response payload
Set VariableThe value that was set
BranchThe 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

FilterExampleResult
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

FilterExampleResult
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

FilterExampleResult
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:

TypeExamplesNotes
String"hello", "E280116"Text values, the most common type
Number42, 3.14, -17Integers and decimals
Booleantrue, falseLogical values, used in conditions
Object{ "key": "value" }Structured data with named properties
Array[1, 2, 3]Ordered collections
NullnullAbsence 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):

  1. Set Variable node: firstItem = {{ output("api-call").body.items | array.first | string.upcase }}
  2. Branch node: {{ output("api-call").body.items | array.size > 0 }}
  3. 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