{"record":{"id":"7f0c0ec6badf1a8c","repo":"n8n-io/n8n","slug":"reflector-merge-index-text-must-be-a-string","errorCode":null,"errorMessage":"Reflector merge[${index}].text must be a string","messagePattern":"Reflector merge\\[(.+?)\\]\\.text must be a string","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts","lineNumber":309,"sourceCode":"\t\tif (typeof item !== 'string') {\n\t\t\tthrow new Error(`Reflector field \"${fieldName}\" must contain only strings`);\n\t\t}\n\t\tstrings.push(item);\n\t}\n\treturn strings;\n}\n\nfunction readMergeArray(value: unknown): ObservationLogMerge[] {\n\tif (!Array.isArray(value)) throw new Error('Reflector field \"merge\" must be an array');\n\treturn value.map(readMerge);\n}\n\nfunction readMerge(value: unknown, index: number): ObservationLogMerge {\n\tif (!isRecord(value)) throw new Error(`Reflector merge[${index}] must be an object`);\n\tconst supersedes = readStringArray(value.supersedes, `merge[${index}].supersedes`);\n\tconst marker = readMarker(value.marker, index);\n\tif (typeof value.text !== 'string') {\n\t\tthrow new Error(`Reflector merge[${index}].text must be a string`);\n\t}\n\n\tconst parentId = readOptionalParentId(value.parentId, index);\n\treturn {\n\t\tsupersedes,\n\t\tmarker,\n\t\ttext: value.text,\n\t\t...(parentId !== undefined && { parentId }),\n\t};\n}\n\nfunction readMarker(value: unknown, index: number): ObservationLogMarker {\n\tif (typeof value !== 'string') {\n\t\tthrow new Error(`Reflector merge[${index}].marker must be a known observation marker`);\n\t}\n\n\tswitch (value.toUpperCase()) {\n\t\tcase 'CRITICAL':","sourceCodeStart":291,"sourceCodeEnd":327,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts#L291-L327","documentation":"Inside each merge object, `text` must be a string — it is the consolidated observation text that replaces the superseded entries. `readMerge` checks `typeof value.text !== 'string'` and throws when `text` is missing, null, a number, or any non-string. This is the payload field; without a valid string the merge is meaningless.","triggerScenarios":"The reflector LLM returns a merge object like `{ \"supersedes\": [\"obs-1\"], \"marker\": \"INFO\" }` (omitted `text`) or `{ ..., \"text\": null }` or `{ ..., \"text\": 123 }`.","commonSituations":"The model omitted `text` when it had nothing new to say. The model used `null` or empty to signal 'no replacement'. The prompt did not mark `text` as required.","solutions":["Check the `index` from the message to find which merge object lacks a string `text`.","Update the reflector prompt to mark `text` as required and instruct the model to omit the entire merge object (not just `text`) if it has no replacement text.","If the model genuinely has no text, it should not emit that merge entry at all — `merge` can be empty `[]`.","Enable structured-output mode to enforce `text: string` at the API level."],"exampleFix":"// before: { \"merge\": [{ \"supersedes\": [\"obs-1\"], \"marker\": \"INFO\", \"text\": null }] }\n// after:  { \"merge\": [{ \"supersedes\": [\"obs-1\"], \"marker\": \"INFO\", \"text\": \"Consolidated finding from obs-1\" }] }","handlingStrategy":"validation","validationCode":"const raw = JSON.parse(extractJsonObject(output));\nif (Array.isArray(raw.merge)) {\n  raw.merge = raw.merge.filter(\n    (m) => isRecord(m) && typeof m.text === 'string' && m.text.length > 0,\n  );\n}","typeGuard":"function hasValidText(value: unknown): value is { text: string } {\n  return typeof value === 'object' && value !== null &&\n    typeof (value as { text?: unknown }).text === 'string';\n}","tryCatchPattern":"try {\n  const reflection = parseObservationLogReflectionJson(output);\n} catch (e) {\n  logger.warn('Reflector merge text missing/not-string', { index: 'see message', output });\n}","preventionTips":["Filter merge objects that lack a valid `text` string before parsing.","Mark `text` as required in the reflector prompt and in any JSON schema sent to the model.","Use structured-output mode to enforce `text: string`."],"tags":["llm-output-validation","observation-log","json-parsing","required-field"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}