n8n-io/n8n · error · Error
Reflector merge[${index}].parentId must be a string or null
Error message
Reflector merge[${index}].parentId must be a string or null What it means
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.
Source
Thrown at packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts:343
switch (value.toUpperCase()) {
case 'CRITICAL':
return 'critical';
case 'IMPORTANT':
return 'important';
case 'INFO':
return 'info';
case 'COMPLETION':
return 'completion';
default:
throw new Error(`Reflector merge[${index}].marker must be a known observation marker`);
}
}
function readOptionalParentId(value: unknown, index: number): string | null | undefined {
if (value === undefined) return undefined;
if (value === null || typeof value === 'string') return value;
throw new Error(`Reflector merge[${index}].parentId must be a string or null`);
}
function withCreatedAt(reflection: ObservationLogReflection, now: Date): ObservationLogReflection {
return {
drop: reflection.drop,
merge: reflection.merge.map((merge, index) => ({
...merge,
createdAt: merge.createdAt ?? new Date(now.getTime() + index),
})),
};
}
function countObservationTokens(entries: ObservationLogEntry[]): number {
return entries.reduce((total, entry) => total + getStoredObservationTokenCount(entry), 0);
}
function compareEntries(a: ObservationLogEntry, b: ObservationLogEntry): number {
const timeDiff = a.createdAt.getTime() - b.createdAt.getTime();View on GitHub (pinned to 5ac6606e81)
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.
Example fix
// before: { "merge": [{ "supersedes": ["obs-1"], "marker": "INFO", "text": "...", "parentId": 5 }] }
// after: { "merge": [{ "supersedes": ["obs-1"], "marker": "INFO", "text": "...", "parentId": "obs-5" }] } Defensive patterns
Strategy: type-guard
Validate before calling
const raw = JSON.parse(extractJsonObject(output));
if (Array.isArray(raw.merge)) {
for (const m of raw.merge) {
if (isRecord(m)) {
if (m.parentId !== undefined && m.parentId !== null && typeof m.parentId !== 'string') {
m.parentId = String(m.parentId); // coerce numbers to strings
}
}
}
} Type guard
function isValidParentId(value: unknown): value is string | null | undefined {
return value === undefined || value === null || typeof value === 'string';
} Try / catch
try {
const reflection = parseObservationLogReflectionJson(output);
} catch (e) {
logger.warn('Reflector parentId invalid type', { index: 'see message', output });
} Prevention
- 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.
When it happens
Trigger: The reflector LLM returns `{ ..., "parentId": 42 }` (numeric parent ID) or `{ ..., "parentId": { "id": "obs-1" } }` (wrapped object).
Common situations: 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.
Related errors
- Reflector field "${fieldName}" must contain only strings
- Reflector merge[${index}] must be an object
- Reflector merge[${index}].marker must be a known observation
- Reflector field "${fieldName}" must be an array
- Reflector field "merge" must be an array
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/2b5445d4eefe52ef.
Report an issue: GitHub.