{"record":{"id":"2b5445d4eefe52ef","repo":"n8n-io/n8n","slug":"reflector-merge-index-parentid-must-be-a-strin","errorCode":null,"errorMessage":"Reflector merge[${index}].parentId must be a string or null","messagePattern":"Reflector merge\\[(.+?)\\]\\.parentId must be a string or null","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts","lineNumber":343,"sourceCode":"\n\tswitch (value.toUpperCase()) {\n\t\tcase 'CRITICAL':\n\t\t\treturn 'critical';\n\t\tcase 'IMPORTANT':\n\t\t\treturn 'important';\n\t\tcase 'INFO':\n\t\t\treturn 'info';\n\t\tcase 'COMPLETION':\n\t\t\treturn 'completion';\n\t\tdefault:\n\t\t\tthrow new Error(`Reflector merge[${index}].marker must be a known observation marker`);\n\t}\n}\n\nfunction readOptionalParentId(value: unknown, index: number): string | null | undefined {\n\tif (value === undefined) return undefined;\n\tif (value === null || typeof value === 'string') return value;\n\tthrow new Error(`Reflector merge[${index}].parentId must be a string or null`);\n}\n\nfunction withCreatedAt(reflection: ObservationLogReflection, now: Date): ObservationLogReflection {\n\treturn {\n\t\tdrop: reflection.drop,\n\t\tmerge: reflection.merge.map((merge, index) => ({\n\t\t\t...merge,\n\t\t\tcreatedAt: merge.createdAt ?? new Date(now.getTime() + index),\n\t\t})),\n\t};\n}\n\nfunction countObservationTokens(entries: ObservationLogEntry[]): number {\n\treturn entries.reduce((total, entry) => total + getStoredObservationTokenCount(entry), 0);\n}\n\nfunction compareEntries(a: ObservationLogEntry, b: ObservationLogEntry): number {\n\tconst timeDiff = a.createdAt.getTime() - b.createdAt.getTime();","sourceCodeStart":325,"sourceCodeEnd":361,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts#L325-L361","documentation":"Each merge object may optionally carry `parentId` to attach the merged observation under a specific parent in the log tree. `readOptionalParentId` allows `undefined` (field absent), `null` (no parent / root), or a `string` (parent ID). It throws for any other type — numbers, booleans, nested objects, arrays.","triggerScenarios":"The reflector LLM returns `{ ..., \"parentId\": 42 }` (numeric parent ID) or `{ ..., \"parentId\": { \"id\": \"obs-1\" } }` (wrapped object).","commonSituations":"The model echoed a numeric ID from the observation log instead of a string. The model wrapped the parent reference in an object. The observation log rendering exposed numeric IDs and the model mirrored them.","solutions":["Use the `index` from the error to locate the offending `parentId`.","Ensure the rendered observation log the reflector sees uses string IDs throughout so the model echoes strings.","Update the prompt: `parentId` is optional; if present it must be a string ID or `null` for root-level.","If your system genuinely uses numeric IDs, pre-normalize them to strings before passing to the reflector."],"exampleFix":"// before: { \"merge\": [{ \"supersedes\": [\"obs-1\"], \"marker\": \"INFO\", \"text\": \"...\", \"parentId\": 5 }] }\n// after:  { \"merge\": [{ \"supersedes\": [\"obs-1\"], \"marker\": \"INFO\", \"text\": \"...\", \"parentId\": \"obs-5\" }] }","handlingStrategy":"type-guard","validationCode":"const raw = JSON.parse(extractJsonObject(output));\nif (Array.isArray(raw.merge)) {\n  for (const m of raw.merge) {\n    if (isRecord(m)) {\n      if (m.parentId !== undefined && m.parentId !== null && typeof m.parentId !== 'string') {\n        m.parentId = String(m.parentId); // coerce numbers to strings\n      }\n    }\n  }\n}","typeGuard":"function isValidParentId(value: unknown): value is string | null | undefined {\n  return value === undefined || value === null || typeof value === 'string';\n}","tryCatchPattern":"try {\n  const reflection = parseObservationLogReflectionJson(output);\n} catch (e) {\n  logger.warn('Reflector parentId invalid type', { index: 'see message', output });\n}","preventionTips":["Render all parent IDs as strings in the reflector input.","Coerce numeric parentIds to strings in a pre-normalization step.","Use structured-output mode with `parentId: string | null` constraint."],"tags":["llm-output-validation","observation-log","json-parsing","type-mismatch","optional-field"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}