Skip to content

feat(automation): support include with multiple node IDs#349

Closed
andypalmi wants to merge 16 commits into
mainfrom
feature/select-nodes-tool
Closed

feat(automation): support include with multiple node IDs#349
andypalmi wants to merge 16 commits into
mainfrom
feature/select-nodes-tool

Conversation

@andypalmi
Copy link
Copy Markdown
Contributor

Summary

  • The getNodes and selectNodes methods now support the include parameter (upstream, downstream, connected) when called with an array of IDs, iterating each node's connections and merging results with deduplication
  • Extracted _getConnectedNodes helper to share traversal logic between single-node and multi-node code paths
  • getNodes now always returns an array and throws if any node ID is not found, replacing null-return behavior
  • Defensive guards retained in invokeAction for both null and empty results

Test plan

  • Verify include with multiple IDs returns connected nodes from all starting points, deduplicated
  • Verify overlapping connections across IDs are properly deduplicated
  • Verify not-found node IDs throw descriptive errors
  • Verify empty results still throw descriptive errors in the action dispatcher

Closes #348

andypalmi added 2 commits May 20, 2026 14:55
Add automation/align-selection action that aligns nodes on the canvas
using Node-RED core alignment actions (grid, left, right, top, bottom,
middle, center).

The action resolves node IDs, reveals the target workspace, selects
the nodes, verifies the selection, and invokes the corresponding
core:align-selection-to-{direction} action.

Config nodes are automatically excluded from the selection since they
have no canvas position and break alignment. The result returns a
structured object with alignedNodes and excludedConfigNodes so the
caller knows which nodes were skipped and does not attempt to
manually reposition them.

Workspace existence and lock checks are performed before alignment.
Non-grid directions require at least 2 non-config nodes.
The getNodes and selectNodes methods now support the include parameter
(upstream, downstream, connected) when called with an array of IDs.
Previously include was ignored for arrays. For each node in the array,
connected nodes are retrieved and merged with deduplication.

Extracted _getConnectedNodes helper to share the traversal logic
between single-node and multi-node code paths.

getNodes now always returns an array and throws if any node ID is not
found, replacing the previous null-return behavior. The invokeAction
cases retain defensive guards for both null and empty results.
@andypalmi andypalmi changed the title Support include parameter with multiple node IDs feat(automation): support include with multiple node IDs May 20, 2026
Base automatically changed from feature/align-nodes-tool to main May 20, 2026 18:48
@andypalmi andypalmi requested a review from cstns May 20, 2026 18:48
Copy link
Copy Markdown
Contributor

@Steve-Mcl Steve-Mcl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I still really dislike this. I think the existing arrangement of splitting the action at the MCP server into 2 distinct tools of select_node (with include) and select_nodes (for selecting a list of ids) is the better approach - but I wont block.

