n8n-io/n8n · error · Error

Reflector merge[${index}] must be an object

Error message

Reflector merge[${index}] must be an object

What it means

Each element of the `merge` array must be a JSON object (record) with `supersedes`, `marker`, `text`, and optional `parentId` fields. `readMerge` calls `isRecord(value)` and throws if the element is not a plain object — for example a bare string, number, or null. The `index` in the message tells you which array position is malformed.

Source

Thrown at packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts:305

function readStringArray(value: unknown, fieldName: string): string[] {
	if (!Array.isArray(value)) throw new Error(`Reflector field "${fieldName}" must be an array`);
	const strings: string[] = [];
	for (const item of value) {
		if (typeof item !== 'string') {
			throw new Error(`Reflector field "${fieldName}" must contain only strings`);
		}
		strings.push(item);
	}
	return strings;
}

function readMergeArray(value: unknown): ObservationLogMerge[] {
	if (!Array.isArray(value)) throw new Error('Reflector field "merge" must be an array');
	return value.map(readMerge);
}

function readMerge(value: unknown, index: number): ObservationLogMerge {
	if (!isRecord(value)) throw new Error(`Reflector merge[${index}] must be an object`);
	const supersedes = readStringArray(value.supersedes, `merge[${index}].supersedes`);
	const marker = readMarker(value.marker, index);
	if (typeof value.text !== 'string') {
		throw new Error(`Reflector merge[${index}].text must be a string`);
	}

	const parentId = readOptionalParentId(value.parentId, index);
	return {
		supersedes,
		marker,
		text: value.text,
		...(parentId !== undefined && { parentId }),
	};
}

function readMarker(value: unknown, index: number): ObservationLogMarker {
	if (typeof value !== 'string') {
		throw new Error(`Reflector merge[${index}].marker must be a known observation marker`);

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Use the `index` from the error message to locate the offending element in the raw LLM output.
  2. Update the reflector prompt: each `merge` element must be a complete object with `supersedes`, `marker`, and `text`; never emit bare text or null.
  3. If truncation is the cause, increase the model's `maxOutputTokens` or reduce the observation-log input size so the reflector has budget to finish.
  4. Enable structured-output / JSON mode on the provider to enforce the object shape at the API level.

Example fix

// before: { "merge": [ "replace obs-1", { "supersedes": [...], ... } ] }
// after:  { "merge": [ { "supersedes": ["obs-1"], "marker": "INFO", "text": "replace obs-1" } ] }
Defensive patterns

Strategy: type-guard

Validate before calling

import { isRecord } from '@n8n/utils/is-record';

const raw = JSON.parse(extractJsonObject(output));
if (Array.isArray(raw.merge)) {
  raw.merge = raw.merge.filter(isRecord); // drop non-object elements
}

Type guard

import { isRecord } from '@n8n/utils/is-record';

function isMergeObject(value: unknown): value is Record<string, unknown> {
  return isRecord(value);
}

Try / catch

try {
  const reflection = parseObservationLogReflectionJson(output);
} catch (e) {
  logger.warn('Reflector merge element not an object', { index: 'see message', output });
}

Prevention

When it happens

Trigger: The reflector LLM returns `{ "merge": ["just text", { ... }] }` where `merge[0]` is a string instead of an object, or `{ "merge": [null] }`.

Common situations: The model produced a mixed array of strings and objects. The model used `null` as a placeholder for a merge it could not produce. A truncated response cut off mid-object, leaving a bare value.

Related errors


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