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
- Filter virtual node ids (VIRTUAL_NODE_IDS) out of selections before constructing groups.
- When creating groups from edges, skip edges incident to Input/Result/Trigger nodes.
- 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
- Filter VIRTUAL_NODE_IDS out of graph selections before group creation.
- Skip edges incident to Input/Result/Trigger when deriving groups from edges.
- Sanitize persisted groups on load by dropping virtual-node references.
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
- Group(s) ${ids} could not be resolved: their start/end nodes
- Duplicate group id: '${g.id}'
- PlanWriteRefusedError
- Can't create script from non-inline script
- no first step found
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/b342c076435bffc1.
Report an issue: GitHub.