Nodes
Configure and test the nodes in a pipeline, set how each one handles errors, and write the expressions they use for dynamic values.
- Test modes
- Sandbox · Live
- On Error
- Stop Pipeline · Continue Execution
- Expressions
- Wrapped in {{ }}
Nodes are the fundamental building blocks of MaestroHub Pipelines. Each node represents a specific action, logic, or integration point in your workflow. By connecting nodes together, you create sophisticated automation without writing code.
The node library is organized into categories based on functionality and continues to grow with new integrations and capabilities added regularly.


Modbus Read


MSSQL Query


REST Request


UNS Publish


Merge


JavaScript
Node Configuration & Testing
When you click a node on the canvas or select Test step from the node's context menu, the Test Node panel opens. This panel has a three-column layout: INPUT, CONFIGURATION, and OUTPUT.


Test Node panel: upstream data on the left, node parameters in the center, and execution results on the right.
INPUT
The left column shows all data available to the node from upstream sources:
- Triggers (
$trigger): the trigger node that started the pipeline. You can edit trigger data manually via a JSON editor or run the trigger to populate it. - Upstream Nodes (
$node["Name"]): grouped by execution depth (direct parents first, then ancestors). Each node's output is displayed as an expandable JSON tree.
If an upstream node has no output yet, a Run button appears. Click it and the system executes all required dependencies in the correct order automatically. Drag any field from the JSON tree directly into an expression input in the CONFIGURATION column.
CONFIGURATION
The center column contains two tabs:
Parameters: node-specific configuration fields that vary by node type (code editor for JavaScript, field mappings for Set, query builder for SQL, etc.).
Settings: shared execution settings that apply to all node types (see Error Handling below).
OUTPUT
The right column shows results after running or testing the node:
- Output tab: the node's output data with syntax-highlighted JSON. Green header for success, red for failure with the error message.
- Input Used tab: which upstream data was actually consumed during execution, shown as variable badges (
$trigger,$node["Set"], etc.) with expandable JSON trees.
Sandbox vs Live
The Test Node button in the top-right corner runs the node. Next to it, a Sandbox | Live control decides how runs from this panel execute. It applies to every run started here: Test Node, each upstream source's Run, and Run all.
- Sandbox: never touches real systems. Nodes that talk to an external system (triggers, connector nodes, and UNS Publish) return a stub result instead of reading or writing.
- Live: performs real I/O against the connected systems.
The control appears only when the node, or a node feeding it, talks to an external system. Nodes such as JavaScript or Set run the same way in both modes. A node that talks to an external system opens in Sandbox; other nodes open in Live.
Error Handling
The Settings tab on every node provides per-node error handling configuration. These settings override the pipeline-level defaults when specified.
| Setting | Options | Description |
|---|---|---|
| Timeout | 1-3600 seconds | Maximum execution time for this node. Leave empty to use the pipeline default. |
| Retry on Timeout | Pipeline Default / Enabled / Disabled | Whether to retry when the node times out. |
| Retry on Fail | Pipeline Default / Enabled / Disabled | Whether to retry when the node fails. Enabling this reveals advanced retry settings. |
| On Error | Pipeline Default / Stop Pipeline / Continue Execution | What happens when the node fails after all retries are exhausted. See Run status and On Error. |
Run status and On Error
On Error decides how the whole run ends, not only what happens at the failing node:
| On Error | The failing node | The run is recorded as |
|---|---|---|
| Stop Pipeline | Marked Failed with its error. No node that depends on it runs: each shows as Skipped, naming the node that failed. Branches that do not depend on it run to the end. | Failed: the run's error is the first failed node's error. |
| Continue Execution | Marked Failed with its error. Downstream nodes still run. | Completed with Errors: the run finished, but at least one node failed. |
| Pipeline Default | Uses the pipeline's own On Error setting (Edit Pipeline → Error Handling; see Error Handling on the Pipelines page). | Per the pipeline setting. |
With Stop Pipeline, a node that combines a failed branch with a healthy one (a Merge, or any node with two inputs) is skipped, because one of its inputs never arrives. A pipeline that reads several devices in separate branches therefore keeps publishing the healthy devices while one is down; the run is still recorded as Failed, so the failure stays visible. An operator's Stop and the pipeline timeout still end every branch.
A run is recorded as Completed only when every node succeeded. Completed with Errors means the run finished, but a step failed for some of its data and the pipeline carried on (for example, a few For-Each items failed). It is neither a success nor a failure, and the platform treats it as its own outcome:
- It appears in Execution History with its own badge, and can be filtered on its own or together with failed runs.
- Pipeline health, the pipelines list, the metrics panel and the Overview count it apart from clean and failed runs: a pipeline whose every run loses a few items reads 100% with errors in amber, not 0% in red.
- It is still not a clean run: it keeps the pipeline on the Overview's failing-pipelines list (a run in a row that failed or completed with errors extends the streak; only a clean run clears it), and it raises the Overview's Investigate failed runs card like a failure does.
- It can be retried like a failed run.
- A Pipeline Event Trigger sees it as
completed_with_errors, notcompleted. Watch that status (orany) if a chained pipeline should also run after a run that completed with errors.
Advanced Retry Settings
When Retry on Fail is enabled, additional settings become available:
| Setting | Range | Description |
|---|---|---|
| Max Attempts | 1-10 | Total number of execution attempts. |
| Initial Delay | 100-30,000 ms | Wait time before the first retry. |
| Max Delay | 1,000-300,000 ms | Maximum wait between retries. |
| Multiplier | 1.0-5.0 | Exponential backoff factor. Each retry waits current delay x multiplier. |
| Jitter Factor | 0-0.5 | Random variation added to retry delays to prevent thundering herd. |
Expressions
Nodes use a built-in expression engine for dynamic values: referencing upstream data, transforming fields, filtering arrays, and building strings on the fly. Expressions appear in most node configuration fields (topics, queries, field mappings, conditions, etc.).
Expression Syntax
Expressions are wrapped in {{ }} delimiters. There are three ways to write them:
Single expression: returns the native type
When the entire field value is a single {{ }} block, the result keeps its native type (number, boolean, object, array). This is what you'll use most often.
{{ $node["API"].result }} → returns the array/object as-is
{{ $input[0].result.price * $input[0].result.quantity }} → returns a number
{{ $input[0].result.score > 50 }} → returns a boolean
Template interpolation: always returns a string
When you mix literal text with one or more {{ }} blocks, each expression is evaluated and inserted into the text. The result is always a string.
Hello, {{ $input[0].result.name }}!
enterprise/{{ $item.site }}/{{ $item.area }}/temperature
Total: ${{ $input[0].result.price * $input[0].result.quantity }}
An object or array inserted into text is written as compact JSON, so Reading: {{ $input[0].result }} gives Reading: {"temp":21.5,"unit":"C"}.
Plain text: no evaluation
If there are no {{ }} delimiters, the value is used as a literal string with no evaluation.
Hello world → literal string "Hello world"
enterprise/site1/area1/temperature → literal string, no expressions
The one rule is: a field is plain text, or it has {{ }}. Two cases are calculated without the braces, so pipelines written before the rule keep working:
- A value that starts with
$, such as$input[0].result.rpm / 60, is calculated as if it were wrapped in{{ }}. - A Condition or Switch expression is always calculated, with or without the braces.
Write the braces anyway. Text that mentions a variable later on, such as Hz: $input[0].result.rpm / 60, is saved as text, as typed: write Hz: {{ $input[0].result.rpm / 60 }} to calculate it.
What the editor tells you
Every expression field says which of the two it holds. Its label reads expression when the value will be calculated and supports expressions when it is text, and a calculated value shows its result from the last run underneath.
- A value calculated without braces offers Add
{{ }}, which writes the same calculation in its usual form. - Text that mentions a variable says it is saved as text and offers Calculate it (the text from the first variable on goes inside
{{ }}) or Keep as text.


