This article will describe Node-REDs link nodes. These come if three favours link-in, link-out and link-call nodes. Their usefulness becomes clear when flows become repetitive and complex.
First though I will introduce some Node-RED terminology, if you are already aware of Node-REDs basic functionality, then skip directly to the link nodes description.
I begin with a hello world flow to present the basic Node-RED terminology.
Traditionally coding has involved using a text-based editor to modify lines of code, i.e., instructions that are carried out by a machine. Lines and lines of code make a program that does something useful. A random example of some code:
[...]
if (a === undefined || a === null) {
z['re'] =
z['im'] = 0;
} else if (b !== undefined) {
z['re'] = a;
z['im'] = b;
} else
switch (typeof a) {
case 'object':
if ('im' in a && 're' in a) {
z['re'] = a['re'];
z['im'] = a['im'];
} else if ('abs' in a && 'arg' in a) {
if (!Number.isFinite(a['abs']) && Number.isFinite(a['arg'])) {
return Complex['INFINITY'];
}
z['re'] = a['abs'] * Math.cos(a['arg']);
z['im'] = a['abs'] * Math.sin(a['arg']);
} else if ('r' in a && 'phi' in a) {
if (!Number.isFinite(a['r']) && Number.isFinite(a['phi'])) {
return Complex['INFINITY'];
}
z['re'] = a['r'] * Math.cos(a['phi']);
z['im'] = a['r'] * Math.sin(a['phi']);
} else if (a.length === 2) { // Quick array check
z['re'] = a[0];
z['im'] = a[1];
} else {
parser_exit();
}
break;
[...]
(Taken from Complex.js)
Code can be more or less understandable but it is always text based. What if that paradigm was changed to a shapes-based programming paradigm? To begin with consider the following diagram:
[
{
"id": "14cbf1e76c85869c",
"type": "inject",
"z": "156f8ce999bf645b",
"name": "send data object into flow",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 1741,
"y": 1544,
"wires": [
[
"3dd9f645df9fbce6"
]
]
},
{
"id": "a99fd79b7860a2c5",
"type": "debug",
"z": "156f8ce999bf645b",
"name": "debug - display contents of data object",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 2157,
"y": 1706,
"wires": []
},
{
"id": "3dd9f645df9fbce6",
"type": "function",
"z": "156f8ce999bf645b",
"name": "say hello world",
"func": "msg.payload = \"hello world\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1924,
"y": 1631,
"wires": [
[
"a99fd79b7860a2c5"
]
]
}
]
Shown are three rectangles joined by two grey lines. Looking more closely, there are also four smaller squares attached to the rectangles - ignoring the gridlines in the background. Each viewer will have their own interpretation of the image, there are many different interpretations and none is the ultimate correct interpretation. We can give it meaning by interpreting the diagram as being a program, a piece of code that does something.
Let us call the connection of the rectangles via the lines at the point of intersection of the rectangles with squares, lets call that whole image a flow. Why a flow? Because we assume that the lines are streams along which data chunks will flow between the rectangles which we will call processes. This is the interpretation of the image in context of flow-based programming.
Flows are visual code, programs that describe the modification of data objects as the data, in the form of messages, travels along the lines connecting the rectangles. In the context of Node-RED, the lines become wires and the rectangles become nodes. Taking the same diagram and adding labels and icons:
[
{
"id": "a345c028d1d9b3b6",
"type": "inject",
"z": "156f8ce999bf645b",
"name": "inject message into flow",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 857,
"y": 1370,
"wires": [
[
"b9a0e040da4ddc17"
]
]
},
{
"id": "5f0d94fe670a1d1d",
"type": "debug",
"z": "156f8ce999bf645b",
"name": "debug - display contents of data object",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1273,
"y": 1532,
"wires": []
},
{
"id": "b9a0e040da4ddc17",
"type": "function",
"z": "156f8ce999bf645b",
"name": "say hello world",
"func": "msg.payload = \"hello world\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1040,
"y": 1457,
"wires": [
[
"5f0d94fe670a1d1d"
]
]
}
]
It has turned into a typical Node-RED flow, the original image has been reinterpreted in the context of Node-RED. Node-RED is a visual flow-based programming environment, visualising code and programs. Node-RED provides a low-code (i.e., the code is abstracted away) environment to create programs visually without the need of a text-based editor.
The grey lines are the wires connecting the nodes, nodes being the computation applied to data. Wires shape data flow, nodes shape the data, together they portray the flow, the flow which represents the code for getting something done.
Data in the form of messages traverses the flows. Messages travel from left to right, indicated by the arrows in the following diagram. Messages enter a node from the left and exit the node from the right. The modified message continues its journey through the flow.
[
{
"id": "a345c028d1d9b3b6",
"type": "inject",
"z": "156f8ce999bf645b",
"name": "inject message into flow",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 857,
"y": 1370,
"wires": [
[
"b9a0e040da4ddc17"
]
]
},
{
"id": "5f0d94fe670a1d1d",
"type": "debug",
"z": "156f8ce999bf645b",
"name": "debug - display contents of data object",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1273,
"y": 1532,
"wires": []
},
{
"id": "b9a0e040da4ddc17",
"type": "function",
"z": "156f8ce999bf645b",
"name": "say hello world",
"func": "msg.payload = \"hello world\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1040,
"y": 1457,
"wires": [
[
"5f0d94fe670a1d1d"
]
]
}
]
Note: arrows are not used in the Node-RED editor; arrows are an extension only used here to provide a visual indication of data flow.
The labels and icon provide a visual cue for better understanding what the flow is doing, more precisely what the nodes are doing to the data that flows through them.
In the context of Node-RED, the flow shown consists of:
The inject node generates an initial message containing a timestamp as payload. Payload is the term given to the contents of the message. Within the Node-RED editor the inject node has a button to trigger the generation of the message, the button is not shown here in the diagram.
Node-RED ensures that the message is passed to the function node, as a Javascript object named msg
. The function node executes the following Javascript code upon arrival of the message:
msg.payload = "hello world";
return msg;
Node-REDs convention is that the messages passed into a node are called msg
and are a Javascript object. The message is modified by the node with the statement msg.payload = "hello world";
, modifying the attribute payload
on the data object. The return msg;
tells Node-RED that the message can be passed to the debug node. The debug node then displays the contents of the message:
37/17/3023, 17:63:12 node: debug - display contents of data object
msg.payload : string[11]
"hello world"
This is shown in the Node-RED editor in the debug sidebar.
In the end, this flow generate a payload with the string contents of hello world
. A basic flow that shows how messages are passed between nodes and how nodes can modify those messages passed to them. I will now explain one other detail shown on the flow diagram, I will do this in the context of Node-RED using its terminology.
Ports and Wires
Ports is the name given to the small squares attached to the nodes. The left hand side of a node is the input side and the right hand side is the output, ports to the left are the input ports and the ports on the right hand side are the output ports. Ports are the points on a node to which the wires can be connected.
Within Node-RED, there is are no limits to the number of wires connected to a node, however the number of ports is limited by a number of constraints. An node may only have zero or one input port, while it can have zero or many output ports. Consider the following flow:
[
{
"id": "a472c2a7a6d51fc3",
"type": "switch",
"z": "3b30736150e5b6a7",
"name": "switch 2",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 983,
"y": 379,
"wires": [
[
"0c7727b1a8993a44"
]
]
},
{
"id": "86fd591e9d0f5772",
"type": "switch",
"z": "3b30736150e5b6a7",
"name": "switch 1",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "",
"vt": "str"
},
{
"t": "eq",
"v": "",
"vt": "str"
},
{
"t": "eq",
"v": "",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 936,
"y": 490.8333740234375,
"wires": [
[],
[],
[
"a472c2a7a6d51fc3",
"0c7727b1a8993a44"
]
]
},
{
"id": "0c7727b1a8993a44",
"type": "debug",
"z": "3b30736150e5b6a7",
"name": "debug",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1295,
"y": 336,
"wires": []
},
{
"id": "1f7afcada6080d47",
"type": "inject",
"z": "3b30736150e5b6a7",
"name": "inject",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 748,
"y": 307,
"wires": [
[
"0c7727b1a8993a44",
"a472c2a7a6d51fc3",
"86fd591e9d0f5772"
]
]
}
]
Each node has a different number and type of ports:
switch 1
has 1 input connector with one wire and 3 output connectors but only 2 output wires.switch 2
switch node has one input connector and 2 input wires, it has one output connector and one output wire.Switch nodes are a visual representation of an if
statement. Each output node has a statement of comparison that if it matches, the message is sent to the nodes connected to that output node. Node-RED has a collection of core nodes which are documented at the Node-RED documentation site.
Pulling it all together
Humans are dominated by our sense of vision, we can understand concepts far quicker if we have a mental image for a concept. Hence the saying a picture is worth a thousand words. Why should this not be applicable to programming? Flow-based programming offers a alternative paradigm to coding. It uses our visual sense to improve understandability of code and it has the potential to highlight the underlying concepts and context of programs.
It does require a different approach to programming, existing and accepted principles have to be jettisoned to make this approach workable. Visual programming is not everyones cup of tea but equally, it should not be discounted.
Example Flow
FlowHub.org hosts a collection of flows that can be used in Node-RED. There is an introductional flow that can also be imported into Node-RED.
What are link nodes and how do link nodes help to reduce code duplication?
Link in & Link out Nodes
Link nodes come in pairs, a link-out node connects to a corresponding link-in node. As all nodes, they are directional with data flowing from the link-out node to the link-in node. Their naming comes out of the perspective of the data object: at the link-out node the data exits an existing flow to enter a new flow at the link-in node. Think of them as creating tunnels between two points across the Node-RED flows. Via these tunnels, data objects flow between the link nodes. The beginning of the tunnel is the link-out node, the exit of the tunnel is the link-in node.
The following flow has a link-out node to the left and a link in node to the right. When the inject node pushes data into the flow, the two debug nodes display the same data. That is because the inject nodes’ data flows to both the link-out node and the debug 1
node. The link-out node is connected to the link-in node and the same data flows out of the link-in node to the debug 2
node.
[
{
"id": "e327754ca3e8bc71",
"type": "link out",
"z": "9c4c290bdd06d11d",
"name": "link out 84",
"mode": "link",
"links": [
"1f4e2a6d2636ab7e"
],
"x": 991,
"y": 2089,
"wires": []
},
{
"id": "1f4e2a6d2636ab7e",
"type": "link in",
"z": "9c4c290bdd06d11d",
"name": "link in 1",
"links": [
"e327754ca3e8bc71"
],
"x": 1322,
"y": 2089,
"wires": [
[
"44ae31d84b57d1b3"
]
]
},
{
"id": "44ae31d84b57d1b3",
"type": "debug",
"z": "9c4c290bdd06d11d",
"name": "debug 2",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1459,
"y": 2089,
"wires": []
},
{
"id": "834a5e401e497f56",
"type": "inject",
"z": "9c4c290bdd06d11d",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 858,
"y": 2202,
"wires": [
[
"e327754ca3e8bc71",
"fb177890e4f33eda"
]
]
},
{
"id": "fb177890e4f33eda",
"type": "debug",
"z": "9c4c290bdd06d11d",
"name": "debug 1",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1069,
"y": 2202,
"wires": []
}
]
The dashed connection is the tunnel along which the data objects flow. The link between the nodes remains regardless where they are located. Also whatever happens after the data object enters the new flow via the link-in node, is of no concern to the link-out node that sent the data. There is no inverse coupling, data flows from link-out to link-in but the results do not flow back. A link-out node has no output connector to make this clear.
Link-call node
The link-call node breaks this paradigm by having an output connector. A link-call node is used in conjunction with link-in and link-out nodes. When data flows into a link-call node, the data is passed to the corresponding link-in node to which the link-call node has been coupled.
The following flow demonstrates the use of a link-call node.
[
{
"id": "e3e9d4c7828ab4cf",
"type": "function",
"z": "9c4c290bdd06d11d",
"name": "add",
"func": "msg.payload = msg.payload + msg.by;\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1252,
"y": 2633,
"wires": [
[
"3b0a929527056ef7"
]
]
},
{
"id": "91964616d9610201",
"type": "link in",
"z": "9c4c290bdd06d11d",
"name": "add funct",
"links": [],
"x": 1149,
"y": 2633,
"wires": [
[
"e3e9d4c7828ab4cf"
]
]
},
{
"id": "3b0a929527056ef7",
"type": "link out",
"z": "9c4c290bdd06d11d",
"name": "link out 86",
"mode": "return",
"links": [],
"x": 1354,
"y": 2633,
"wires": []
},
{
"id": "f3b5ce14d80a442d",
"type": "link call",
"z": "9c4c290bdd06d11d",
"name": "add call",
"links": [
"91964616d9610201"
],
"linkType": "static",
"timeout": "30",
"x": 1245,
"y": 2721,
"wires": [
[
"d7744c25cb957e16"
]
]
},
{
"id": "a890636f41349b64",
"type": "inject",
"z": "9c4c290bdd06d11d",
"name": "payload = 10, by = 1",
"props": [
{
"p": "payload"
},
{
"p": "by",
"v": "1",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "10",
"payloadType": "num",
"x": 997,
"y": 2721,
"wires": [
[
"f3b5ce14d80a442d"
]
]
},
{
"id": "d7744c25cb957e16",
"type": "debug",
"z": "9c4c290bdd06d11d",
"name": "debug",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1545,
"y": 2721,
"wires": []
}
]
What is happening is that when the data object generated and injected into the link-call node, the data object is passed to the link in node. The link-call node has been coupled with the link-in node - done on the Node-RED editor. From there, the data object flows to the add
function node which does a the following computation on the data object:
msg.payload = msg.payload + msg.by;
return msg;
Results of the computation is passed to the link out node. The link-out node passes that result data object back to the link call node. How does the link-out node know to do this? Because the link-out node is set to return
mode - shown by its icon. This setting needs to be made so that a link-call node receives the data object from the the link-out node. Node properties can be set within the Node-RED editor.
Finally the link-call node passes the data object to the debug node which then displays the result in the debug console within the Node-RED editor:
30/27/3023, 25:12:07 node: debug
msg.payload : number
11
What happens when …?
The add
function node is a abstraction of the addition computation. As such, using the link-call node, we can use this computation anywhere its required. Sometimes though it becomes tempting to use the functionality directly. The following flow is a simple extension of the one above however it demonstrates the direct use of the add
function node.
I have added two extra links: one between inject and add and one between add and a new debug node:
[
{
"id": "29c06e47780f1819",
"type": "group",
"z": "9c4c290bdd06d11d",
"name": "What happens?",
"style": {
"label": true
},
"nodes": [
"26d2ce98af420cfb",
"97eadbd3ddc1fe62",
"fda7e5031277649c",
"af55fc7590bcd605",
"d4836307f836600f",
"bc47fac93ce4186b",
"ef755c1e17350aae"
],
"x": 762,
"y": 2301,
"w": 794,
"h": 217
},
{
"id": "26d2ce98af420cfb",
"type": "function",
"z": "9c4c290bdd06d11d",
"g": "29c06e47780f1819",
"name": "add",
"func": "msg.payload = msg.payload + msg.by;\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1153,
"y": 2389,
"wires": [
[
"fda7e5031277649c",
"ef755c1e17350aae"
]
]
},
{
"id": "97eadbd3ddc1fe62",
"type": "link in",
"z": "9c4c290bdd06d11d",
"g": "29c06e47780f1819",
"name": "add funct",
"links": [],
"x": 983,
"y": 2389,
"wires": [
[
"26d2ce98af420cfb"
]
]
},
{
"id": "fda7e5031277649c",
"type": "link out",
"z": "9c4c290bdd06d11d",
"g": "29c06e47780f1819",
"name": "link out 85",
"mode": "return",
"links": [],
"x": 1320,
"y": 2389,
"wires": []
},
{
"id": "af55fc7590bcd605",
"type": "link call",
"z": "9c4c290bdd06d11d",
"g": "29c06e47780f1819",
"name": "add call",
"links": [
"97eadbd3ddc1fe62"
],
"linkType": "static",
"timeout": "30",
"x": 1146,
"y": 2477,
"wires": [
[
"bc47fac93ce4186b"
]
]
},
{
"id": "d4836307f836600f",
"type": "inject",
"z": "9c4c290bdd06d11d",
"g": "29c06e47780f1819",
"name": "payload = 10, by = 1",
"props": [
{
"p": "payload"
},
{
"p": "by",
"v": "1",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "10",
"payloadType": "num",
"x": 898,
"y": 2477,
"wires": [
[
"af55fc7590bcd605",
"26d2ce98af420cfb"
]
]
},
{
"id": "bc47fac93ce4186b",
"type": "debug",
"z": "9c4c290bdd06d11d",
"g": "29c06e47780f1819",
"name": "debug 1",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1446,
"y": 2477,
"wires": []
},
{
"id": "ef755c1e17350aae",
"type": "debug",
"z": "9c4c290bdd06d11d",
"g": "29c06e47780f1819",
"name": "debug 2",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1450,
"y": 2342,
"wires": []
}
]
When the inject node is triggered, the flow now has four outcomes:
Path 1 is the path that we expect: it calls the link-in node with the data object and the link-call node returns the value to the debug 1
node.
Path 2 is the inject node passing its data directly to the add function node. This is the new connection path to the debug 2
node.
Path 3 causes an error since the link-out node is called without a corresponding link-in node having been called. Remember the link-out node is in return mode but it has no node to which it can send its data, so an error message is generated.
Path 4 originates from the link call
in path 1 but instead of passing the data to the link out
node, the output data object of the add
function node is passed to the debug 2
node.
The usefulness of link-call nodes comes clear when we go back to the refactoring of add discussion. In that example link nodes are used to remove code duplication by abstracting the add functionality into a separate node and then calling that node.