{"record":{"id":"7414a67ad4521f1d","repo":"windmill-labs/windmill","slug":"invalid-group-at-index-index-must-be-an-object","errorCode":null,"errorMessage":"Invalid group at index ${index}: must be an object","messagePattern":"Invalid group at index (.+?): must be an object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/copilot/chat/flow/helperUtils.ts","lineNumber":98,"sourceCode":"\trawScriptModule.value.content = code\n\treturn rawScriptModule\n}\n\nexport function validateFlowGroups(\n\trawGroups: unknown,\n\tmoduleIds?: Set<string>\n): FlowGroup[] | null {\n\tif (rawGroups == null) {\n\t\treturn null\n\t}\n\n\tif (!Array.isArray(rawGroups)) {\n\t\tthrow new Error('Flow groups must be an array')\n\t}\n\n\treturn rawGroups.map((group, index) => {\n\t\tif (!group || typeof group !== 'object' || Array.isArray(group)) {\n\t\t\tthrow new Error(`Invalid group at index ${index}: must be an object`)\n\t\t}\n\t\tconst g = group as Record<string, unknown>\n\t\tif (typeof g.start_id !== 'string' || !g.start_id) {\n\t\t\tthrow new Error(`Invalid group at index ${index}: start_id must be a non-empty string`)\n\t\t}\n\t\tif (typeof g.end_id !== 'string' || !g.end_id) {\n\t\t\tthrow new Error(`Invalid group at index ${index}: end_id must be a non-empty string`)\n\t\t}\n\t\tif (moduleIds) {\n\t\t\tif (!moduleIds.has(g.start_id)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Invalid group at index ${index}: start_id \"${g.start_id}\" does not match any flow module`\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (!moduleIds.has(g.end_id)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Invalid group at index ${index}: end_id \"${g.end_id}\" does not match any flow module`\n\t\t\t\t)","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts#L80-L116","documentation":"Each entry of the flow groups array must be a plain object describing a note box (start_id, end_id, optional summary/color). validateFlowGroups rejects null, non-object, or array elements at the given index so downstream code can safely read group fields.","triggerScenarios":"Calling validateFlowGroups with an array containing null, a primitive (e.g. 'group1', 42), or a nested array at that index — e.g. groups: [null] or groups: [['a','b']].","commonSituations":"LLM emitting a list of id pairs instead of group objects; JSON with empty slots; mapping over module ids directly and passing the result as groups.","solutions":["Replace the invalid element with a group object: { start_id: '<module id>', end_id: '<module id>' }","Remove the null/primitive entry from the array","If ids-only was intended, restructure as objects with start_id/end_id"],"exampleFix":"// before\ngroups: ['a', 'b']\n// after\ngroups: [{ start_id: 'a', end_id: 'b' }]","handlingStrategy":"type-guard","validationCode":"const bad = (groups ?? []).findIndex(g => g == null || typeof g !== 'object' || Array.isArray(g))\nif (bad !== -1) throw new Error(`groups[${bad}] must be an object`)","typeGuard":"function isFlowGroup(g) {\n  return typeof g === 'object' && g !== null && !Array.isArray(g)\n}","tryCatchPattern":"try {\n  const groups = validateFlowGroups(raw, moduleIds)\n} catch (e) {\n  if (e.message.includes('must be an object')) {\n    const idx = Number(e.message.match(/index (\\d+)/)?.[1])\n    sanitized.splice(idx, 1) // drop the bad entry and retry\n    return validateFlowGroups(sanitized, moduleIds)\n  }\n  throw e\n}","preventionTips":["Filter out null/undefined entries before validating groups","Never pass raw id lists as groups; always build { start_id, end_id } objects","Validate each group object individually when constructing it"],"tags":["validation","flow-groups","type-error"],"backgroundTag":"schema-validation-failed","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"}