windmill-labs/windmill · error

Flow JSON must be an object

Error message

Flow JSON must be an object

What it means

validateEditableFlowJson is the entry point for the copilot's flow JSON representation; it first requires the input to be a non-null, non-array object. Anything else — null, undefined, a bare array, a string, a number — cannot represent a flow and is rejected immediately before any field-level checks.

Source

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

	'modules',
	'schema',
	'preprocessor_module',
	'failure_module',
	'groups',
	'notes'
] as const

/**
 * Parse and validate a raw object as an `EditableFlowJson`. Validates module
 * shape, schema shape, optional special modules (with their reserved ids),
 * groups, top-level flow settings, and that no module ids collide.
 */
export function validateEditableFlowJson(
	rawFlow: unknown,
	ctx: FlowValidationContext = {}
): EditableFlowJson {
	if (!rawFlow || typeof rawFlow !== 'object' || Array.isArray(rawFlow)) {
		throw new Error('Flow JSON must be an object')
	}

	const flow = rawFlow as Record<string, unknown>

	// Reject unknown top-level keys: silently dropping them would make patch
	// tools report success for edits that never land on the flow.
	const allowedKeys = new Set<string>([
		...EDITABLE_FLOW_STRUCTURAL_KEYS,
		...FLOW_VALUE_SETTINGS_KEYS
	])
	const unknownKeys = Object.keys(flow).filter((key) => !allowedKeys.has(key))
	if (unknownKeys.length > 0) {
		throw new Error(
			`Unknown top-level flow key(s): ${unknownKeys.join(', ')}. Allowed keys: ${[...allowedKeys].join(', ')}`
		)
	}

	const settingsResult = flowValueSettingsSchema.safeParse(flow)

View on GitHub (pinned to e474e8803c)

Solutions

  1. Wrap the payload in an object: pass {"modules": [...], ...} rather than the modules array itself.
  2. JSON.parse the input first if it is a string.
  3. Handle null/undefined upstream and skip validation for a genuinely absent flow.

Example fix

// before
validateEditableFlowJson([{id:'m1',value:{...}}])
// after
validateEditableFlowJson({modules:[{id:'m1',value:{...}}]})
Defensive patterns

Strategy: type-guard

Validate before calling

function assertFlowObject(raw) {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
    throw new TypeError('Flow JSON must be a non-array object')
  return raw
}

Type guard

function isEditableFlowJson(v) {
  return typeof v === 'object' && v !== null && !Array.isArray(v) &&
    Array.isArray(v.modules)
}

Try / catch

try {
  const flow = validateEditableFlowJson(raw)
} catch (e) {
  if (String(e.message) === 'Flow JSON must be an object') {
    // wrap arrays in {modules: ...} or JSON.parse strings before retrying
  } else throw e
}

Prevention

When it happens

Trigger: Calling validateEditableFlowJson (via flowTools parsedFlow/patch tools) with null, undefined, an array of modules, or a JSON string instead of an object with `modules` etc.

Common situations: An LLM emits a bare array of modules instead of an object; JSON.parse produced a string; an upstream function returned null for a missing flow.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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