windmill-labs/windmill · error

Group '${g.id}' references virtual node: groups cannot inclu

Error message

Group '${g.id}' references virtual node: groups cannot include Input, Result, or Trigger

What it means

buildStructureTreeRecurse also rejects groups whose start_id or end_id is a virtual node id (Input, Result, Trigger). Virtual nodes are graph scaffolding, not real flow modules, so they cannot be members of a branch group.

Source

Thrown at frontend/src/lib/components/graph/flowStructure.ts:91

	const indexMap = new Map<string, number>()
	for (let i = 0; i < modules.length; i++) {
		indexMap.set(modules[i].id, i)
	}

	// Reject duplicate group IDs
	const seenGroupIds = new Set<string>()
	for (const g of groups) {
		if (seenGroupIds.has(g.id)) {
			throw new Error(`Duplicate group id: '${g.id}'`)
		}
		seenGroupIds.add(g.id)
	}

	// Reject groups referencing virtual nodes
	for (const g of groups) {
		if (VIRTUAL_NODE_IDS.has(g.start_id) || VIRTUAL_NODE_IDS.has(g.end_id)) {
			throw new Error(
				`Group '${g.id}' references virtual node: groups cannot include Input, Result, or Trigger`
			)
		}
	}

	// Partition: groups for this level vs rest
	const levelGroups: GraphGroup[] = []
	const otherGroups: GraphGroup[] = []
	for (const g of groups) {
		if (indexMap.has(g.start_id) && indexMap.has(g.end_id)) {
			const s = indexMap.get(g.start_id)!
			const e = indexMap.get(g.end_id)!
			if (s > e) {
				throw new Error(
					`Group '${g.id}' has inverted range: start_id='${g.start_id}' (index ${s}) > end_id='${g.end_id}' (index ${e})`
				)
			}
			levelGroups.push(g)

View on GitHub (pinned to e474e8803c)

Solutions

  1. Filter virtual node ids (VIRTUAL_NODE_IDS) out of selections before constructing groups.
  2. When creating groups from edges, skip edges incident to Input/Result/Trigger nodes.
  3. Sanitize stored groups on load by dropping any group referencing virtual nodes.

Example fix

// before
const group = { id, start_id: sel[0].id, end_id: sel[1].id }
// after
const real = sel.filter((n) => !VIRTUAL_NODE_IDS.has(n.id))
if (real.length !== sel.length) return // or re-select
const group = { id, start_id: real[0].id, end_id: real[real.length - 1].id }
Defensive patterns

Strategy: validation

Validate before calling

const safeGroups = groups.filter(
  (g) => !VIRTUAL_NODE_IDS.has(g.start_id) && !VIRTUAL_NODE_IDS.has(g.end_id)
)
const tree = buildStructureTree(modules, safeGroups)

Type guard

function groupsExcludeVirtualNodes(groups: GraphGroup[]): boolean {
  return groups.every((g) => !VIRTUAL_NODE_IDS.has(g.start_id) && !VIRTUAL_NODE_IDS.has(g.end_id))
}

Try / catch

try {
  const tree = buildStructureTree(modules, groups)
} catch (e) {
  if (e.message.includes('virtual node')) {
    return buildStructureTree(modules, groups.filter(nonVirtual))
  }
  throw e
}

Prevention

When it happens

Trigger: Creating a group from a graph selection that includes the Input, Result, or Trigger boundary nodes, then passing it to buildStructureTree via buildStructureTreeRecurse.

Common situations: Select-all in a flow graph editor capturing boundary nodes; drag-select rectangles that overlap virtual node handles; programmatic group creation using edge endpoints that include virtual nodes.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/b342c076435bffc1. Report an issue: GitHub.