Function Node

The Function Node allows execution of arbitrary, user-defined JavaScript against a workflow payload.

Function Node

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.