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

  1. Rename every duplicate to a fresh unique ID and re-validate.
  2. Remember IDs are checked recursively — inspect nested children inside for/branch-all/branch-one modules too.
  3. 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

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


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/3757227883af68eb. Report an issue: GitHub.