Unconnected junction nodes

We've all run into this: a junction node sits right on top of a wire — visually it looks like part of the flow — but it isn't actually connected to it. Finding these cases currently takes a lot of effort, because nothing in the editor distinguishes a junction that's properly wired from one that merely overlaps a wire.

The nasty case is a junction with exactly one connection (one wire in and nothing out, or vs.). A junction with zero connections is at least somewhat visible, since it usually sits on its own. But a single-connection junction that happens to lie over another wire is practically impossible to spot by eye — and Ctrl+F doesn't help either, because junctions have no name or properties to match against.

It would be very user friendly if such cases could be more easily identifiable. Two possible directions (either would solve it):

  1. A way to find junction nodes by their number of connections — the most useful being 0 and 1. This could live in the search, or as a "validate flows"-style check.
  2. Or, junctions with fewer than two connections could be rendered with a visual marker (for example a highlight, a different color, or a red exclamation mark), so they stand out on the canvas.

For context: right now the only approach I've found is to parse flows.json and count, for each junction, how often its ID appears in other nodes' wires (plus subflow in/out/status ports). That identifies which junctions are affected — but it still doesn't help me locate them in the editor: a junction has no name, so Ctrl+F won't find it by ID, and there are no visible coordinates to navigate to. In practice I have to trace back to a named upstream node to even find the junction on the canvas.

Optional, more general idea: the mechanism described in option 1 could be extended to any node type, not just junctions. For most standard nodes this is less interesting, but for function nodes it would be handy — e.g. quickly finding function nodes with no outgoing wire, which are often a forgotten or dead branch. The junction case above would just be one specific use of that more general capability.

Thanks!

This has come up a few times and I too have fallen for that trap.

There is a fan node - node-red-contrib-fan - which was the father of this.

Things I have learnt:
1 - don't put junction nodes over existing wires so as to make it look like it is using those wires.
2 - When connecting to a junctionnode: after connecting to it, drag the node to be sure all observed wires move too. This then detects any wires not connected.

The fan node has a couple of nice things:
1 - It is easier to see
2 - you can expand it's size and put a Name in it to help identify what all the wires are doing.

