n8n-io/n8n · error · Error

Reflector merge[${index}].text must be a string

Error message

Reflector merge[${index}].text must be a string

What it means

Inside each merge object, `text` must be a string — it is the consolidated observation text that replaces the superseded entries. `readMerge` checks `typeof value.text !== 'string'` and throws when `text` is missing, null, a number, or any non-string. This is the payload field; without a valid string the merge is meaningless.

Source

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

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

	switch (value.toUpperCase()) {
		case 'CRITICAL':

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Check the `index` from the message to find which merge object lacks a string `text`.
  2. Update the reflector prompt to mark `text` as required and instruct the model to omit the entire merge object (not just `text`) if it has no replacement text.
  3. If the model genuinely has no text, it should not emit that merge entry at all — `merge` can be empty `[]`.
  4. Enable structured-output mode to enforce `text: string` at the API level.

Example fix

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

Strategy: validation

Validate before calling

const raw = JSON.parse(extractJsonObject(output));
if (Array.isArray(raw.merge)) {
  raw.merge = raw.merge.filter(
    (m) => isRecord(m) && typeof m.text === 'string' && m.text.length > 0,
  );
}

Type guard

function hasValidText(value: unknown): value is { text: string } {
  return typeof value === 'object' && value !== null &&
    typeof (value as { text?: unknown }).text === 'string';
}

Try / catch

try {
  const reflection = parseObservationLogReflectionJson(output);
} catch (e) {
  logger.warn('Reflector merge text missing/not-string', { index: 'see message', output });
}

Prevention

When it happens

Trigger: The reflector LLM returns a merge object like `{ "supersedes": ["obs-1"], "marker": "INFO" }` (omitted `text`) or `{ ..., "text": null }` or `{ ..., "text": 123 }`.

Common situations: The model omitted `text` when it had nothing new to say. The model used `null` or empty to signal 'no replacement'. The prompt did not mark `text` as required.

Related errors


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