windmill-labs/windmill · error · Error

Invalid note at index ${index}: text must be a string

Error message

Invalid note at index ${index}: text must be a string

What it means

A note's text field holds the sticky-note content and must be a string (empty string allowed). validateFlowNotes throws this error when text is missing or of another type, since the renderer and editor operate on plain string content.

Source

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

	// 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`)
		}
		if (seenIds.has(n.id)) {
			throw new Error(`Invalid note at index ${index}: duplicate note id "${n.id}"`)
		}
		seenIds.add(n.id)
		if (typeof n.text !== 'string') {
			throw new Error(`Invalid note at index ${index}: text must be a string`)
		}
		const type = n.type ?? 'free'
		if (type !== 'free' && type !== 'group') {
			throw new Error(`Invalid note at index ${index}: type must be "free" or "group"`)
		}
		if (n.color !== undefined && n.color !== null) {
			if (typeof n.color !== 'string' || !ALLOWED_NOTE_COLORS.has(n.color)) {
				throw new Error(
					`Invalid note at index ${index}: color must be one of ${[...ALLOWED_NOTE_COLORS].join(', ')}`
				)
			}
		}
		if (n.position !== undefined && n.position !== null) {
			const p = n.position as Record<string, unknown>
			if (
				typeof p !== 'object' ||
				Array.isArray(n.position) ||
				typeof p.x !== 'number' ||

View on GitHub (pinned to e474e8803c)

Solutions

  1. Provide text as a plain string on every note, even if empty ('')
  2. Stringify structured content (join lines with \n, serialize objects) before passing
  3. Convert numbers/booleans with String(...) if the content is scalar

Example fix

// before
notes: [{ id: 'n1', text: ['line1', 'line2'] }]
// after
notes: [{ id: 'n1', text: 'line1\nline2' }]
Defensive patterns

Strategy: validation

Validate before calling

notes.forEach((n, i) => {
  if (typeof n.text !== 'string') throw new Error(`notes[${i}].text must be a string`);
});

Type guard

function hasNoteText(n: unknown): n is { text: string } & Record<string, unknown> {
  return typeof (n as any)?.text === 'string';
}

Try / catch

try {
  validateFlowNotes(rawNotes, moduleIds);
} catch (e) {
  if (e instanceof Error && e.message.includes('text must be a string')) {
    // stringify offending text fields and retry
  } else throw e;
}

Prevention

When it happens

Trigger: notes: [{ id: 'n1' }] (text missing), notes: [{ id: 'n1', text: 123 }], or notes: [{ id: 'n1', text: { body: 'hi' } }] passed to patch_flow_json/set_flow_json.

Common situations: An LLM agent puts rich structured content (markdown object, array of lines) into text; a caller passes null for empty notes instead of ''; numeric or boolean content was passed unconverted.

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/bfe48738dcf9e6ff. Report an issue: GitHub.