{"record":{"id":"3d8ce2ab0864163a","repo":"n8n-io/n8n","slug":"reflector-output-must-be-a-json-object","errorCode":null,"errorMessage":"Reflector output must be a JSON object","messagePattern":"Reflector output must be a JSON object","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts","lineNumber":71,"sourceCode":"export type RunObservationLogReflectorResult =\n\t| { status: 'skipped'; reason: 'below-threshold'; tokenCount: number }\n\t| {\n\t\t\tstatus: 'ran';\n\t\t\ttokenCount: number;\n\t\t\tremainingTokenCount: number;\n\t\t\toverBudgetAfterReflection: boolean;\n\t\t\treflection: ObservationLogReflection;\n\t\t\tresult: ObservationLogReflectionResult;\n\t  };\n\nexport function parseObservationLogReflectionJson(output: string): ObservationLogReflection {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(extractJsonObject(output));\n\t} catch {\n\t\tthrow new Error('Reflector output must be valid JSON');\n\t}\n\tif (!isRecord(parsed)) throw new Error('Reflector output must be a JSON object');\n\n\treturn {\n\t\tdrop: readStringArray(parsed.drop ?? [], 'drop'),\n\t\tmerge: readMergeArray(parsed.merge ?? []),\n\t};\n}\n\nexport function renderObservationLogForReflection(entries: ObservationLogEntry[]): string {\n\tconst activeEntries = entries.filter((entry) => entry.status === 'active').sort(compareEntries);\n\tconst activeIds = new Set(activeEntries.map((entry) => entry.id));\n\tconst childrenByParent = new Map<string, ObservationLogEntry[]>();\n\tconst roots: ObservationLogEntry[] = [];\n\n\tfor (const entry of activeEntries) {\n\t\tif (entry.parentId && activeIds.has(entry.parentId)) {\n\t\t\tconst children = childrenByParent.get(entry.parentId) ?? [];\n\t\t\tchildren.push(entry);\n\t\t\tchildrenByParent.set(entry.parentId, children);","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/runtime/memory/observation-log-reflector.ts#L53-L89","documentation":"Warning thrown by the tool-node validator when a tool node has no parameters set. Most tool nodes need at least one parameter (often a $fromAI description) to be useful to an agent. A small allowlist of tools (calculator, vector stores, MCP client tool, Wikipedia, SerpApi) is exempt.","triggerScenarios":"isToolNode(node.type) is true (type contains 'tool'/'Tool'), the node is not in TOOLS_WITHOUT_PARAMETERS, the nodeTypesProvider (if present) does not report an empty properties array, and node.config.parameters is missing or has zero own keys.","commonSituations":"Adding a custom HTTP/tool node and forgetting its description parameters; an AI builder emits a tool shell without $fromAI fields; the provider info was unavailable so the static allowlist did not cover a parameterless custom tool.","solutions":["Add the parameters the tool needs (e.g. a $fromAI description for each agent-provided argument).","If the tool genuinely takes no parameters, register it in the node type with an empty properties array so the provider path skips validation.","Confirm node.type is correct — a misnamed type that happens to contain 'tool' will trigger this check."],"exampleFix":"// before\nconst t = toolHttp({ name: 'Fetcher' });\n\n// after — add a parameter the agent can supply\ntoolHttp({\n  name: 'Fetcher',\n  parameters: { url: expr('={{ $fromAI.url }}') },\n});","handlingStrategy":"validation","validationCode":"const TOOLS_WITHOUT_PARAMETERS = new Set([\n  '@n8n/n8n-nodes-langchain.toolCalculator',\n  '@n8n/n8n-nodes-langchain.toolVectorStore',\n  '@n8n/n8n-nodes-langchain.vectorStoreInMemory',\n  '@n8n/n8n-nodes-langchain.mcpClientTool',\n  '@n8n/n8n-nodes-langchain.toolWikipedia',\n  '@n8n/n8n-nodes-langchain.toolSerpApi',\n]);\n\nfunction needsParameters(type: string, params: unknown, propertiesLength: number | undefined): boolean {\n  if (!/tool/i.test(type)) return false;\n  if (TOOLS_WITHOUT_PARAMETERS.has(type)) return false;\n  if (propertiesLength === 0) return false;\n  return !params || Object.keys(params as object).length === 0;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always add at least one $fromAI-described parameter to custom tool nodes.","Register genuinely parameterless tools with an empty properties array so the provider path skips validation.","Confirm node.type spelling — types containing 'tool' opt into this check."],"tags":["ai","langchain","tool-node","parameters"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}