{"record":{"id":"a6ec42105be668c2","repo":"windmill-labs/windmill","slug":"group-g-id-has-inverted-range-start-id-g","errorCode":null,"errorMessage":"Group '${g.id}' has inverted range: start_id='${g.start_id}' (index ${s}) > end_id='${g.end_id}' (index ${e})","messagePattern":"Group '(.+?)' has inverted range: start_id='(.+?)' \\(index (.+?)\\) > end_id='(.+?)' \\(index (.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/graph/flowStructure.ts","lineNumber":105,"sourceCode":"\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)) {\n\t\t\tconst s = indexMap.get(g.start_id)!\n\t\t\tconst e = indexMap.get(g.end_id)!\n\t\t\tif (s > e) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Group '${g.id}' has inverted range: start_id='${g.start_id}' (index ${s}) > end_id='${g.end_id}' (index ${e})`\n\t\t\t\t)\n\t\t\t}\n\t\t\tlevelGroups.push(g)\n\t\t} else {\n\t\t\totherGroups.push(g)\n\t\t}\n\t}\n\n\t// Validate no partial overlaps\n\tfor (let i = 0; i < levelGroups.length; i++) {\n\t\tfor (let j = i + 1; j < levelGroups.length; j++) {\n\t\t\tconst a = levelGroups[i]\n\t\t\tconst b = levelGroups[j]\n\t\t\tconst aStart = indexMap.get(a.start_id)!\n\t\t\tconst aEnd = indexMap.get(a.end_id)!\n\t\t\tconst bStart = indexMap.get(b.start_id)!\n\t\t\tconst bEnd = indexMap.get(b.end_id)!","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/graph/flowStructure.ts#L87-L123","documentation":"buildStructureTreeRecurse lays out flow groups as [start_id, end_id] ranges over the ordered node index of the current level. It throws when a group's start node appears after its end node in that ordering, which means the group cannot be rendered as a nested rectangle. This guards the graph layout from inconsistent group metadata.","triggerScenarios":"A GraphGroup whose start_id resolves to a higher index in indexMap than its end_id — e.g. group data produced by the server or a migration lists the group's end step before its start step, or ids were edited so start/end are swapped.","commonSituations":"Hand-edited or API-patched flow group definitions; a step reordered/deleted in the editor while group ids were stored from an older structure; importing a flow JSON whose group ranges do not match the module order.","solutions":["Open the flow definition and fix the group so start_id points to the earlier step and end_id to the later one (swap them if inverted).","Re-create the offending group in the flow editor so ids are recomputed from the current step order.","Check for deleted/duplicated steps that shifted indices and update group start_id/end_id accordingly.","If produced by a script/import, regenerate group ids against the current flow version before deploying."],"exampleFix":"// before\n{ \"id\": \"g1\", \"start_id\": \"step3\", \"end_id\": \"step1\" }\n// after\n{ \"id\": \"g1\", \"start_id\": \"step1\", \"end_id\": \"step3\" }","handlingStrategy":"validation","validationCode":"const idx = id => indexMap.get(id)\nfor (const g of groups) {\n  const s = idx(g.start_id), e = idx(g.end_id)\n  if (s !== undefined && e !== undefined && s > e) console.warn('inverted group', g.id)\n}","typeGuard":"function hasValidRange(g, indexMap) {\n  const s = indexMap.get(g.start_id), e = indexMap.get(g.end_id)\n  return s !== undefined && e !== undefined && s <= e\n}","tryCatchPattern":"try { tree = buildStructureTree(items, groups) } catch (e) { if (String(e.message).includes('inverted range')) groups = groups.filter(hasValidRange.bind(null, indexMap)); else throw e }","preventionTips":["Always generate group start_id/end_id from the current step order, never store absolute assumptions","Validate group ranges after any import or merge of flow JSON","Re-derive groups when steps are reordered or deleted"],"tags":["graph","flow-structure","data-integrity"],"backgroundTag":"invalid-group-range","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"}