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
- Check the `index` from the message to find which merge object lacks a string `text`.
- 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.
- If the model genuinely has no text, it should not emit that merge entry at all — `merge` can be empty `[]`.
- 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
- Filter merge objects that lack a valid `text` string before parsing.
- Mark `text` as required in the reflector prompt and in any JSON schema sent to the model.
- Use structured-output mode to enforce `text: string`.
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
- Reflector merge[${index}].marker must be a known observation
- Reflector field "${fieldName}" must be an array
- Reflector field "${fieldName}" must contain only strings
- Reflector field "merge" must be an array
- Reflector merge[${index}] must be an object
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/7f0c0ec6badf1a8c.
Report an issue: GitHub.