1 last consideration - have you checked the affect of throwing errors has (instead of the original behaviour of returning null/empty array) on the existing expert action links (e.g. [function 1](nr-assistant://automation/select-nodes?id=709483aa8eeae4e5e) / [function 2](nr-assistant://automation/open-node-edit?id=709483aa8eeae4e5e)

Comment thread resources/expertAutomations.js Outdated
Comment on lines +512 to +532
const ids = Array.isArray(nodeId) ? nodeId : [nodeId]
const nodes = ids.map(id => {
const node = this.RED.nodes.node(id)
if (!node) throw new Error(`Node ${id} not found`)
return node
})
if (!include) {
return nodes
}
return null
const seen = new Set()
const result = []
for (const node of nodes) {
const connected = this._getConnectedNodes(node, include)
for (const n of connected) {
if (!seen.has(n.id)) {
seen.add(n.id)
result.push(n)
}
}
}
return result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will return an odd shaped array.
Also, IIRC, get upstream includes the source node so we might get duplicates in the result.

I think a better approach is to

  • loop ids - for each id
    • if id is already collected, continue (skip performing the include op altogether for this node)
    • get the include (if any)
    • loop the result of _getConnectedNodes & only add if not existing

andypalmi added 7 commits May 22, 2026 15:07
Add full parameter to get-nodes action. When false (default), returns
slim summaries (id, type, name, x, y, z, wires, valid) instead of
full exported node data.
Instead of throwing when a node ID is not found, getNodes now
filters out missing nodes and returns the rest. This prevents
errors when action links reference nodes that have since been
removed. The invokeAction layer still throws if no nodes at all
are found. Also adds deduplication optimization for the include
traversal.
When get-nodes or select-nodes receives IDs that don't exist,
the result now includes a warning listing the missing IDs
alongside any nodes that were found, instead of throwing an
error. This gives the agent actionable feedback without
breaking the response.
When get-nodes or select-nodes is called with include, the result
now groups connected nodes under each source node using the
direction as key (e.g. "downstream": [...]), instead of returning
a flat deduplicated array. This makes it clear which connections
belong to which source node.
When include is 'connected', the grouped result now separates
nodes into 'upstream' and 'downstream' keys instead of a single
'connected' array. This gives the caller clear directionality
for each source node's connections.
Replace getAllFlowNodes with BFS using getNodeLinks for upstream
and downstream traversal. The core getAllFlowNodes only applies
direction on the first hop then follows all links, which causes
sibling nodes to appear in directional results.
@andypalmi
Copy link
Copy Markdown
Contributor Author

Regarding splitting into two MCP tools - I don't think that makes sense from the agent's perspective. It would end up with two tools that do basically the same thing, and it would need to decide which one to use each time. A single select_nodes tool that accepts an array of IDs and optionally an include direction covers both use cases cleanly.

I've addressed the throwing issue - getNodes and selectNodes no longer throw for missing node IDs. Missing IDs are silently skipped (like the original behavior), and the result now includes a warning field listing which nodes weren't found, so the caller gets feedback without the request failing.

Applied your suggestions on the inline code:

  • Loop skips nodes already collected (if (seen.has(node.id)) continue) before calling _getConnectedNodes
  • Results from _getConnectedNodes are individually deduplicated
  • Missing node IDs are silently filtered out instead of throwing

I also changed the result shape when include is used — instead of a flat deduplicated array, results are now grouped by source node with the direction as the key. When using connected, the result splits into separate upstream and downstream keys per node. For example, given:

[inject1] → [function1] → [debug1]
                  ↕
[inject2] → [function2] → [debug2]

Calling get-nodes with ids: ["function1", "function2"], include: "connected" returns:

{
  "success": true,
  "nodes": [
    {
      "id": "function1", "type": "function",
      "upstream": [
        { "id": "inject1", "type": "inject" }
      ],
      "downstream": [
        { "id": "debug1", "type": "debug" },
        { "id": "function2", "type": "function" }
      ]
    },
    {
      "id": "function2", "type": "function",
      "upstream": [
        { "id": "inject2", "type": "inject" },
        { "id": "function1", "type": "function" }
      ],
      "downstream": [
        { "id": "debug2", "type": "debug" }
      ]
    }
  ]
}

Each source node lists its connections split by direction, so the agent can clearly see the flow topology. A node can appear in multiple source nodes' connections (e.g. function1 and function2 are connected to each other).

andypalmi added 6 commits May 22, 2026 16:52
…t-nodes

levels param groups connected nodes by depth (1 = direct, 2 = two hops),
making parallel vs serial topology visible. full param on select-nodes
returns summarized nodes by default, matching get-nodes behavior.
Config nodes (category: config) are filtered out since they are not
canvas nodes. Link nodes (link in/out/call) are excluded to avoid
resolving virtual cross-tab links during traversal.
Source node is now included in direction arrays for non-leveled results
and at level 0 for leveled results, matching Node-RED's selection
behavior.
Link nodes connected by a wire are included in connection results.
Traversal stops at them so virtual cross-tab links are not resolved.
Connection results always use leveled format (grouped by depth) when
include is specified. levels defaults to 0 (all levels). Junction nodes
are now resolved via RED.nodes.junction fallback since they are not in
the regular node registry.
The validator rejects integer type for JavaScript numbers. Using number
type instead since JSON has no distinct integer type.
@andypalmi
Copy link
Copy Markdown
Contributor Author

Closing — decided to rely on Node-RED core actions instead of maintaining custom traversal code

@andypalmi andypalmi closed this May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support include parameter with multiple node IDs in get-nodes and select-nodes

2 participants