{"record":{"id":"c4c029bca8291e30","repo":"windmill-labs/windmill","slug":"duplicate-group-id-g-id","errorCode":null,"errorMessage":"Duplicate group id: '${g.id}'","messagePattern":"Duplicate group id: '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/graph/flowStructure.ts","lineNumber":83,"sourceCode":"\nfunction buildStructureTreeRecurse(\n\tmodules: FlowModule[],\n\tgroups: GraphGroup[]\n): { items: FlowStructureNode[]; consumed: Set<string> } {\n\tif (modules.length === 0) {\n\t\treturn { items: [], consumed: new Set() }\n\t}\n\n\tconst indexMap = new Map<string, number>()\n\tfor (let i = 0; i < modules.length; i++) {\n\t\tindexMap.set(modules[i].id, i)\n\t}\n\n\t// Reject duplicate group IDs\n\tconst seenGroupIds = new Set<string>()\n\tfor (const g of groups) {\n\t\tif (seenGroupIds.has(g.id)) {\n\t\t\tthrow new Error(`Duplicate group id: '${g.id}'`)\n\t\t}\n\t\tseenGroupIds.add(g.id)\n\t}\n\n\t// Reject groups referencing virtual nodes\n\tfor (const g of groups) {\n\t\tif (VIRTUAL_NODE_IDS.has(g.start_id) || VIRTUAL_NODE_IDS.has(g.end_id)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Group '${g.id}' references virtual node: groups cannot include Input, Result, or Trigger`\n\t\t\t)\n\t\t}\n\t}\n\n\t// Partition: groups for this level vs rest\n\tconst levelGroups: GraphGroup[] = []\n\tconst otherGroups: GraphGroup[] = []\n\tfor (const g of groups) {\n\t\tif (indexMap.has(g.start_id) && indexMap.has(g.end_id)) {","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/graph/flowStructure.ts#L65-L101","documentation":"buildStructureTreeRecurse validates the group list before partitioning nodes, rejecting duplicate group ids because groups are consumed and tracked by id — duplicates would corrupt the consumption bookkeeping and produce ambiguous structure trees.","triggerScenarios":"Passing a GraphGroup array containing two entries with the same id, e.g., from a buggy merge of group sets, a re-selection that appends instead of replaces, or data loaded twice from storage.","commonSituations":"Group list built by concatenating saved groups with newly created ones without deduplication; id-generation collisions from a faulty uuid source; importing flow state that already contains the group being added.","solutions":["Deduplicate groups by id before calling (e.g., new Map(groups.map(g => [g.id, g])).values()).","Replace existing groups with the same id on selection instead of appending.","Ensure group ids are generated from a reliable unique source (crypto.randomUUID or equivalent)."],"exampleFix":"// before\nconst tree = buildStructureTree(modules, [...savedGroups, newGroup])\n// after\nconst merged = new Map([...savedGroups, newGroup].map((g) => [g.id, g]))\nconst tree = buildStructureTree(modules, [...merged.values()])","handlingStrategy":"validation","validationCode":"const uniqueGroups = [...new Map(groups.map((g) => [g.id, g])).values()]\nconst tree = buildStructureTree(modules, uniqueGroups)","typeGuard":"function hasUniqueGroupIds(groups: GraphGroup[]): boolean {\n  return new Set(groups.map((g) => g.id)).size === groups.length\n}","tryCatchPattern":"try {\n  const tree = buildStructureTree(modules, groups)\n} catch (e) {\n  if (e.message.includes('Duplicate group id')) {\n    return buildStructureTree(modules, dedupeById(groups))\n  }\n  throw e\n}","preventionTips":["Merge group lists by id (upsert) instead of concatenating.","Generate group ids with a reliable unique source.","Deduplicate groups loaded from persisted state."],"tags":["svelte","flow-graph","duplicate-id"],"backgroundTag":"duplicate-identifier","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}