That is a not a problem unique to the link call feature. Junctions are not a thing in older node-red installations. certain RED api calls and properties in a function node are not in older node-red versions...
That is a good observation. One that didn't turn up in the proposal thread. Off the top of my head, I can think of one way - a registration of intent to use call a flow from within the function node - on the setup panel (i.e. declare that it will be called making it referenceable).
Implementing that would mean pulling this feature. Noodling!
I'm not arguing against function nodes, they definitely have their place.
True but generally it's not something that Nick is encouraging - as I understand (breaking backward compatibility).
Ok, then why break the wiring "contract" in the process of solving a state management issue?
I solved my state issues by using function node with two output ports:
[{"id":"3081fbf40e0de137","type":"group","z":"152eea3f3f65686e","name":"buffer tasks","style":{"label":true},"nodes":["baf52fe106736606","535b53921b88f908"],"x":1304.2857666015625,"y":434.8571472167969,"w":452,"h":162.14285278320312},{"id":"baf52fe106736606","type":"function","z":"152eea3f3f65686e","g":"3081fbf40e0de137","name":"queue requests so that gpu/cpu don't get overloaded","func":"if (msg.check_queue) {\n if (flow.get(\"whisper_running\")) {\n node.send([undefined, msg])\n } else {\n let queue = flow.get(\"whisper_queue\")\n\n if (Array.isArray(queue) && queue.length > 0) {\n let newtask = queue.shift()\n flow.set(\"whisper_running\", true)\n flow.set(\"whisper_queue\", queue)\n \n node.send([newtask, msg])\n } else {\n node.send([undefined, undefined])\n }\n }\n} else {\n let checkQueMsg = { \n '_msgid': RED.util.generateId(), \n 'check_queue': true \n }\n\n // these cause issues when objects are serizalised\n // plus they are no longer needed.\n delete msg.res\n delete msg.req\n delete msg.orig_res\n \n let queue = flow.get(\"whisper_queue\") || []\n if ( !msg.check_queue && queue.map(d => d._msgid).indexOf(msg._msgid) < 0 ) {\n queue.push(msg)\n }\n flow.set(\"whisper_queue\", queue)\n node.send([undefined, checkQueMsg])\n}\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1530.2857666015625,"y":475.8571472167969,"wires":[["1567b81a574c33c9"],["535b53921b88f908"]]},{"id":"535b53921b88f908","type":"join","z":"152eea3f3f65686e","g":"3081fbf40e0de137","name":"bunch up messages over 1 second","mode":"custom","build":"merged","property":"check_queue","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":false,"accumulate":false,"timeout":"1","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1533,"y":556,"wires":[["baf52fe106736606"]]}]
this implements a queue for ensuring that only one message is passed down the flow at a time. The loop is a gatekeeper for a queue stored in the flow context.
As long as the flow.get(...) call is synchronous and isn't called multiple times, this buffer flow works fine (well it's been working fine!).
What or who maintains that when the function changes? Or you have to register intent else the linkcall fails with an error "attempting to call non-registered linkcall" - else these two spots will drift apart.
A function node calling to a link out link in set up as returning to calling node, is virtually the same as a link call node, and saves the use of mulitple cluttering nodes.
If the function is named well or a link call is noted in the comments or a flag added , then to me the flow still is visually clear. A link call shows no wiring and i use these rather than subflows, I find it visually clearer to me.
I welcome this functionality as it offers a clear way of reusing functional flows without repetition and multiple cluttering nodes.
I would ask can we add the node.linkcall to JSONata as well.
I think we can live with multple ways of doing things, one size does not fit all.
There is another reason not to upgrade beyond NR 4! Please no, then a link call can be made in a change, switch, write file, read file, ....
Would have to be something "automatic", comments and code drift apart and are maintained differently. I really would like something like the dotted lines:
When I select all, the link nodes are clearly linked: automatically. If this would be the case for the function node automatically then ok, it's then visually clear what is happening.
I see this automatic highlight part of making the function node to a link out/link call node: then it should also be shown as such and not hidden away in some comment or flag.
Thank god the Luddites did not stop the automated looms, or we would still be wearing itchy expensive rags.
Any node which performs this kind of invisible link or subflow call should have an extra "output" marker, centralised on the bottom of the node.
Of course you could not wire up to this marker, but it would form the end point for dotted wire[s] when the node is selected.
You could also perhaps use the oneditsave inspect the function for the node.linkcall signature and use that to change the icon to be the link call one ? So at least it is visible. But yes may be several ways
Steve, I agree with your assessment. Also agree that a visual indicator would be helpful. Dave's suggestion of automating it would seem the best solution. However, in my view, that could be a Node-RED v5.1 solution, no need to pull the PR from v5.
I think that perhaps people need to remember that, while some folk use Node-RED as a visual programming tool, others use it as an easy-to-use, low-code compute platform.
As a platform, I am much less worried about having ALL the logic visually laid out. I very commonly collapse visual flows into a function node to keep the visual complexity to a manageable level.
As such, the ability to trigger a link call from a function node is a nice addition.
-
Not all progress is automatically good, the assumption is that anything that is faster or more expensive must be better isn't always true. Else we might actually be living on the moon or what happened there?
-
That's also probably why refrigerators don't cook eggs, because that's not their job.
So it is with NR function node does it have to bake eggs or go to the moon? Is the overall effect of the change a positive or negative? No one can say that since the confusion factor that (node.linkcall) is trying to solve creates a new source of confusion (hidden data flows). Now adding hidden data flows all over the place, would that improve the clarity of a flow? Is clarity of a flow even an aim of NR? Or is the aim of NR to get to the moon while baking eggs?
Neither. Flow clarity is certainly desirable but it was never the original intent of Node-RED - at least as I see it. That intent was to create a platform that enabled more people to programme a computer to do useful things. As such, it isn't necessarily a mandate that all logic must be visible.
Are we splitting hairs here? How can getting more people involved in programming be combined with confusing flow structures? I would have thought that if more people should be involved then clarity and understanding would be the highest priority? But if you mean more artificial intelligent and people with telepathic powers then yes, then clarity wouldn't be required!
If you want more people involved in programming than look at your smart phone and see how that is designed. Everything that happens there is programming the device and everyone who uses and understands that device via the interface, is a programmer of that device. So in fact, NR should be using that kind of visual interface for programming because, it seems, folks understand that interface.
Either way, the future will be visual and verbal - with or without NR.
Almost certainly already mentioned elsewhere...
The buttons for the various sidebar contents can be dragged and reordered, but it seems not to change the editor configuration at all?
It's even possible to drag the left sidebar icons to the right of all the right sidebar icons.
If moving these around has no effect it should perhaps be disallowed?
The tooltip on the ctrl-p button remains "Toggle palette" even if the palette itself has been dragged to the right hand sidebar.
I was going to mention that the default keyboard shortcuts no longer make sense.
I would suggest something like this:
Toggle left sidebar - ctrl-shift-[
Toggle right sidebar - ctrl-shift-]
Name call all you want, but I completely agree with gregorius.
I would argue REQUIRED, not just helpful.
That's understandable for your uses. However, I am worried about it. Very much so. It is one of the core guiding reasons I use node-red privately and professionally.
Anyway, it is what it is. Everyone has opinions, just because some conflict doesn't make any of them less valid for their frame of reference or use cases.
Can you please give us a concrete example to prove your argument. I want to see the same problem solved with and without link call feature.
Lookup technocrats and how and why the EU was formed. Brussels is a bunch of technocrats that are - by design- there to prevent politicians from starting wars in Europe.
Exactly because individual European politicians could not and would not agree on anything beyond the price of bread, Brussels became the decision making centre of Europe (mainland). Well that's not 100%, really what we have in the EU is a mess of different factions all vying for wealth and control ... pretty much like everywhere else.
Basically AI would be a rehash of the same idea that has already failed failing is great. Yes we do still have wars in Europe and no, Brussels did not prevent poverty or an uneven distribution of wealth from happening.
But the good news is that, in the big scheme of things, it doesn't really matter. 100 years 1000 years or a million years down the road, the universe will still be here, the earth will be here but whether we are here, neither will care.
Here is a real scenario involving a library where no Node-RED node exists, requiring a Function Node.
You are building an AI Agent using the langgraph NPM library. The agent needs to call MCP Tools (or regular langgraph tools) to fetch data from a PLC via OPCUA. You already have a working, visual Node-RED flow that handles the OPCUA communication via a contrib node.
Without linkcall
Because you cannot "await" a flow, you have to break the logic into disconnected pieces and manually manage the plumbing though context or other means.
- Exit Point: Pause the Langgraph execution.
- Save State: Store the active graph instance in
context(keyed by tenant ID because this is multi-tenant). - Save Intent: Store the original
msgand the current "tool call ID" so you know what to do when the data comes back. - Emit: Send a message out of the Function node to the OPCUA flow.
- The Return: The OPCUA flow finishes and hits a new Function node.
- Rehydrate: You must now fetch the graph instance, the original
msg, and the tool state back fromcontext. - Logic Resume: Manually resolve the tool call within the restored graph and proceed.
- I cant be bothered to continue this to the end, its messy and brittle.
Now multiply that by dozens of MCP tools (Read PLC, Search RAG, Send MQTT, etc.).
What was once an ephemeral, single-lifecycle operation now requires complex state management, semaphore interlocking, and cleanup logic.
With linkcall
- Call the flow and await the reply
Done.
// Inside the Langgraph tool definition
const tool = async (args) => {
// Call the visual OPCUA flow as if it were a local JS function
const { payload: plcData } = await node.linkcall('read-opc-flow', { topic: args.tag });
// Return the awaited result & the graph continues
return plcData;
};
I am not saying these things are not possible without linkcall, just that they are much more difficult and nuanced.
I wanted u to share 2 flows.json with and without your feature. Make the minimum example possible that prove your point (removal of spagethi due to excessive wiring). Dont use text, please.
Call the flow and await the reply
[{"id":"38bf73a4a0a6c063","type":"function","z":"a3eacd3cd06ac2b4","name":"function 2","func":"if ( msg.linkcall_done) {\n // do other stuff\n msg.payload = msg.result\n node.send([undefined, msg])\n} else {\n node.send([msg, undefined])\n}","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":878.5,"y":706,"wires":[["e03b1255d5fdbf39"],["33a98a4ed3760c0c"]]},{"id":"f897d52a858b915d","type":"inject","z":"a3eacd3cd06ac2b4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":609,"y":505,"wires":[["38bf73a4a0a6c063"]]},{"id":"e03b1255d5fdbf39","type":"link call","z":"a3eacd3cd06ac2b4","name":"call read-opc-flow","links":["83863fc6805f71cc"],"linkType":"static","timeout":"30","x":1096,"y":605,"wires":[["f5e1883e3923a929"]]},{"id":"33a98a4ed3760c0c","type":"function","z":"a3eacd3cd06ac2b4","name":"do something with the data from the linkcall","func":"\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1208,"y":791,"wires":[[]]},{"id":"f5e1883e3923a929","type":"change","z":"a3eacd3cd06ac2b4","name":"","rules":[{"t":"set","p":"result","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"linkcall_done","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1250,"y":704,"wires":[["38bf73a4a0a6c063"]]},{"id":"83863fc6805f71cc","type":"link in","z":"a3eacd3cd06ac2b4","name":"read-opc-flow","links":[],"x":909,"y":332,"wires":[["185b7fd0facd286d"]]},{"id":"185b7fd0facd286d","type":"function","z":"a3eacd3cd06ac2b4","name":"function 3","func":"msg.payload = \"hello world\"\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1101,"y":242,"wires":[["9ab91a6aeabd4e78"]]},{"id":"9ab91a6aeabd4e78","type":"link out","z":"a3eacd3cd06ac2b4","name":"link out 17","mode":"return","links":[],"x":1313,"y":340,"wires":[]}]
this does the same but using two output ports on the function node.
What is wrong with this solution?
the function 2 node does this:
if ( msg.linkcall_done) {
// do other stuff
msg.payload = msg.result
node.send([undefined, msg])
} else {
node.send([msg, undefined])
}
so the msg only goes out on the second port when the data from the link call is available. Much as the await does.
