windmill-labs/windmill · error

Special modules must be provided via preprocessor_module and

Error message

Special modules must be provided via preprocessor_module and failure_module, not inside modules

What it means

As the final structural check in validateFlowModules, the collected module IDs are scanned for the reserved preprocessor/failure special IDs. If any module inside the modules array uses one of those reserved IDs, this error is thrown: special modules must be delivered through the dedicated preprocessor_module / failure_module fields of the flow payload, never inline in modules.

Source

Thrown at frontend/src/lib/components/copilot/chat/flow/editableFlowJson.ts:263

			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}"`)
				.join(
					', '
				)} need a provider input transform, or an "agent" path linking them to a saved agent`
		)
	}

	// An agent tool's `summary` is the name the LLM sees; the worker rejects anything outside

View on GitHub (pinned to e474e8803c)

Solutions

  1. Extract the module with the reserved id from modules and pass it as preprocessor_module (for the preprocessor id) or failure_module (for the failure id).
  2. If the special module is unchanged, omit it from the modules payload entirely and only supply the dedicated field.
  3. Derive the payload from buildEditableFlowJson output, which already separates special modules from the modules array.

Example fix

// before
validateFlowModules([...mods, { id: 'failure', value: {...} }])
// after
setFlowJson({ modules: mods, failure_module: { id: 'failure', value: {...} } })
Defensive patterns

Strategy: validation

Validate before calling

const RESERVED = ['preprocessor', 'failure']
const offenders = modules.filter((m) => RESERVED.includes(m.id))
if (offenders.length) {
  throw new Error('Move reserved modules to preprocessor_module/failure_module fields')
}

Type guard

function hasReservedIdsInline(modules: FlowModule[]): boolean {
  return modules.some((m) => m.id === 'preprocessor' || m.id === 'failure')
}

Try / catch

try {
  validateFlowModules(modules)
} catch (e) {
  if (e.message.includes('Special modules must be provided via')) {
    const { modules: rest, pre, fail } = splitSpecialModules(modules)
    return setFlowJson({ modules: rest, preprocessor_module: pre, failure_module: fail })
  } else throw e
}

Prevention

When it happens

Trigger: validateFlowModules (via flowTools or the 'modules' setter) receiving a modules array that contains a module with id === SPECIAL_MODULE_IDS.PREPROCESSOR or SPECIAL_MODULE_IDS.FAILURE, instead of passing it through the preprocessor_module/failure_module arguments.

Common situations: Echoing a raw backend flow payload where special modules sit in the modules list; porting flow JSON from exports/API responses into the editable representation without lifting the special modules into their dedicated fields.

Related errors


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