Skip to content

feat(editor): Add empty node groups (no-changelog) - #37884

Draft
jgalmeida wants to merge 33 commits into
masterfrom
jgalmeida/feat-empty-node-groups-poc
Draft

feat(editor): Add empty node groups (no-changelog)#37884
jgalmeida wants to merge 33 commits into
masterfrom
jgalmeida/feat-empty-node-groups-poc

Conversation

@jgalmeida

@jgalmeida jgalmeida commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

POC for empty node groups on the canvas. An empty group renders as a card with an inline, editable description, so you can name and describe a block of work before adding any nodes. Empty groups can be created from the canvas toolbar and context menu, connect through chip handles, and be filled by adding a node or generating one through the AI builder.

How to test

  1. Open a workflow.
  2. Add an empty group from the canvas toolbar or context menu.
  3. Give it a name and click the description to add one.
  4. Fit the workflow to view (zoom out): the description and its edit affordance stay usable down to ~0.4 zoom.
  5. Add a node to the group, or use Build to generate one.

Related Linear tickets, Github issues, and Community forum posts

Review / Merge checklist

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)

🤖 PR Summary generated by AI

Review in cubic

jgalmeida and others added 30 commits September 3, 2026 16:11
Only the canvas inserts this node, so it must not be user-addable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The re-insert added the placeholder and its connections without history,
and snapshotted the group after the insert, so undo restored the deleted
node next to an orphan placeholder on a duplicate parallel path. Record
the placeholder and its edges, and snapshot the group before the insert.

Also count only connectable members when deciding whether the deleted
node was the group's last one: a group of one node plus a sticky lost
its placeholder and bridged past itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An empty group holds only a hidden placeholder, so ungrouping strands it
on the canvas, there are no nodes to add to the chat, and collapsing is a
no-op. Hide those controls and guard the shared ungroup and toggle paths.

Also cap the description panel height so a long description scrolls
instead of covering the canvas, and reject a second connection onto a
chip rather than auto-extending the group out of its empty state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An empty group is force-collapsed by the mapping, so it has no inline
title editor even when the view store still reports it expanded. The
rename flow read the view store alone, set the autofocus id and nothing
happened. Decide on what the canvas actually renders instead, and apply
the same correction to the description context menu item.

Also open that rename flow right after the "add empty group" menu item
creates a group, so the user can type its title straight away.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e last real member

Counting connectable members does not say which node is being deleted.
Deleting a sticky from a group of one node plus a sticky passed the
guard, so a placeholder joined a group that still held a real node and
was then orphaned: two connectable members never read as an empty group.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The chip handles were opacity:0 and pointer-events:none, inherited from
the decorative handles on a normal collapsed group, so a connection could
never start or land on an empty group. Give an empty group's handles a
visible, interactive style while normal groups keep the hidden ones.
…tion

Generate on an empty group now drops three chained demo nodes into the
group when the AI builder is unavailable, so the empty-to-filled round
trip is demoable offline. An empty group always shows its description and
enlarges its connection handle hit area.
A collapsed group with a description now shows it under the title, not
only on hover or toggle, unifying empty and filled chips.
Add the prototype's resting empty-group affordances to the chip:

- An EMPTY badge and a dashed border, gated on isEmptyGroup.
- A centered "+" that opens the node creator scoped to the group's
  placeholder. Picking a node runs the existing replace path, so the
  placeholder is swapped for the node inside the group and its boundary
  connections move onto the node. No new fill helper is needed.
- A Build button in the description editor next to Save. Build saves the
  objective, then generates. The standalone toolbar sparkle is removed
  for empty groups so there is one Build affordance.

Connection inheritance on fill is the grouped replaceNode contract, which
is already locked by its own test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An empty group's card carries an add-node body under the header, so the
VueFlow node height, the collapsed rect used by the layout pushes, and the
component wrapper must all agree on the card height. getGroupCardHeight is
the single source for that value. Expanded groups are unchanged.

Also replaces the v1 alignment plan with the frame-by-frame v2 plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The group title bar is now one card, as in the prototype frames: a header
with the title, a right slot (EMPTY badge on an empty group, collapse chevron
on a filled one), and the description inline under the title. The
description is clamped to one line when expanded and two when collapsed, and
opens an in-place editor on click; Save and Build stay in that editor. An
empty group adds a body with the centered add-node "+". Collapsed cards show
their side handles as dots because edges re-anchor onto them; only an empty
card accepts new connections.