A field saved as text says so; an expression shows its result
Settings that name a field to read, such as the Aggregator's value field, the Buffer's item and size fields and the Serializer's input field, take a plain path (result.items) or an expression in {{ }}. There, text without braces is a path.
You can drag fields from the INPUT panel directly into expression fields. The editor automatically wraps the reference in {{ }} for you.
One language for calculations
Pipeline fields and the UNS's derived series all calculate in this same expression language: the operators, functions and number rules on this page. Division always gives a decimal: {{ 310 / 100 }} is 3.1. In the UNS you write the expression without {{ }}.
Three other places take something else, each for its own job:
| Where | What you write |
|---|---|
| The JavaScript node | JavaScript code, with the same $input, $node and $trigger variables. |
| An access rule's condition | A CEL expression over the request, such as request.time_of_day < 9. |
| A connector function definition | ((name)) slots, filled with the parameters a pipeline passes. A slot holds a name, never a calculation: compute the value in the pipeline with {{ }}. |
Expression Variables
Variables provide access to runtime context. All variables are prefixed with $.
| Variable | Type | Description | Example |
|---|---|---|---|
$node["Name"] | any | Output from any upstream node, by display name. | {{ $node["API"].result.rows }} |
$trigger | object | The trigger data that started the pipeline. | {{ $trigger.result.message }} |
$input | any | Resolved output from the immediate predecessor node(s). | {{ $input[0].result.value }} |
$execution | object | Metadata about the current execution. | {{ $execution.id }} |
$item | any | Current item in a For-Each loop (innermost loop). | {{ $item.id }} |
$index | number | Current iteration index (0-based) in a For-Each loop (innermost loop). | {{ $index }} |
$node Access Patterns
Access upstream node outputs using the node's display name in bracket notation:
{{ $node["Set"].result.firstName }}
{{ $node["HTTP Request"].result.body.data[0].id }}
{{ $node["Merge"]["Set"].field }}
Keys with special characters use bracket notation automatically:
{{ $node["Set"]["field-name"] }}
{{ $node["Data"][0].value }}
$execution Properties
| Property | Type | Description |
|---|---|---|
$execution.id | string | Unique execution identifier. |
$execution.startedAt | number | Start time as Unix timestamp (ms). |
$execution.pipelineId | string | Pipeline ID. |
$execution.pipelineName | string | Pipeline name. |
$execution.workspaceId | string | Workspace ID. |
$execution.triggerType | string | Trigger type (manual, webhook, cron, etc.). |
Named Loop Variables
When a For-Each node defines an Item Variable Name (e.g., order), additional named variables become available inside the loop body:
| Variable | Description |
|---|---|
$order | Alias for the current item (same as $item). |
$orderIndex | Alias for the current index (same as $index). |
Named variables improve readability and remain accessible by name. See the For-Each Loop documentation for details.
Operators
Standard operators are available inside expressions.
Arithmetic
| Operator | Example | Description |
|---|---|---|
+ | {{ $input[0].result.a + $input[0].result.b }} | Addition (also concatenates strings) |
- | {{ $input[0].result.a - $input[0].result.b }} | Subtraction |
* | {{ $input[0].result.price * $input[0].result.qty }} | Multiplication |
/ | {{ $input[0].result.total / $input[0].result.count }} | Division |
% | {{ $input[0].result.value % 3 }} | Modulo |
** | {{ 2 ** 10 }} | Exponentiation |
Comparison
| Operator | Example |
|---|---|
== | {{ $input[0].result.status == "active" }} |
!= | {{ $input[0].result.type != "error" }} |
> >= < <= | {{ $input[0].result.age >= 18 }} |
=== and !== are silently converted to == and !=. Both forms work identically.
Logical
| Operator | Example |
|---|---|
&& | {{ $input[0].result.isActive && $input[0].result.isVerified }} |
|| | {{ $input[0].result.isAdmin || $input[0].result.isModerator }} |
! | {{ !$input[0].result.isDisabled }} |
Ternary
{{ $input[0].result.score > 50 ? "pass" : "fail" }}
{{ $input[0].result.score >= 90 ? "A" : ($input[0].result.score >= 80 ? "B" : "C") }}
Nil Coalescing (??)
Returns the left side if it is not nil; otherwise returns the right side.
{{ $input[0].result.nickname ?? "No nickname" }}
{{ $input[0].result.config.timeout ?? 30 }}
Membership (in)
{{ "admin" in $input[0].result.roles }}
Expression Functions
The expression engine provides a library of built-in functions for transforming data. Functions can be used inside {{ }} blocks.
String Functions
| Function | Description | Example |
|---|---|---|
upper(s) | Converts to uppercase. | {{ upper($item.name) }} → "ALICE" |
lower(s) | Converts to lowercase. | {{ lower($item.status) }} → "active" |
trim(s) | Removes leading/trailing whitespace. | {{ trim($input[0].result.name) }} |
trimLeft(s, chars) | Removes specified characters from the left. | {{ trimLeft($input[0].result.code, "0") }} |
trimRight(s, chars) | Removes specified characters from the right. | {{ trimRight($input[0].result.path, "/") }} |
split(s, sep) | Splits a string into an array. | {{ split("a,b,c", ",") }} → ["a","b","c"] |
join(arr, sep) | Joins array elements into a string. | {{ join($input[0].result.tags, ", ") }} → "tag1, tag2" |
replace(s, old, new) | Replaces the first occurrence. | {{ replace($input[0].result.path, "/old", "/new") }} |
replaceAll(s, old, new) | Replaces all occurrences. | {{ replaceAll($input[0].result.text, " ", "_") }} |
substring(s, start, end) | Extracts a substring [start, end). | {{ substring($input[0].result.code, 0, 3) }} → "ABC" |
startsWith(s, prefix) | Returns true if the string starts with the prefix. | {{ startsWith($input[0].result.topic, "enterprise") }} |
endsWith(s, suffix) | Returns true if the string ends with the suffix. | {{ endsWith($input[0].result.file, ".csv") }} |
padLeft(s, len, pad) | Pads from the left to the target length. | {{ padLeft($input[0].result.id, 5, "0") }} → "00042" |
padRight(s, len, pad) | Pads from the right to the target length. | {{ padRight($input[0].result.name, 10, ".") }} |
contains(s, substr) | Returns true if the string contains the substring. | {{ contains($input[0].result.msg, "error") }} |
Math Functions
| Function | Description | Example |
|---|---|---|
abs(n) | Absolute value. | {{ abs($input[0].result.delta) }} |
ceil(n) | Rounds up to nearest integer. | {{ ceil(4.2) }} → 5 |
floor(n) | Rounds down to nearest integer. | {{ floor(4.8) }} → 4 |
round(n, decimals) | Rounds to N decimal places. | {{ round($input[0].result.price, 2) }} → 9.43 |
min(a, b, ...) | Returns the smallest value. Also accepts an array. | {{ min($input[0].result.a, $input[0].result.b) }} |
max(a, b, ...) | Returns the largest value. Also accepts an array. | {{ max($input[0].result.readings) }} |
sum(arr) | Sums all elements in an array. | {{ sum($input[0].result.scores) }} |
avg(arr) | Average of all elements in an array. | {{ avg($input[0].result.temperatures) }} |
pow(base, exp) | Exponentiation. | {{ pow(2, 10) }} → 1024 |
sqrt(n) | Square root. | {{ sqrt(144) }} → 12 |
mod(a, b) | Integer modulo. | {{ mod(17, 5) }} → 2 |
Array Functions
| Function | Description | Example |
|---|---|---|
first(arr) | Returns the first element. | {{ first($input[0].result) }} |
last(arr) | Returns the last element. | {{ last($input[0].result) }} |
nth(arr, n) | Returns element at index N (safe: returns nil if out of bounds). | {{ nth($input[0].result, 2) }} |
len(v) | Length of an array, object, or string. | {{ len($input[0].result) }} |
pluck(arr, field) | Extracts a single field from each object. | {{ pluck($input[0].result.users, "name") }} → ["Alice","Bob"] |
unique(arr) | Removes duplicate values. | {{ unique($input[0].result.tags) }} |
flatten(arr) | Flattens one level of nesting. | {{ flatten($input[0].result.nested) }} |
sortAsc(arr) | Sorts numerically, ascending. | {{ sortAsc($input[0].result.scores) }} |
sortDesc(arr) | Sorts numerically, descending. | {{ sortDesc($input[0].result.scores) }} |
reverse(arr) | Reverses the array. | {{ reverse($input[0].result) }} |
compact(arr) | Removes nil, empty strings, empty arrays, and empty objects. | {{ compact($input.values) }} |
slice(arr, start, end) | Returns a sub-array [start, end). | {{ slice($input[0].result, 0, 5) }} |
includes(arr, val) | Returns true if the array contains the value. | {{ includes($input[0].result.roles, "admin") }} |
indexOf(arr, val) | Index of first occurrence (-1 if not found). | {{ indexOf($input[0].result, "target") }} |
Array Higher-Order Functions
These functions accept a predicate expression using # to reference the current element. See The # Closure Variable below.
| Function | Description | Example |
|---|---|---|
filter(arr, predicate) | Returns elements matching the predicate. | {{ filter($input[0].result, #.status == "active") }} |
map(arr, transform) | Transforms each element. | {{ map($input[0].result.prices, # * 1.1) }} |
find(arr, predicate) | Returns the first matching element. | {{ find($input[0].result.users, #.id == 42) }} |
count(arr, predicate) | Counts matching elements. | {{ count($input[0].result, #.success == true) }} |
all(arr, predicate) | Returns true if all elements match. | {{ all($input[0].result.readings, # > 0) }} |
any(arr, predicate) | Returns true if any element matches. | {{ any($input[0].result, #.error != "") }} |
none(arr, predicate) | Returns true if no elements match. | {{ none($input[0].result, #.failed == true) }} |
one(arr, predicate) | Returns true if exactly one element matches. | {{ one($input[0].result.alerts, #.severity == "critical") }} |
reduce(arr, accumulator, initial) | Reduces the array to a single value. # is the current element, #acc is the accumulator. | {{ reduce($input[0].result, #acc + #.price, 0) }} |
Object Functions
| Function | Description | Example |
|---|---|---|
keys(obj) | Returns the object's keys as a sorted array. | {{ keys($item) }} → ["name","value"] |
values(obj) | Returns the object's values as an array. | {{ values($item) }} → [{...}] |
has(obj, key) | Returns true if the key exists. | {{ has($input, "email") }} |
get(obj, path, default) | Gets a nested value by dot-path, with a fallback. | {{ get($input, "user.address.city", "Unknown") }} |
merge(a, b, ...) | Shallow-merges objects. Later objects override earlier keys. | {{ merge($input[0].result.defaults, $input[0].result.overrides) }} |
pick(obj, key1, key2, ...) | Returns a new object with only the specified keys. | {{ pick($input, "name", "email") }} |
omit(obj, key1, key2, ...) | Returns a new object without the specified keys. | {{ omit($input, "password", "secret") }} |
Some nodes (e.g., industrial connectors) produce arrays where each item has a dynamic key rather than a fixed property name:
[{"Energy_KWh": {"value": 124299}}, {"Coolant_Temp": {"value": 218}}]
Use keys() and values() to extract the dynamic key and its value:
{{ keys($item)[0] }} → "Energy_KWh"
{{ values($item)[0].value }} → 124299
See For-Each Loop: Example 5 for a full walkthrough.
Type Functions
| Function | Description | Example |
|---|---|---|
toString(v) | Converts to string. An object or array becomes compact JSON, a time becomes ISO 8601 in UTC. | {{ toString(42) }} → "42"; {{ toString({"a": 1}) }} → {"a":1} |
toJSON(v) | Encodes as compact JSON. Same as toString for objects and arrays; a string comes back quoted and escaped, ready to place inside a JSON body. | {"name": {{ toJSON($input[0].result.name) }}} |
toNumber(v) | Converts to number. | {{ toNumber("3.14") }} → 3.14 |
toInt(v) | Converts to integer (truncates decimals). | {{ toInt(3.99) }} → 3 |
toBool(v) | Converts to boolean. "", "false", "0", 0, nil → false. | {{ toBool($input[0].result.flag) }} |
typeOf(v) | Returns the type name: "string", "number", "boolean", "array", "object", or "nil". | {{ typeOf($input[0].result.value) }} |
isNil(v) | Returns true if the value is nil. | {{ isNil($input[0].result.optional) }} |
isEmpty(v) | Returns true if nil, "", [], or {}. | {{ isEmpty($input[0].result.list) }} |
isArray(v) | Returns true if the value is an array. | {{ isArray($input[0].result) }} |
isObject(v) | Returns true if the value is an object. | {{ isObject($input[0].result) }} |
isNumber(v) | Returns true if the value is numeric. | {{ isNumber($input[0].result.value) }} |
isString(v) | Returns true if the value is a string. | {{ isString($input[0].result.name) }} |
Utility Functions
| Function | Description | Example |
|---|---|---|
coalesce(a, b, ...) | Returns the first non-nil, non-empty value. | {{ coalesce($input[0].result.nickname, $input[0].result.name, "Anonymous") }} |
defaultTo(v, fallback) | Returns v unless it is nil or empty, then returns fallback. | {{ defaultTo($input[0].result.timeout, 30) }} |
ternary(cond, ifTrue, ifFalse) | If cond is true, returns ifTrue; otherwise ifFalse. | {{ ternary($input[0].result.active, "ON", "OFF") }} |
The # Closure Variable
The # symbol is a special variable used inside array higher-order functions like filter, map, find, all, any, count, none, one, and reduce. It represents the current element being processed.
With simple arrays
When the array contains plain values (numbers, strings), # is the value itself:
{{ filter([1, 2, 3, 4, 5], # > 3) }} → [4, 5]
{{ map([1, 2, 3], # * 2) }} → [2, 4, 6]
{{ all([10, 20, 30], # > 5) }} → true
With arrays of objects
When the array contains objects, use dot notation on # to access fields:
{{ filter($input[0].result.users, #.status == "active") }}
{{ find($input[0].result, #.id == 42) }}
{{ map($input[0].result.orders, #.total) }}
{{ count($input[0].result.readings, #.success == true) }}
With reduce
In reduce, # is the current element and #acc is the running accumulator:
{{ reduce($input[0].result, #acc + #.price, 0) }} → sum of all prices
The third argument (0) is the initial accumulator value.
Expression Error Handling
| Scenario | Behavior |
|---|---|
Missing top-level field ($input[0].result.missing) | Returns nil with no error. |
Nil input ($input when input is nil) | Returns nil with no error. |
Deeply nested nil access ($input[0].result.a.b.c where a is nil) | Returns an error, because accessing a field on nil is not allowed. Use get($input, "a.b.c", defaultValue) for safe deep access. |
Array out of bounds ($input[0].result[10] on a 2-item array) | Returns an error. Use nth($input[0].result, 10) for safe access (returns nil). |
Nil in template interpolation (Hello, {{ $input[0].result.name }} where name is nil) | Renders nil as empty string: "Hello, ". |
Plain text without {{ }} | Returned as-is, no evaluation. |
Use get(), nth(), ??, and defaultTo() to avoid errors when data may be missing:
{{ get($input, "user.address.city", "Unknown") }}
{{ nth($input[0].result, 5) ?? "fallback" }}
{{ defaultTo($input[0].result.name, "Anonymous") }}
{{ $input[0].result.optional ?? "default" }}
Popular Nodes
Explore commonly used nodes to understand how they work in your pipelines:
- Schedule Trigger: automate workflows with time-based triggers using cron expressions
- Condition: branch your workflow based on data conditions and business rules
- Set: transform and manipulate data fields as they flow through your pipeline
- JavaScript: execute custom logic for complex business requirements