Say you have all the wires that are to do Function x (yeah, I can't think just now of a better name) that you have Joined together with the fan node to go elsewhere - more tidily that all the individual wires - you put the text function x in the name of the fan node.
Then when looking at the flow, you see the fan node with all the wires and it is named.
So you know what all those wires are doing and it (kinda) helps see things more clearly.

That's prevention, not detection — the request is about detecting existing cases. The drag-test doesn't really work when you have several hundred junction nodes and don't know whether any is broken at all, let alone which one.

How are you expecting the editor to know that you want wires connected to a junction node or not?

That the node is on top of the wire doesn't mean a thing to the editor.

I fear you have your work cut out for yourself.

Why could the editor not detect a wire passing directly under a node I/O connection?

This JSONata expression will return all junction Id's that have no in or out wire and with no wires at all connected
eg

[{"id":"7e18efe9337808ab","type":"inject","z":"613df62afc8a16bf","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"id\":\"5aeabf8c4a8bdef8\",\"type\":\"inject\",\"z\":\"613df62afc8a16bf\",\"name\":\"\",\"props\":[{\"p\":\"payload\"},{\"p\":\"topic\",\"vt\":\"str\"}],\"repeat\":\"\",\"crontab\":\"\",\"once\":false,\"onceDelay\":0.1,\"topic\":\"\",\"payload\":\"\",\"payloadType\":\"date\",\"x\":120,\"y\":885,\"wires\":[[\"d96b6507558b093b\"]]},{\"id\":\"928fb8d89f07a80a\",\"type\":\"debug\",\"z\":\"613df62afc8a16bf\",\"name\":\"debug 18\",\"active\":true,\"tosidebar\":true,\"console\":false,\"tostatus\":false,\"complete\":\"false\",\"statusVal\":\"\",\"statusType\":\"auto\",\"x\":450,\"y\":675,\"wires\":[]},{\"id\":\"5c12d62b5db0e661\",\"type\":\"inject\",\"z\":\"613df62afc8a16bf\",\"name\":\"\",\"props\":[{\"p\":\"payload\"},{\"p\":\"topic\",\"vt\":\"str\"}],\"repeat\":\"\",\"crontab\":\"\",\"once\":false,\"onceDelay\":0.1,\"topic\":\"\",\"payload\":\"\",\"payloadType\":\"date\",\"x\":210,\"y\":735,\"wires\":[[\"062592ee003bc92b\",\"60dfb6ccede5d405\"]]},{\"id\":\"b56e0f71070c9bb9\",\"type\":\"debug\",\"z\":\"613df62afc8a16bf\",\"name\":\"debug 29\",\"active\":true,\"tosidebar\":true,\"console\":false,\"tostatus\":false,\"complete\":\"false\",\"statusVal\":\"\",\"statusType\":\"auto\",\"x\":600,\"y\":795,\"wires\":[]},{\"id\":\"270b4fe377d19bdf\",\"type\":\"junction\",\"z\":\"613df62afc8a16bf\",\"x\":300,\"y\":645,\"wires\":[[\"928fb8d89f07a80a\"]]},{\"id\":\"062592ee003bc92b\",\"type\":\"junction\",\"z\":\"613df62afc8a16bf\",\"x\":300,\"y\":735,\"wires\":[[]]},{\"id\":\"60dfb6ccede5d405\",\"type\":\"junction\",\"z\":\"613df62afc8a16bf\",\"x\":285,\"y\":705,\"wires\":[[\"928fb8d89f07a80a\"]]},{\"id\":\"180e8f16269453a8\",\"type\":\"junction\",\"z\":\"613df62afc8a16bf\",\"x\":345,\"y\":660,\"wires\":[[]]},{\"id\":\"d96b6507558b093b\",\"type\":\"change\",\"z\":\"613df62afc8a16bf\",\"name\":\"\",\"rules\":[{\"t\":\"set\",\"p\":\"payload\",\"pt\":\"msg\",\"to\":\"\",\"tot\":\"str\"}],\"action\":\"\",\"property\":\"\",\"from\":\"\",\"to\":\"\",\"reg\":false,\"x\":235,\"y\":930,\"wires\":[[\"9aedbcd00a762307\"]]},{\"id\":\"9aedbcd00a762307\",\"type\":\"debug\",\"z\":\"613df62afc8a16bf\",\"name\":\"debug 28\",\"active\":true,\"tosidebar\":true,\"console\":false,\"tostatus\":false,\"complete\":\"false\",\"statusVal\":\"\",\"statusType\":\"auto\",\"x\":420,\"y\":915,\"wires\":[]}]","payloadType":"json","x":185,"y":825,"wires":[["0ccf1b06d397e110"]]},{"id":"0ccf1b06d397e110","type":"change","z":"613df62afc8a16bf","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t   $junc := $$.payload[type = \"junction\"];\t   $ids := $$.payload.wires[0];\t   $junc[$not($.id in $ids) or $count($.wires[0])<1].id\t)\t    ","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":825,"wires":[["928fb8d89f07a80a"]]},{"id":"928fb8d89f07a80a","type":"debug","z":"613df62afc8a16bf","name":"debug 18","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":450,"y":675,"wires":[]}]

You would have to load your flow.json using a file read node.

Nice, that's much closer to what I'm after — thanks! Two caveats I hit when doing the same via a script, though:

  • wires[0] only checks the first output (so a junction fed only by e.g. output 1 of a switch is a false positive), and subflow in/out/status connections aren't in the wires array either.
  • But the bigger point: this still only tells me which junction IDs are affected — it doesn't help me locate them on the canvas, since a junction has no name for Ctrl+F and there are no coordinates to jump to. That last step is really the core of the request.

Junctions only have one output, this is what wires[0] is searching for on the output of only the junction nodes.

Once you have the id's you can search for them in the info sidebar.

Looks like the search issue was not fixed my mistake. How to find a Junction - #11 by cymplecy

Nice use of Jsonata once again @E1cid.
I've been struggling all morning to get chatgpt to give me a working Jsonata expression!

This flow uses E1cid's, tweeking the output to show the name of the tab for each junction and it's x,y coordinates (because searching for a junction is not that easy)

[{"id":"0ccf1b06d397e110","type":"change","z":"72e3ac42d134d33c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t   $junc := $$.payload[type = \"junction\"];\t   $ids := $$.payload.wires[0];\t   $junc[$not($.id in $ids) or $count($.wires[0])<1].id\t)\t    ","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":580,"wires":[["b688a0e30a71e486"]]},{"id":"928fb8d89f07a80a","type":"debug","z":"72e3ac42d134d33c","name":"Junctions missing input or output wires","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":630,"y":640,"wires":[]},{"id":"e5ade9098f7dd07f","type":"file in","z":"72e3ac42d134d33c","name":"Rescan flows file","filename":"path","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":330,"y":580,"wires":[["397e22af8ea7a052"]]},{"id":"397e22af8ea7a052","type":"json","z":"72e3ac42d134d33c","name":"","property":"payload","action":"","pretty":false,"x":490,"y":580,"wires":[["0ccf1b06d397e110"]]},{"id":"d33657be1d76029f","type":"function","z":"72e3ac42d134d33c","name":"Find tab names & junctions","func":"let tablist = {}\nlet junctionlist = {}\nfor (let i in msg.payload) {\n    const id = msg.payload[i].id\n    if (msg.payload[i].type == \"tab\") {\n        tablist[id] = msg.payload[i].label\n    }\n    if (msg.payload[i].type == \"junction\") {\n        junctionlist[id] = msg.payload[i]\n    }\n}\nflow.set(\"tablist\", tablist)\nflow.set(\"junctionlist\", junctionlist)\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":520,"wires":[["e5ade9098f7dd07f"]]},{"id":"8b05d215b697cb81","type":"json","z":"72e3ac42d134d33c","name":"","property":"payload","action":"","pretty":false,"x":490,"y":460,"wires":[["d33657be1d76029f"]]},{"id":"6f7b7be18dea0aee","type":"file in","z":"72e3ac42d134d33c","name":"Scan flows file","filename":"path","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":320,"y":460,"wires":[["8b05d215b697cb81"]]},{"id":"5e411acbede24c51","type":"inject","z":"72e3ac42d134d33c","name":"flows file","props":[{"p":"path","v":"/home/pi/.node-red/flows.json","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":160,"y":460,"wires":[["6f7b7be18dea0aee"]]},{"id":"b688a0e30a71e486","type":"function","z":"72e3ac42d134d33c","name":"Enhance output","func":"const tablist = flow.get('tablist') || {}\nconst junctionlist = flow.get ('junctionlist') || {}\n\nfor (let i=0; i< msg.payload.length; i++) {\n    const j = msg.payload[i]\n    msg.payload[i] = {\n        \"id\": j,\n        \"tabid\": junctionlist[j].z,\n        \"tabname\": tablist[junctionlist[j].z],\n        \"x,y\": junctionlist[j].x + \",\" + junctionlist[j].y\n    }\n}\nreturn msg;\n/*\n\n*/","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":640,"wires":[["928fb8d89f07a80a"]]}]

That is indeed a problem. I've also played around with orphan nodes (as I called them) but in my implementation, I ignored junctions for exactly that reason - there was no way to highlight them.

There is also no jQuery triggery to hack around that because junction nodes do not have an identifying id within the workspace, i.e. $('#<node-id>') will give you access to any node but not junction.

Node-RED lint (GitHub - node-red/nrlint: Node-RED Flow Linter · GitHub) does already have a rule to report unconnected nodes...

Locate under-connected junctions (console helper)

Two things in one here: first, a solution to find and visually mark junctions anywhere in Node-RED — across all tabs and subflows. And second, this doubles as a working proof of concept for my feature request issue #4967: it shows that locating and highlighting junctions is achievable, even though there's currently no built-in way to do it.

The script finds and jumps to junctions with fewer than 2 connections (dead/under-connected ones). It counts incoming + outgoing wires as well as subflow in/out/status ports. (Dropping the connection-count filter turns it into a "mark every junction" tool — the locate/highlight mechanism is the same.)

It's a read-only helper — it only reads the flow and draws a marker on screen, it never modifies anything, so there's nothing to accidentally break.

How it works: it reads the full flow via RED.nodes.createCompleteNodeSet(), computes each junction's connection count, then locates the elements on the canvas using d3's bound data (__data__) — which sidesteps the fact that junctions have no id-addressable DOM node. It auto-switches tabs and centers each hit with a pulsing marker.

Usage: open the Node-RED editor, open the browser DevTools console, paste the script.
(If Node-RED runs inside an iframe, e.g. the Home Assistant add-on, select the Node-RED frame in the console's context dropdown first — otherwise you'll get RED is not defined.)

Controls:

  • next() / prev() — step through manually
  • goTo(n) — jump to result n
  • play() or play(1000) — auto-advance (ms per step)
  • stop() — halt auto-advance
  • clearRing() — remove the marker

Re-running the script is safe: it stops and replaces any previous instance automatically.

(function(){
  // --- Self-protect: kill any previous instance before starting a new one ---
  if(window.__jfLocator && typeof window.__jfLocator.destroy==='function'){
    window.__jfLocator.destroy();
  }

  if(typeof RED.nodes.createCompleteNodeSet!=='function'){
    console.warn('RED.nodes.createCompleteNodeSet is missing – let me know and I will use another approach.');return;
  }
  if(!document.getElementById('jf-loc-style')){
    const s=document.createElement('style'); s.id='jf-loc-style';
    s.textContent='@keyframes jfpulse{0%{transform:translate(-50%,-50%) scale(.5);opacity:1}100%{transform:translate(-50%,-50%) scale(1.8);opacity:.1}}';
    document.head.appendChild(s);
  }

  // 1. Get the complete flow set (same shape as flows.json, incl. junctions & subflows)
  const flows=RED.nodes.createCompleteNodeSet();

  // Container labels (tabs + subflows)
  const label={};
  flows.forEach(n=>{
    if(n.type==='tab') label[n.id]=n.label||('Tab '+n.id);
    if(n.type==='subflow') label[n.id]='Subflow: '+(n.name||n.id);
  });

  // 2. Count degree: incoming + outgoing wires, plus subflow in/out/status ports
  const grad={}; const add=id=>{ if(id) grad[id]=(grad[id]||0)+1; };
  flows.forEach(n=>{
    if(Array.isArray(n.wires)) n.wires.forEach(o=>{ if(Array.isArray(o)) o.forEach(z=>{ add(n.id); add(z); }); });
    if(n.type==='subflow') ['in','out','status'].forEach(k=>{
      const arr=Array.isArray(n[k])?n[k]:(n[k]?[n[k]]:[]);
      arr.forEach(p=>(p.wires||[]).forEach(w=>add(w.id)));
    });
  });

  // 3. Collect junctions with degree < 2
  const list=[];
  flows.forEach(n=>{ if(n.type==='junction'){ const g=grad[n.id]||0; if(g<2)
    list.push({id:n.id,z:n.z,x:n.x,y:n.y,grad:g,tab:label[n.z]||n.z}); }});

  // Summary
  const byTab={}; list.forEach(j=>{(byTab[j.tab]=byTab[j.tab]||[]).push(j);});
  console.log('Suspect junctions (connections < 2): '+list.length);
  Object.keys(byTab).forEach(t=>console.log('   '+t+': '+byTab[t].length));

  // --- Navigation state ---
  const sleep=ms=>new Promise(r=>setTimeout(r,ms));
  const getSvg=()=>document.querySelector('#red-ui-workspace-chart svg')||document.querySelector('svg');
  function findEl(id){ let f=null; getSvg().querySelectorAll('g').forEach(g=>{ if(g.__data__&&g.__data__.id===id) f=g; }); return f; }

  let cur=null, idx=-1, timer=null;

  function ring(el,text){
    const r=el.getBoundingClientRect(), cx=r.left+r.width/2, cy=r.top+r.height/2, size=70;
    const m=document.createElement('div');
    m.className='jf-loc-ring';
    Object.assign(m.style,{position:'fixed',left:cx+'px',top:cy+'px',width:size+'px',height:size+'px',
      border:'3px solid #e00',borderRadius:'50%',boxShadow:'0 0 0 3px rgba(238,0,0,.4),0 0 14px 5px rgba(238,0,0,.55)',
      pointerEvents:'none',zIndex:99999,animation:'jfpulse 1s ease-out infinite'});
    const dot=document.createElement('div');
    Object.assign(dot.style,{position:'absolute',left:'50%',top:'50%',width:'9px',height:'9px',
      transform:'translate(-50%,-50%)',background:'#e00',borderRadius:'50%'}); m.appendChild(dot);
    const t=document.createElement('div');t.textContent=text;
    Object.assign(t.style,{position:'absolute',top:'-22px',left:'50%',transform:'translateX(-50%)',
      background:'#e00',color:'#fff',font:'11px monospace',padding:'2px 6px',borderRadius:'4px',whiteSpace:'nowrap'});
    m.appendChild(t);document.body.appendChild(m);return m;
  }

  async function show(i){
    if(cur){cur.remove();cur=null;}
    if(i<0||i>=list.length){console.log('Reached the end.');stopTimer();return false;}
    idx=i; const j=list[i];
    if(RED.workspaces.active()!==j.z){ RED.workspaces.show(j.z); await sleep(400); }
    const el=findEl(j.id);
    if(!el){console.warn('Element not found (subflow may need to be opened):',j.id,'on',j.tab);return true;}
    el.scrollIntoView({block:'center',inline:'center'}); await sleep(250);
    cur=ring(el,(i+1)+'/'+list.length+'  ['+j.tab+']  degree '+j.grad+'  '+j.id);
    console.log((i+1)+'/'+list.length,'['+j.tab+']','degree '+j.grad,j.id,'x='+j.x,'y='+j.y);
    return true;
  }

  function stopTimer(){ if(timer){clearInterval(timer);timer=null;console.log('Stopped at '+(idx+1)+'/'+list.length);} }

  // --- Public API ---
  window.next=()=>{stopTimer();show(idx+1);};
  window.prev=()=>{stopTimer();show(idx-1);};
  window.goTo=n=>{stopTimer();show(n-1);};
  window.clearRing=()=>{stopTimer();if(cur){cur.remove();cur=null;}};
  window.play=(ms)=>{ stopTimer(); const step=ms||1200; if(idx>=list.length-1) idx=-1;
    (async()=>{ await show(idx+1);
      timer=setInterval(async()=>{ if(!await show(idx+1)) stopTimer(); }, step);
      console.log('play running ('+step+'ms). Stop with  stop()'); })(); };
  window.stop=stopTimer;

  // --- Cleanup handle used for self-protect on re-run ---
  window.__jfLocator={
    destroy(){
      try{ if(timer){clearInterval(timer);timer=null;} }catch(e){}
      try{ if(cur){cur.remove();cur=null;} }catch(e){}
      document.querySelectorAll('.jf-loc-ring').forEach(d=>d.remove());
      ['next','prev','goTo','play','stop','clearRing'].forEach(f=>{ try{delete window[f]}catch(e){window[f]=undefined} });
    }
  };

  if(!list.length){console.log('Nothing to do – every junction has >= 2 connections.');return;}
  console.log('Manual: next() | prev() | goTo(n) | clearRing()   Auto: play() / play(1000) | stop()');
  show(0);
})();