n8n-io/n8n · error · Error

Reflector merge[${index}].marker must be a known observation

Error message

Reflector merge[${index}].marker must be a known observation marker

What it means

`readMarker` is called for each merge object and first checks that `value.marker` is a string. This throw (line 323) fires when `marker` is not a string at all — it is missing, null, a number, etc. The marker classifies the merged observation's severity. A separate throw at line 336 covers the case where the string is not one of the four known values.

Source

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

	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`);
	}

	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;

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Use the `index` from the error to find the merge object whose `marker` is non-string.
  2. Update the reflector prompt: `marker` is required and must be one of `CRITICAL`, `IMPORTANT`, `INFO`, `COMPLETION` (case-insensitive).
  3. If you want `marker` to be optional, patch the reader to default to `'info'` when `marker` is absent — but coordinate with the memory system's expectations.
  4. Enable structured-output / JSON-schema mode to enforce `marker: string` at the API level.

Example fix

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

Strategy: validation

Validate before calling

const raw = JSON.parse(extractJsonObject(output));
if (Array.isArray(raw.merge)) {
  for (const m of raw.merge) {
    if (isRecord(m) && typeof m.marker !== 'string') {
      m.marker = 'INFO'; // default to info when missing/non-string
    }
  }
}

Type guard

function isStringMarker(value: unknown): value is string {
  return typeof value === 'string';
}

Try / catch

try {
  const reflection = parseObservationLogReflectionJson(output);
} catch (e) {
  logger.warn('Reflector marker not a string', { index: 'see message', output });
}

Prevention

When it happens

Trigger: The reflector LLM returns a merge object with `{ "marker": null }`, `{ "marker": 1 }`, or omits `marker` entirely (so it is `undefined`).

Common situations: The reflector prompt does not list `marker` as required. The model used a non-string sentinel. The model omitted the field for merges it considered low priority.

Related errors


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