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
- Provide text as a plain string on every note, even if empty ('')
- Stringify structured content (join lines with \n, serialize objects) before passing
- 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
- Always set text as a plain string, using \n for multi-line content
- Convert structured content to a string before building the note
- Use '' rather than null for intentionally empty notes
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
- Flow notes must be an array
- Invalid note at index ${index}: must be an object
- Invalid note at index ${index}: id must be a non-empty strin
- Invalid note at index ${index}: duplicate note id "${n.id}"
- Invalid note at index ${index}: type must be "free" or "grou
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/bfe48738dcf9e6ff.
Report an issue: GitHub.