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

  1. Use the `index` from the error to locate the offending `parentId`.
  2. Ensure the rendered observation log the reflector sees uses string IDs throughout so the model echoes strings.
  3. Update the prompt: `parentId` is optional; if present it must be a string ID or `null` for root-level.
  4. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/2b5445d4eefe52ef. Report an issue: GitHub.