The card height comes from getGroupCardHeight, the same source the mapping
uses for the VueFlow node, so the handles sit at the middle of what is drawn.
The floating description panel and its pin, info-icon and hover-reveal
affordances are gone from the component; their providers are retired
separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The canvas toolbar gets an add-group button above the sticky-note button,
as in the prototype; it inserts the group at the centre of the visible
canvas. Creation moves into useCanvasOperations.addEmptyGroup, which adds the
placeholder node and its group inside one history bulk so undo removes both
together. The context menu action uses the same path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The comment described a missing drop guard. Group connection rules are
decided on release for every endpoint, and node handles carry no earlier
per-group validation either, so an empty card already behaves as a node does
during a drag. The comment now says so; no marker remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nline card

The group card shows its description inline in every state, so the pin/eye
toggle, the info-icon hover reveal, the per-workflow pinned-state storage
and the four show/hide context-menu items have nothing left to control.
Remove them and their strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VueFlow pauses its props-to-store sync for a tick after every store echo and
drops a nodes update that lands in that window. Adding the placeholder and
creating its group a microtask apart could therefore leave the canvas with a
bare placeholder and no group card. Run both store mutations in one
synchronous block so the mapping emits a single VueFlow update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A source node validates a drop with its own isValidConnection, which parses
the target handle id as a port string (mode/type/index). The group card's
handles used the bare ids "left"/"right", which parse to the default mode
"outputs", so a node output dropped on the group's left handle read as
output-to-output and was refused before the connect event fired.

Give the group handles the same port-string ids node handles use:
inputs/main/0 on the left (target) and outputs/main/0 on the right (source).
NodeView.resolveGroupEndpoint still rewrites the endpoint onto the
placeholder node, so the stored connection is unchanged; the collapsed-group
edge remap keeps using the constants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The editor measured its textarea inside a bare nextTick, which can run before
the browser lays the element out, so scrollHeight read the collapsed one-line
height and the editor opened too short until the next keystroke. Measure in a
requestAnimationFrame instead, and give the textarea a fixed one-line starting
height so the measurement is stable under the canvas zoom transform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header clips its overflow so the resting title and description stay inside
their line clamps. The description editor is an absolutely positioned overlay
that grows below the header, so its action buttons (cancel, save, build) were
clipped by that same overflow. Drop the clip only while editing; the header
keeps its fixed height, so the card layout is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lower the group description zoom threshold so a fit-to-view of a normal
workflow still shows the description and its click-to-edit target. Below
0.66 an empty group lost its only affordance to add a description.

Elevate the description editor overlay with a shadow so it reads as a
popover above the member nodes it covers, not a box glued on top.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cubic-dev-ai

cubic-dev-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Running ultrareview automatically — Adds a new placeholder node type and reworks group/connection logic across 35 files, touching core workflow data, deletion/undo paths, and execution pass-through—high risk of subtle regressions in group handling and workflow integrity.. I'll post findings when complete.

@n8n-assistant

n8n-assistant Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR review overview

Based on ownership of the 33 changed files in this PR:

Ownership Files owned Share Source code Test files Misc
@n8n-io/frontend 28 85% +941 / -684 +672 / -449 +0 / -0
@n8n-io/nodes 4 12% +48 / -0 +15 / -0 +0 / -0
@n8n-io/catalysts 1 3% +3 / -2 +0 / -0 +0 / -0
Total 33 100% +992 / -686 +687 / -449 +0 / -0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 6.59kB (0.01%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
editor-ui-esm 63.33MB 6.59kB (0.01%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: editor-ui-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/src-*.js -146 bytes 805.37kB -0.02%
assets/src-*.js 229 bytes 3.37MB 0.01%
assets/worker-*.js 3.2MB 3.22MB 17352.85% ⚠️
assets/worker-*.js -3.2MB 18.44kB -99.43%
assets/workflows.store-*.js 722 bytes 833.22kB 0.09%
assets/useWorkflowDocumentRenderData-*.js -5.4kB 685.47kB -0.78%
assets/useWorkflowDocumentRenderData-*.css 329 bytes 398.31kB 0.08%
assets/NodeView-*.js 4.76kB 346.51kB 1.39%
assets/useCanvasOperations-*.js 4.05kB 154.23kB 2.7%
assets/constants-*.js -40 bytes 44.69kB -0.09%
assets/NodeCreation-*.js 711 bytes 15.22kB 4.9%
assets/useCanvasMapping.groups-*.js 1.53kB 11.49kB 15.36% ⚠️
assets/durations-*.js -164 bytes 1.19kB -12.15%

Files in assets/workflows.store-*.js:

  • ./src/app/stores/workflowDocument.store.ts → Total Size: 17.45kB

  • ./src/features/workflows/canvas/canvas.types.ts → Total Size: 1.49kB

Files in assets/useWorkflowDocumentRenderData-*.js:

  • ./src/features/workflows/canvas/components/Canvas.vue → Total Size: 351 bytes

  • ./src/features/shared/contextMenu/composables/contextMenuGroupView.ts → Total Size: 277 bytes

  • ./src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue → Total Size: 405 bytes

  • ./src/features/workflows/canvas/composables/useCanvasMapping.ts → Total Size: 9.29kB

  • ./src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts → Total Size: 11.45kB

  • ./src/features/shared/contextMenu/composables/useContextMenuItems.ts → Total Size: 12.81kB

Files in assets/NodeView-*.js:

  • ./src/features/workflows/canvas/components/WorkflowCanvas.vue → Total Size: 355 bytes

  • ./src/app/views/NodeView.vue → Total Size: 298 bytes

Files in assets/useCanvasOperations-*.js:

  • ./src/app/composables/useCanvasOperations.ts → Total Size: 92.11kB

Files in assets/constants-*.js:

  • ./src/app/constants/nodeTypes.ts → Total Size: 5.85kB

  • ./src/app/constants/localStorage.ts → Total Size: 2.52kB

Files in assets/NodeCreation-*.js:

  • ./src/features/shared/nodeCreator/views/NodeCreation.vue → Total Size: 338 bytes

Files in assets/useCanvasMapping.groups-*.js:

  • ./src/features/workflows/canvas/stores/canvasNodeGroups.constants.ts → Total Size: 1.57kB

  • ./src/features/workflows/canvas/composables/useCanvasMapping.groups.ts → Total Size: 9.23kB

@n8n-assistant n8n-assistant Bot added n8n team Authored by the n8n team node/new Creation of an entirely new node labels Sep 4, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

Ultrareview completed in 15m 20s

17 issues found and verified against the latest diff

Confidence score: 2/5

  • packages/frontend/editor-ui/src/app/views/NodeView.vue can apply async demo-generation results to the newly active workflow after navigation, potentially mutating the wrong workflow; bind the continuation to the originating group before merging results.
  • packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts can corrupt workflow topology when deleting grouped nodes with multiple main ports, and NodeView.vue can leave a direct bypass around generated chains; preserve distinct branch/merge connections and prevent old neighbours from being reconnected after boundary edges are created.
  • packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts and useCanvasMapping.groups.ts can retain expanded state for empty groups while rendering no frame, shifting nearby nodes and allowing hidden placeholders to be selected; use one effective collapsed state for layout, mapping, and selection.
  • packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue has two interaction regressions: expanded-group descriptions can render behind member nodes, and long collapsed names may not receive a tooltip; move the editor to the canvas popper and detect horizontal truncation.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts">

<violation number="1" location="packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts:404">
P2: Custom agent: **Frontend**

`add_empty_group` is untested, so regressions in this empty-canvas menu path or its read-only gating can ship despite the existing operation test. Add a `useContextMenu` test that asserts the item appears for an empty selection and is disabled in read-only mode.</violation>
</file>

<file name="packages/frontend/editor-ui/src/app/views/NodeView.vue">

<violation number="1" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1124">
P2: When the mock generation operation rejects, this detached promise produces an unhandled failure after `onGenerateGroup` has already returned. Await the operation and catch the error so a failed generation is reported instead of becoming an unhandled rejection.</violation>

<violation number="2" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1159">
P2: When mock generation runs from a scoped document, calling `useUniqueNodeName()` inside the event handler resolves the wrong document context because Vue has no active injection context after setup. Create this composable during setup and reuse it, or derive unique names from `workflowDocumentStore` directly.

(Based on your team's feedback about avoiding provide/inject outside the component tree.)</violation>

<violation number="3" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1163">
P1: Custom agent: **Frontend**

When navigation occurs while demo generation is loading node metadata, the continuation mutates the newly active workflow instead of the group that started generation. Bind the operation to the originating document and abort all post-await mutations when the document changes.</violation>

<violation number="4" location="packages/frontend/editor-ui/src/app/views/NodeView.vue:1232">
P1: When a connected empty group uses the mock generation path, deleting the placeholder reconnects its old neighbours after the new boundary edges were added. The generated chain therefore has an extra direct bypass from each predecessor to successor. Replace the placeholder through the existing grouped-node replacement operation, or remove it without connecting adjacent nodes.</violation>
</file>

<file name="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue">

<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue:112">
P3: The new `$style.empty` and `$style.descriptionEditing` bindings have no matching module styles and add no useful state styling. Remove both unused references or add the corresponding module selectors.</violation>

<violation number="2" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue:789">
P2: Custom agent: **Frontend**

A long collapsed group name is now ellipsized, but `updateTruncated()` still checks only vertical overflow, so `isTitleTruncated` stays false and the full name tooltip never opens. Detect horizontal overflow with `scrollWidth > clientWidth` for collapsed titles.</violation>

<violation number="3" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue:883">
P2: When a filled group is expanded, a long description editor is painted inside the group’s `-10000` VueFlow stacking context, so member nodes paint over the textarea and actions. Teleport the editor to the canvas popper layer or raise the group node while editing.</violation>
</file>

<file name="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts">

<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts:97">
P1: Custom agent: **Frontend**

When an expanded group loses its last real node, this line collapses only `rect` while the view state still treats the group as expanded. The layout then moves nearby nodes around the invisible `expandedRect`, even though only the empty card renders. Use `collapsedRect` as the empty group’s effective `expandedRect`, or exclude empty groups from layout push sources.</violation>
</file>

<file name="packages/frontend/editor-ui/src/features/workflows/canvas/stores/canvasNodeGroups.constants.ts">

<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/stores/canvasNodeGroups.constants.ts:23">
P2: This threshold change leaves the existing title-bar test failing: at zoom `0.5`, the description now renders because `0.5 >= 0.4`. Update the test to use a zoom below `0.4` and add a boundary assertion for `0.4`.</violation>
</file>

<file name="packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue">

<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue:982">
P3: When an empty group is selected, Ctrl/Cmd+Shift+G remains enabled because `canUngroupSelection` only checks that a group is selected, but this guard silently returns. Exclude empty groups from the shortcut eligibility, or disable the shortcut when no non-empty group is selected.</violation>

<violation number="2" location="packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue:1548">
P2: When a customer right-clicks a new canvas location and chooses Add empty group, this event carries no location, so the group is created at the previous click position or `[0, 0]`. Forward the projected context-menu coordinates through the event, or update `lastClickPosition` when opening the canvas context menu.</violation>
</file>

<file name="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.groups.ts">

<violation number="1" location="packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasMapping.groups.ts:275">
P2: When an empty group is still expanded in view state, this line collapses only its rendered card. Layout then pushes nearby nodes for an invisible frame, while selection can add the hidden placeholder; use one effective collapsed predicate for layout and selection too, or clear the view state when the group becomes empty.</violation>
</file>

<file name="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts">

<violation number="1" location="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts:623">
P1: When the deleted group member has multiple main input or output ports, this rewiring changes the workflow topology: distinct IF/Switch branches can both execute downstream, and separate Merge inputs are collapsed. Do not replace multi-port members with the single-port placeholder, or add matching ports and preserve each connection index.</violation>

<violation number="2" location="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts:3784">
P2: When two empty-group creations overlap, such as a double-click while the node-property load is pending, both calls compute `Group 1` before either mutation. Compute the default name after the await with a fresh group list, or serialize/recheck creation before calling `createGroup`.</violation>

<violation number="3" location="packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts:3801">
P2: When a user adds an empty group after cancelling a connection, this call can connect the hidden placeholder to the previously active node instead of creating a standalone group. Pass `isAutoAdd: true` or otherwise disable automatic connection for the placeholder insertion.</violation>
</file>

<file name="packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts">

<violation number="1" location="packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts:286">
P2: When a group still references an unresolved non-sticky node, this filter drops that member and classifies `[placeholderId, staleId]` as empty. Require every member ID to resolve before returning the placeholder, so stale groups are not collapsed or given empty-group chip operations.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.
Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

const names = ['Extract', 'Transform', 'Load'].map((label) => uniqueNodeName(label));

// Add the three nodes chained left to right.
await addNodesAndConnections(

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P1: Custom agent: Frontend

When navigation occurs while demo generation is loading node metadata, the continuation mutates the newly active workflow instead of the group that started generation. Bind the operation to the originating document and abort all post-await mutations when the document changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/views/NodeView.vue, line 1163:

<comment>When navigation occurs while demo generation is loading node metadata, the continuation mutates the newly active workflow instead of the group that started generation. Bind the operation to the originating document and abort all post-await mutations when the document changes.</comment>

<file context>
@@ -1043,6 +1081,157 @@ function onCreateSticky() {
+	const names = ['Extract', 'Transform', 'Load'].map((label) => uniqueNodeName(label));
+
+	// Add the three nodes chained left to right.
+	await addNodesAndConnections(
+		names.map((name, i) => ({
+			type: SET_NODE_TYPE,
</file context>
Fix with cubic

Comment on lines +97 to 99
rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect,
collapsedRect,
expandedRect,

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P1: Custom agent: Frontend

When an expanded group loses its last real node, this line collapses only rect while the view state still treats the group as expanded. The layout then moves nearby nodes around the invisible expandedRect, even though only the empty card renders. Use collapsedRect as the empty group’s effective expandedRect, or exclude empty groups from layout push sources.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/composables/useCanvasNodeGroupLayout.ts, line 97:

<comment>When an expanded group loses its last real node, this line collapses only `rect` while the view state still treats the group as expanded. The layout then moves nearby nodes around the invisible `expandedRect`, even though only the empty card renders. Use `collapsedRect` as the empty group’s effective `expandedRect`, or exclude empty groups from layout push sources.</comment>

<file context>
@@ -81,13 +84,17 @@ export function buildNodeGroupLayoutComponents({
 			nodeIds: [...group.nodeIds],
-			rect: isGroupCollapsed(group.id) ? collapsedRect : expandedRect,
+			// An empty group always renders as its card, whatever the view state says.
+			rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect,
 			collapsedRect,
 			expandedRect,
</file context>
Suggested change
rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect,
collapsedRect,
expandedRect,
rect: isEmpty || isGroupCollapsed(group.id) ? collapsedRect : expandedRect,
collapsedRect,
expandedRect: isEmpty ? collapsedRect : expandedRect,
Fix with cubic

{ node: placeholder.name, type: main, index: 0 },
]);
}
for (const outgoing of bySource[node.name]?.[main]?.flat() ?? []) {

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P1: When the deleted group member has multiple main input or output ports, this rewiring changes the workflow topology: distinct IF/Switch branches can both execute downstream, and separate Merge inputs are collapsed. Do not replace multi-port members with the single-port placeholder, or add matching ports and preserve each connection index.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts, line 623:

<comment>When the deleted group member has multiple main input or output ports, this rewiring changes the workflow topology: distinct IF/Switch branches can both execute downstream, and separate Merge inputs are collapsed. Do not replace multi-port members with the single-port placeholder, or add matching ports and preserve each connection index.</comment>

<file context>
@@ -559,6 +561,75 @@ export function useCanvasOperations() {
+				{ node: placeholder.name, type: main, index: 0 },
+			]);
+		}
+		for (const outgoing of bySource[node.name]?.[main]?.flat() ?? []) {
+			if (!outgoing) continue;
+			addPlaceholderConnection([
</file context>
Fix with cubic

.map((name) => store.getNodeByName(name)?.id)
.filter((id): id is string => Boolean(id));
store.addNodesToGroup(groupId, createdIds);
deleteNode(placeholder.id, { trackHistory: true });

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P1: When a connected empty group uses the mock generation path, deleting the placeholder reconnects its old neighbours after the new boundary edges were added. The generated chain therefore has an extra direct bypass from each predecessor to successor. Replace the placeholder through the existing grouped-node replacement operation, or remove it without connecting adjacent nodes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/views/NodeView.vue, line 1232:

<comment>When a connected empty group uses the mock generation path, deleting the placeholder reconnects its old neighbours after the new boundary edges were added. The generated chain therefore has an extra direct bypass from each predecessor to successor. Replace the placeholder through the existing grouped-node replacement operation, or remove it without connecting adjacent nodes.</comment>

<file context>
@@ -1043,6 +1081,157 @@ function onCreateSticky() {
+		.map((name) => store.getNodeByName(name)?.id)
+		.filter((id): id is string => Boolean(id));
+	store.addNodesToGroup(groupId, createdIds);
+	deleteNode(placeholder.id, { trackHistory: true });
+}
+
</file context>
Fix with cubic

disabled: isReadOnly.value,
},
{
id: 'add_empty_group',

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P2: Custom agent: Frontend

add_empty_group is untested, so regressions in this empty-canvas menu path or its read-only gating can ship despite the existing operation test. Add a useContextMenu test that asserts the item appears for an empty selection and is disabled in read-only mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/shared/contextMenu/composables/useContextMenuItems.ts, line 404:

<comment>`add_empty_group` is untested, so regressions in this empty-canvas menu path or its read-only gating can ship despite the existing operation test. Add a `useContextMenu` test that asserts the item appears for an empty selection and is disabled in read-only mode.</comment>

<file context>
@@ -454,10 +400,13 @@ export function useContextMenuItems(
 					disabled: isReadOnly.value,
 				},
+				{
+					id: 'add_empty_group',
+					label: i18n.baseText('contextMenu.addEmptyGroup'),
+					disabled: isReadOnly.value,
</file context>
Fix with cubic

(c): c is IConnection => Boolean(c),
);

const { uniqueNodeName } = useUniqueNodeName();

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P2: When mock generation runs from a scoped document, calling useUniqueNodeName() inside the event handler resolves the wrong document context because Vue has no active injection context after setup. Create this composable during setup and reuse it, or derive unique names from workflowDocumentStore directly.

(Based on your team's feedback about avoiding provide/inject outside the component tree.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/views/NodeView.vue, line 1159:

<comment>When mock generation runs from a scoped document, calling `useUniqueNodeName()` inside the event handler resolves the wrong document context because Vue has no active injection context after setup. Create this composable during setup and reuse it, or derive unique names from `workflowDocumentStore` directly.

(Based on your team's feedback about avoiding provide/inject outside the component tree.) </comment>

<file context>
@@ -1043,6 +1081,157 @@ function onCreateSticky() {
+		(c): c is IConnection => Boolean(c),
+	);
+
+	const { uniqueNodeName } = useUniqueNodeName();
+	const names = ['Extract', 'Transform', 'Load'].map((label) => uniqueNodeName(label));
+
</file context>
Fix with cubic

// is still empty — the placeholder is its only real member.
const connectableMembers = group.nodeIds
.map((id) => workflowDocumentNodes.getNodeById(id))
.filter((node) => node !== undefined && node.type !== STICKY_NODE_TYPE);

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P2: When a group still references an unresolved non-sticky node, this filter drops that member and classifies [placeholderId, staleId] as empty. Require every member ID to resolve before returning the placeholder, so stale groups are not collapsed or given empty-group chip operations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts, line 286:

<comment>When a group still references an unresolved non-sticky node, this filter drops that member and classifies `[placeholderId, staleId]` as empty. Require every member ID to resolve before returning the placeholder, so stale groups are not collapsed or given empty-group chip operations.</comment>

<file context>
@@ -274,6 +275,24 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
+			// is still empty — the placeholder is its only real member.
+			const connectableMembers = group.nodeIds
+				.map((id) => workflowDocumentNodes.getNodeById(id))
+				.filter((node) => node !== undefined && node.type !== STICKY_NODE_TYPE);
+			if (connectableMembers.length !== 1) return undefined;
+			const [node] = connectableMembers;
</file context>
Suggested change
.filter((node) => node !== undefined && node.type !== STICKY_NODE_TYPE);
.filter((node) => node === undefined || node.type !== STICKY_NODE_TYPE);
Fix with cubic

// A description can run to GROUP_DESCRIPTION_MAX_LENGTH; cap the editor
// so a long one scrolls instead of covering the canvas.
max-height: 40vh;
z-index: var.$index-popper;

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P2: When a filled group is expanded, a long description editor is painted inside the group’s -10000 VueFlow stacking context, so member nodes paint over the textarea and actions. Teleport the editor to the canvas popper layer or raise the group node while editing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue, line 883:

<comment>When a filled group is expanded, a long description editor is painted inside the group’s `-10000` VueFlow stacking context, so member nodes paint over the textarea and actions. Teleport the editor to the canvas popper layer or raise the group node while editing.</comment>

<file context>
@@ -849,39 +784,141 @@ function onWrapperPointerDown(event: PointerEvent) {
+	// A description can run to GROUP_DESCRIPTION_MAX_LENGTH; cap the editor
+	// so a long one scrolls instead of covering the canvas.
+	max-height: 40vh;
+	z-index: var.$index-popper;
+}
+
</file context>
Fix with cubic

) {
// Ungrouping an empty group would strand its placeholder as a bare node on
// the canvas. Every entry point (toolbar, shortcut, context menu) lands here.
if (workflowDocumentStore.value.isEmptyGroup(groupId)) return;

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P3: When an empty group is selected, Ctrl/Cmd+Shift+G remains enabled because canUngroupSelection only checks that a group is selected, but this guard silently returns. Exclude empty groups from the shortcut eligibility, or disable the shortcut when no non-empty group is selected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue, line 982:

<comment>When an empty group is selected, Ctrl/Cmd+Shift+G remains enabled because `canUngroupSelection` only checks that a group is selected, but this guard silently returns. Exclude empty groups from the shortcut eligibility, or disable the shortcut when no non-empty group is selected.</comment>

<file context>
@@ -960,6 +977,10 @@ function onCanvasGroupUngroup(
 ) {
+	// Ungrouping an empty group would strand its placeholder as a bare node on
+	// the canvas. Every entry point (toolbar, shortcut, context menu) lands here.
+	if (workflowDocumentStore.value.isEmptyGroup(groupId)) return;
+
 	// Capture before deletion — the group is gone by the time we track.
</file context>
Fix with cubic

$style.wrapper,
{
[$style.collapsed]: isCollapsed.value,
[$style.empty]: isEmptyGroup.value,

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

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.

P3: The new $style.empty and $style.descriptionEditing bindings have no matching module styles and add no useful state styling. Remove both unused references or add the corresponding module selectors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/editor-ui/src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.vue, line 112:

<comment>The new `$style.empty` and `$style.descriptionEditing` bindings have no matching module styles and add no useful state styling. Remove both unused references or add the corresponding module selectors.</comment>

<file context>
@@ -93,6 +109,8 @@ const wrapperClasses = computed(() => [
 	$style.wrapper,
 	{
 		[$style.collapsed]: isCollapsed.value,
+		[$style.empty]: isEmptyGroup.value,
+		[$style.readOnly]: props.readOnly,
 		[$style.selected]: props.selected,
</file context>
Fix with cubic

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
94730 3 94727 29
View the top 3 failed test(s) by shortest run time
src/modules/mcp/__tests__/workflow-operations.test.ts > applyOperations > setNodeGroups > schema rejects a description over the max length
Stack Traces | 0.00748s run time
AssertionError: expected true to be false // Object.is equality

- Expected
+ Received

- false
+ true

 ❯ .../mcp/__tests__/workflow-operations.test.ts:1206:27
src/features/workflows/canvas/components/elements/groups/CanvasNodeGroupTitleBar.test.ts > CanvasNodeGroupTitleBar > description > hides the description below the zoom threshold but keeps the header
Stack Traces | 0.0171s run time
AssertionError: expected <div …(2)><div …(2)></div></div> to be null

- Expected:
null

+ Received:
<div
  class="description nodrag"
  data-test-id="canvas-node-group-description"
>
  <!-- The editor overlays the card so a long objective can grow while
					the card keeps the height the mapping gave it. -->
  <div
    class="descriptionText twoLines"
    data-test-id="canvas-node-group-description-text"
  >
    x
  </div>
</div>

 ❯ .../elements/groups/CanvasNodeGroupTitleBar.test.ts:368:67
src/modules/n8n-packages/__tests__/workflow-node-groups.integration.test.ts > workflow package import — node groups > truncates an over-long group description instead of rejecting the package
Stack Traces | 0.0897s run time
AssertionError: expected 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx…' to have a length of 1000 but got 200

- Expected
+ Received

- 1000
+ 200

 ❯ .../n8n-packages/__tests__/workflow-node-groups.integration.test.ts:224:46

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed n8n team Authored by the n8n team node/new Creation of an entirely new node

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant