Function Node
The Function Node allows execution of arbitrary, user-defined JavaScript against a workflow payload.
Configuration
The Function Node configuration takes two inputs.
Function Scope
Optionally, you may provide a payload path to serve as the value of the payload
variable in the function script. If a path is not provided, the entire workflow payload will act as the payload
variable value. The maximum amount of data that can be provided to a Function Node is 5MB. By specifying a path, you can reduce the size of the data to only that which you need, leading to better workflow performance and less risk of exceeding the 5MB limit. In the screenshot above, the Function Node is scoped to the data
path on the workflow’s payload.
For Edge Workflows, the ability to provide a Function Scope Path
is only available for the Gateway Edge Agent v1.30.0 and higher.
Code
The provided code must be valid JavaScript and the web interface provides a code editor with syntax highlighting. ES5 and ES6 syntax are both valid. Other ECMAScript specifications are not supported.
Example: Calculating an Average
if(payload.values?.length > 0) {
const total = payload.values.reduce((acc, val) => {
return acc + val;
});
payload.average = total / payload.values.length;
}
The example code above is calculating the average across values in an array. The result is being placed at payload.average
. The payload
variable points to whatever was passed in as the Function Scope. If no function scope was provided, payload
points to the entire workflow payload object. In this example, the function scope was set to data
. This means that when the node completes, the result will placed back on the payload at data.average
(since payload
is scoped to data
).
Considerations
When writing your Function Node script, there are a few points to consider:
Buffers
The Buffer Object is available in the Function Node. The Buffer
class is specifically designed to process raw or binary data.
For example, if your payload contains a hex-encoded string that represents the float 3.14159
, it can be converted back to the float using the following code:
var hex = payload.data.hex;
var b = Buffer.from(hex, 'hex');
var value = b.readFloatLE(0);
payload.data.value = value; // 3.14159
Console Output
Several console methods are available and can be used to provide feedback on code execution, debug errors, and aid in the development process. When a console function is invoked, an entry is added to the debug log with whatever message you provided.
Example - Console Output
if(payload.values && payload.values.length > 0) {
const total = payload.values.reduce((acc, val) => {
return acc + val;
});
payload.average = total / payload.values.length;
} else {
console.error('Invalid array. Could not calculate average.');
}
Console outputs can be categorized based on their level of importance, with each console method corresponding to a particular debug level in the debug log as described below.
Console Method | Debug Level |
---|---|
console.error |
error |
console.warn |
warning |
console.info |
info |
console.log |
info |
console.trace |
verbose |
console.debug |
verbose |
The above console methods will output to the debug log only if their corresponding debug level is configured to be displayed.
For Edge Workflows, debug levels are only applicable for the Gateway Edge Agent v1.38.0 or higher. In earlier versions, all console methods will have a debug level of verbose
.
Asynchronous Operations
Asynchronous operations (Promises, setTimeout, setInterval, async, and await) are only supported in Edge Workflows and while running the Gateway Edge Agent v1.43.2 or greater. Asynchronous operations are not supported in Application Workflows or Experience Workflows.
Callbacks can be used, but they must be wrapped in a promise.
The example below uses the built-in DNS module to perform a DNS lookup. The lookup function is asynchronous, uses a callback, and must be wrapped in a promise.
const dns = require('node:dns')
const [address, family] = await new Promise((resolve, reject) => {
dns.lookup('losant.com', (err, address, family) => {
if(err) { return reject(err); }
resolve([address, family]);
});
});
payload.working = payload.working || {};
payload.working.dns = { address: address, family: family };
A good use case for asynchronous operations is to connect to controllers not natively supported by the Gateway Edge Agent. When doing this, you must connect, perform your action (read PLC tags, etc.) and disconnect in a single function node. The function node cannot be used for long-running asynchronous processes. For example, you cannot open a persistent connection to a controller or start any kind of server. The function must complete within the workflow’s timeout period. If it does not, the Gateway Edge Agent will automatically end the function.
Modules and Libraries
Edge Workflow functions have access to nearly every built-in Node.js module. Application and Experience workflows only have access to the Buffer module.
Third-party libraries, such as those published on npmjs.com and referenced through require
, are supported for Edge Workflows. Application and Experience workflows do not support any third-party libraries.
There are two primary ways to make third-party modules available to your function node. The first is to mount the library into the container using a Docker Volume (the -v
argument). You can then access this module by its location on disk:
const _ = require('/path/to/my/scripts/lodash.min.js');
Another option is to extend the base Gateway Edge Agent image with your desired modules already installed. The example Dockerfile below creates a new image with two modules installed.
FROM losant/edge-agent:latest
RUN npm install -g numjs
RUN npm install -g danfojs
Once this image is built, you can run it identically to how you’d run the base Gateway Edge Agent. You can access these modules using the following syntax:
const npmjs = require('numjs');
const danfo = require('danfojs-node');
This option is recommended for production use cases. This gives you a self-contained solution that you can push to your own Docker repository. You can then deploy this image as you would any other Docker image. Please keep in mind that you must build the Docker image on the same architecture as your gateway. For example, if you’re deploying to a Raspberry Pi, you must build your Docker image on an ARM64 system.
Performance
For security purposes, each time a Function Node is invoked, a discrete sandbox is created in which the code executes. Setting up this sandbox and copying the payload into it introduces a fixed time cost. This has several implications:
- A Function Node will almost always be slower than a native node when running an equivalent task.
- We advise against using Function Nodes in Loops when possible. If you’re using a loop and a function is required to process each item, consider putting the function node after the loop to process every item at once (instead of inside the loop processing each item individually).
Additionally, there is a limit on the number of concurrent Function Nodes that can be running in an application. This may lead to Function Nodes being queued, resulting in long workflow execution times and potentially workflow timeouts.
For more information on best practices for using Function Nodes, please see our Building Performant Workflows reference guide.
Payload Modification
If you provided a Function Scope Path
, the data at the provided path will be available at the variable payload
; otherwise the payload
variable will be the entire current payload of the workflow. There are two ways a Function Node can modify this value.
Mutating the Payload
In most cases, users opt to modify properties on the incoming payload
. For example, given the following script:
payload.data.newItem = 'Something Special';
payload.data.oldNumber = payload.data.oldNumber + 1;
And an example payload of:
{
...
"data": {
"oldNumber": 5
}
...
}
The payload after the execution of that Function Node would be:
{
...
"data": {
"newItem": "Something Special",
"oldNumber": 6
}
...
}
Returning a New Payload
Alternatively, you may return
any value within the Function Node. If a Function Scope Path
has been provided, this value will serve as the new value at that path; otherwise that value will then serve as the full workflow payload from that point forward.
return { value: 'Total Replacement' }
If the above were the entire contents of a Function Node with no Function Scope Path
provided, the payload after the execution of the Function Node would entirely be replaced and be the object:
{ "value": "Total Replacement" }
Replacing the entire payload is generally not recommended. There are many values that are automatically placed on the payload that are required by other nodes. For example, the Endpoint Trigger places a replyId
on the payload that is required by the Endpoint: Reply Node. It’s very easy to inadvertently remove required fields when replacing the payload, therefore it is not a recommended practice.
Note: If a return statement is used, but no value is returned, the value of the payload
variable at the end of the node’s execution is used as the new payload.
Was this page helpful?
Still looking for help? You can also search the Losant Forums or submit your question there.