windmill-labs/windmill · error · Error

Flow notes must be an array

Error message

Flow notes must be an array

What it means

validateFlowNotes validates the optional `notes` array attached to a flow update. If rawNotes is neither null/undefined nor an array (e.g. an object or string was passed), this error is thrown. It enforces the FlowNote[] shape the note editor expects before notes are placed on the canvas.

Source

Thrown at frontend/src/lib/components/copilot/chat/flow/helperUtils.ts:156

 * `set_flow_json` rather than being rejected. When `moduleIds` is provided,
 * every `contained_node_ids` entry of a `group` note must reference an existing
 * module.
 *
 * A provided palette `color` is always preserved as-is; the default is only
 * filled in when a note omits `color` entirely (FlowNote.color is required).
 *
 * Free notes are also given a concrete `position` and `size` when missing. A
 * free note without geometry is not draggable/resizable in the editor (you'd
 * have to resize it first to give it a size) — UI-created notes always set both,
 * so agent-created notes must too. Provided geometry is preserved untouched.
 */
export function validateFlowNotes(rawNotes: unknown, moduleIds?: Set<string>): FlowNote[] | null {
	if (rawNotes == null) {
		return null
	}

	if (!Array.isArray(rawNotes)) {
		throw new Error('Flow notes must be an array')
	}

	const seenIds = new Set<string>()
	// Column and running y-cursor for auto-placed free notes so consecutive ones
	// stack below each other by their actual heights instead of overlapping. A
	// preserved note (explicit geometry) sitting in this column also advances the
	// cursor, so a later auto-placed note doesn't land on top of it.
	const AUTO_STACK_X = -(MIN_NOTE_WIDTH + 100)
	const AUTO_STACK_GAP = 24
	let autoStackY = 0
	return rawNotes.map((note, index) => {
		if (!note || typeof note !== 'object' || Array.isArray(note)) {
			throw new Error(`Invalid note at index ${index}: must be an object`)
		}
		const n = note as Record<string, unknown>
		if (typeof n.id !== 'string' || !n.id) {
			throw new Error(`Invalid note at index ${index}: id must be a non-empty string`)
		}

View on GitHub (pinned to e474e8803c)

Solutions

  1. Wrap the note object(s) in an array: notes: [{ id: 'n1', text: '...' }]
  2. Pass null or omit notes entirely if there are no notes to set
  3. Ensure the argument is a JSON array before calling the tool

Example fix

// before
notes: { id: 'n1', text: 'Review this' }
// after
notes: [{ id: 'n1', text: 'Review this' }]
Defensive patterns

Strategy: type-guard

Validate before calling

if (notes != null && !Array.isArray(notes)) {
  notes = [notes]; // or throw before calling the tool
}

Type guard

function isFlowNoteArray(v: unknown): v is FlowNote[] {
  return v == null || (Array.isArray(v) && v.every((n) => n !== null && typeof n === 'object' && !Array.isArray(n)));
}

Try / catch

try {
  const notes = validateFlowNotes(rawNotes, moduleIds);
} catch (e) {
  if (e instanceof Error && e.message === 'Flow notes must be an array') {
    // coerce to [rawNotes] and retry once
  } else throw e;
}

Prevention

When it happens

Trigger: Calling patch_flow_json/set_flow_json with notes: { id: 'n1', text: 'hi' } (single object instead of array) or notes: 'some text'.

Common situations: An LLM agent emits a single note object instead of wrapping it in an array; a caller confuses notes with a map keyed by id; JSON was flattened by an intermediate serialization step.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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