windmill-labs/windmill · error
Duplicate module IDs found in flow
Error message
Duplicate module IDs found in flow
What it means
Inside validateFlowModules, after schema validation succeeds, all module IDs (collected recursively, including nested branch/loop children via collectAllFlowModuleIdsFromModules) are checked for uniqueness with a Set. Any repetition throws 'Duplicate module IDs found in flow', guarding the graph invariant that every step is uniquely addressable.
Source
Thrown at frontend/src/lib/components/copilot/chat/flow/editableFlowJson.ts:256
const expectedFormat = getExpectedFormat(targetSchema)
if (expectedFormat) {
message += `\n Expected format: ${expectedFormat}`
}
}
}
if (moduleId) {
return `Module "${moduleId}" -> ${fieldPath}: ${message}`
}
return `${path.join('.')}: ${message}`
})
throw new Error(`Invalid flow modules:\n${errors.join('\n')}`)
}
const ids = collectAllFlowModuleIdsFromModules(parsedModules)
if (ids.length !== new Set(ids).size) {
throw new Error('Duplicate module IDs found in flow')
}
const reservedIds = ids.filter(
(id) => id === SPECIAL_MODULE_IDS.PREPROCESSOR || id === SPECIAL_MODULE_IDS.FAILURE
)
if (reservedIds.length > 0) {
throw new Error(
'Special modules must be provided via preprocessor_module and failure_module, not inside modules'
)
}
// Not expressible in the schema: `provider` is required only when the step is standalone, and
// making AiAgent a conditional union breaks the FlowModuleValue discriminated union it belongs to.
const providerless = collectProviderlessAgentIds(parsedModules)
if (providerless.length > 0) {
throw new Error(
`AI agent modules ${providerless
.map((id) => `"${id}"`)View on GitHub (pinned to e474e8803c)
Solutions
- Rename every duplicate to a fresh unique ID and re-validate.
- Remember IDs are checked recursively — inspect nested children inside for/branch-all/branch-one modules too.
- De-duplicate programmatically before submitting (e.g. walk the tree and assert Set(ids).size === ids.length).
Example fix
// before: same id in two branch arms
[{ id: 'a', value: { type: 'branchone', ... } }] // both arms contain { id: 'x' }
// after: rename one occurrence
arms[0].modules[0].id = 'x'; arms[1].modules[0].id = 'x2' Defensive patterns
Strategy: validation
Validate before calling
import { collectAllFlowModuleIdsFromModules } from './editableFlowJson'
const ids = collectAllFlowModuleIdsFromModules(modules)
if (new Set(ids).size !== ids.length) throw new Error('Duplicate module IDs (including nested children)') Type guard
function allIdsUnique(modules: FlowModule[]): boolean {
const ids = collectAllFlowModuleIdsFromModules(modules)
return new Set(ids).size === ids.length
} Try / catch
try {
await setFlow({ modules })
} catch (e) {
if (e.message === 'Duplicate module IDs found in flow') {
await setFlow(renameDuplicatesDeep(modules)) // rename includes nested branch/loop children
} else throw e
} Prevention
- Rename IDs on every copy, including nested children inside branch/for modules
- Check ID uniqueness recursively, not just at the top level
- Generate IDs from a unique-id helper rather than hand-writing them
When it happens
Trigger: validateFlowModules called with a modules array where the same id occurs twice at any depth — two siblings, a parent and nested child, or duplicates across branch arms.
Common situations: Duplicating a branch arm without renaming child IDs; template concatenation appending an already-present module; AI regeneration reusing a previous step's ID for a new step.
Related errors
- Flow modules must be an array
- Invalid flow modules:\n${errors.join('\n')}
- Special modules must be provided via preprocessor_module and
- result.substring(__RESULT_ERR_PREFIX.length)
- Invalid migration name '${name}': use only letters, digits,
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/3757227883af68eb.
Report an issue: GitHub.