n8n-io/n8n · warning · Error

Reflector output did not contain a JSON object

Error message

Reflector output did not contain a JSON object

What it means

Warning thrown by the unknown-config-keys validator when a node's config object has top-level keys that are not recognised NodeConfig fields. Unrecognised keys are silently dropped during serialization, which leaves the node with empty parameters — a common LLM/MCP-client mistake of placing parameters directly under config instead of config.parameters.

Source

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

	if (hasAncestorIn(id, ownActionIds, activeById)) return false;
	if (hasAncestorIn(id, allSameKindActionIds, activeById)) return true;
	return !hasAncestorIn(id, otherRemovalIds, activeById);
}

function normalizeReplacementParentId(
	parentId: string | null | undefined,
	activeById: Map<string, ObservationLogEntry>,
	removedIds: Set<string>,
): string | null | undefined {
	if (parentId === undefined || parentId === null) return parentId;
	return activeById.has(parentId) && !removedIds.has(parentId) ? parentId : null;
}

function extractJsonObject(output: string): string {
	const start = output.indexOf('{');
	const end = output.lastIndexOf('}');
	if (start === -1 || end === -1 || end < start) {
		throw new Error('Reflector output did not contain a JSON object');
	}
	return output.slice(start, end + 1);
}

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');

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Move the unknown keys inside config.parameters so they survive serialization.
  2. If a key is a legitimate new NodeConfig field, add it to the KNOWN_CONFIG_KEYS map (and to the NodeConfig type).
  3. Prefix truly internal markers with '_' so they are intentionally ignored.

Example fix

// before — url sits at the top level of config and gets dropped
httpRequest({ name: 'Get', url: 'https://example.com' });

// after
httpRequest({ name: 'Get', parameters: { url: 'https://example.com' } });
Defensive patterns

Strategy: validation

Validate before calling

const KNOWN_CONFIG_KEYS = new Set([
  'parameters', 'credentials', 'name', 'position', 'webhookId', 'disabled', 'notes',
  'notesInFlow', 'executeOnce', 'retryOnFail', 'maxTries', 'waitBetweenTries',
  'alwaysOutputData', 'onError', 'extendsCredential', 'pinData', 'output', 'subnodes',
]);

function findUnknownConfigKeys(config: Record<string, unknown>): string[] {
  return Object.keys(config).filter((k) => !KNOWN_CONFIG_KEYS.has(k) && !k.startsWith('_'));
}

const unknown = findUnknownConfigKeys(node.config);
if (unknown.length) throw new Error(`Move into config.parameters: ${unknown.join(', ')}`);

Prevention

When it happens

Trigger: Object.keys(node.config) includes any key not in KNOWN_CONFIG_KEYS (parameters, credentials, name, position, webhookId, disabled, notes, notesInFlow, executeOnce, retryOnFail, maxTries, waitBetweenTries, alwaysOutputData, onError, extendsCredential, pinData, output, subnodes) and not starting with '_'.

Common situations: An LLM puts node inputs at the top level (e.g. config.url instead of config.parameters.url); a migration that nested the schema; copying an example that predated the NodeConfig contract.

Related errors


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