n8n-io/n8n · error · Error

Reflector output must be valid JSON

Error message

Reflector output must be valid JSON

What it means

Thrown by the subnode-connection validator when a node type that is only valid as a subnode (e.g. an embedding or language model) has no AI connection to a parent node. Such types must be attached via the parent's subnodes config (embedding(), languageModel(), etc.).

Source

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

}

export type RunObservationLogReflectorResult =
	| { status: 'skipped'; reason: 'below-threshold'; tokenCount: number }
	| {
			status: 'ran';
			tokenCount: number;
			remainingTokenCount: number;
			overBudgetAfterReflection: boolean;
			reflection: ObservationLogReflection;
			result: ObservationLogReflectionResult;
	  };

export function parseObservationLogReflectionJson(output: string): ObservationLogReflection {
	let parsed: unknown;
	try {
		parsed = JSON.parse(extractJsonObject(output));
	} catch {
		throw new Error('Reflector output must be valid JSON');
	}
	if (!isRecord(parsed)) throw new Error('Reflector output must be a JSON object');

	return {
		drop: readStringArray(parsed.drop ?? [], 'drop'),
		merge: readMergeArray(parsed.merge ?? []),
	};
}

export function renderObservationLogForReflection(entries: ObservationLogEntry[]): string {
	const activeEntries = entries.filter((entry) => entry.status === 'active').sort(compareEntries);
	const activeIds = new Set(activeEntries.map((entry) => entry.id));
	const childrenByParent = new Map<string, ObservationLogEntry[]>();
	const roots: ObservationLogEntry[] = [];

	for (const entry of activeEntries) {
		if (entry.parentId && activeIds.has(entry.parentId)) {
			const children = childrenByParent.get(entry.parentId) ?? [];

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Attach the node via the parent's subnode factory, e.g. agent({ ..., languageModel: lm }) or agent({ ..., embedding: emb }).
  2. Use the dedicated factory functions (languageModel(), embedding()) and wire them into the parent's subnodes config.
  3. Remove the orphan subnode if it is no longer needed.

Example fix

// before — model created standalone, never attached
const lm = chatOpenAi({ name: 'GPT' });
const emb = embeddingsOpenAi({ name: 'Embed' });

// after
const agentNode = agent({
  name: 'Agent',
  languageModel: chatOpenAi({ name: 'GPT' }),
});
Defensive patterns

Strategy: validation

Validate before calling

function assertSubnodeAttached(type: string, hasAiConnection: boolean, subnodeInfo: { subnodeField: string } | null): void {
  if (subnodeInfo && !hasAiConnection) {
    throw new Error(`${type} must be attached to a parent as ${subnodeInfo.subnodeField}`);
  }
}

Prevention

When it happens

Trigger: getRequiredSubnodeInfo(graphNode.instance.type) returns a subnodeInfo (connectionType + subnodeField), and hasAiConnectionOfType(graphNode, subnodeInfo.connectionType) is false — i.e. the node has no AI-side connection of the required type to a parent.

Common situations: Adding a Chat OpenAI / Embeddings model as a top-level node instead of attaching it; an AI builder creates the model but forgets the parent's subnodes config; deleting the parent left an orphan subnode.

Related errors


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