{"record":{"id":"78de8edc494fad09","repo":"n8n-io/n8n","slug":"reflector-field-fieldname-must-contain-only-s","errorCode":null,"errorMessage":"Reflector field \"${fieldName}\" must contain only strings","messagePattern":"Reflector field \"(.+?)\" must contain only strings","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts","lineNumber":292,"sourceCode":"\tif (parentId === undefined || parentId === null) return parentId;\n\treturn activeById.has(parentId) && !removedIds.has(parentId) ? parentId : null;\n}\n\nfunction extractJsonObject(output: string): string {\n\tconst start = output.indexOf('{');\n\tconst end = output.lastIndexOf('}');\n\tif (start === -1 || end === -1 || end < start) {\n\t\tthrow new Error('Reflector output did not contain a JSON object');\n\t}\n\treturn output.slice(start, end + 1);\n}\n\nfunction readStringArray(value: unknown, fieldName: string): string[] {\n\tif (!Array.isArray(value)) throw new Error(`Reflector field \"${fieldName}\" must be an array`);\n\tconst strings: string[] = [];\n\tfor (const item of value) {\n\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}","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts#L274-L310","documentation":"After `readStringArray` confirms a value is an array, it iterates every element and asserts each is a `typeof string`. This throws when the array contains non-string elements (numbers, booleans, nested objects, null). The reflector contract requires every element of `drop` and `merge[i].supersedes` to be a plain string.","triggerScenarios":"The reflector LLM returns `{ \"drop\": [123, \"obs-2\"] }` (mixed number and string) or `{ \"merge\": [{ \"supersedes\": [null, \"id1\"] }] }` (null mixed in).","commonSituations":"The model confused numeric observation IDs with string IDs. The model emitted `null` placeholders for entries it could not supersede. A schema-drift where the reflector prompt changed the expected ID format.","solutions":["Log the raw LLM output and identify which array element is non-string (the error names the field, e.g. `merge[0].supersedes`).","Fix the reflector prompt to clarify that all IDs are strings and `null` must not appear inside arrays — omit the entry instead.","If observation IDs in your system are numeric, ensure the reflector input renders them as quoted strings so the model echoes them back as strings.","Consider enabling strict JSON-schema constrained decoding (structured outputs) on providers that support it so the array element type is enforced at the API level."],"exampleFix":"// before: LLM returns { \"drop\": [42, \"obs-2\"] }\n// Ensure the rendered observation log the reflector sees uses string IDs:\n//   [obs-42] some observation text\n// so the model echoes \"obs-42\" not 42.\n// after: { \"drop\": [\"obs-42\", \"obs-2\"] }","handlingStrategy":"validation","validationCode":"function isStringArray(value: unknown): value is string[] {\n  return Array.isArray(value) && value.every((item) => typeof item === 'string');\n}\n\n// Pre-check before parseObservationLogReflectionJson:\nconst raw = JSON.parse(extractJsonObject(output));\nfor (const field of ['drop'] as const) {\n  if (raw[field] !== undefined && !isStringArray(raw[field])) {\n    throw new Error(`Pre-validation: ${field} contains non-string elements`);\n  }\n}","typeGuard":"function isStringArray(value: unknown): value is string[] {\n  return Array.isArray(value) && value.every((item) => typeof item === 'string');\n}","tryCatchPattern":"try {\n  const reflection = parseObservationLogReflectionJson(output);\n} catch (e) {\n  logger.warn('Reflector array element type error', { field: 'see message', output });\n  // skip reflection cycle\n}","preventionTips":["Render all observation IDs as quoted strings in the reflector input so the LLM echoes strings.","Use structured-output mode with `string[]` array element constraints.","Treat reflection failures as non-fatal — wrap in try/catch and continue the run."],"tags":["llm-output-validation","observation-log","json-parsing","type-mismatch"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}