Skip to content

Commit 29e3f25

Browse files
czlonkowskiJohanDuqueclaude
authored
fix: n8n_executions reads AI sub-node data and every run of a node (v2.80.1) (#1058)
* fix: n8n_executions never reads ai_* connection data for AI Agent sub-nodes The execution processor (preview/summary/filtered/full/error modes) only inspected run.data.main when extracting node input/output data. Regular nodes connect via `main`, but LangChain AI Agent sub-nodes (Chat Model, Output Parser, Tool, Memory, Embeddings, etc.) connect to their parent Agent via special ai_* connection types instead (ai_languageModel, ai_outputParser, ai_tool, ai_memory, ...). Their task data lives at run.data.ai_languageModel etc., so every mode silently returned itemsInput: 0, itemsOutput: 0, data.output: [] for these nodes regardless of flags. Also fixes includeInputData, which read a nonexistent `inputData` field on the run - n8n actually stores this as `inputOverride`, keyed by connection type the same way `data` is. This made includeInputData a no-op for every node type, not only AI sub-nodes. Both files now merge over every connection-type key present (main and any ai_*) rather than hardcoding `main`, mirroring the same fix pattern already used elsewhere in this repo for disconnected-node detection (dynamic iteration over connection types instead of a fixed list). Verified against a live n8n Cloud execution with Chat Model, Output Parser, Tool, and Memory sub-nodes: all four now report correct itemsInput/itemsOutput/data across preview, summary, filtered, full, and error modes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 6191e99) * fix: execution processor only returned the first run's data per node Node-level summary counts (itemsInput/itemsOutput) already summed across every run of a node - countItems() correctly loops over the whole runData array. But the code building the actual returned data.output/data.input arrays only ever read nodeData[0], the first run. A node invoked more than once within a single execution (most commonly an AI Agent's Chat Model: once to decide to call a tool, again to produce the final answer) has one runData entry per invocation, so every invocation after the first was silently dropped from the response while still being counted in the totals. Concretely: itemsOutput: 2 but data.output containing only 1 item - and it was always the first, least useful one (the short "I'll call a tool" turn), never the second (the actual answer, sometimes truncated by a max_tokens ceiling). Both execution-processor.ts and error-execution-processor.ts had this pattern in multiple places (data extraction, item counts for preview/ execution-path, and error detection). Fixed by merging across every run of a node - by branch/port index, in run order - instead of reading index 0, so itemsLimit truncation now behaves as "first N items across all invocations" rather than "first N items of the first invocation only". Error detection was extended the same way: an error on a later run (not just the first) is no longer invisible. Verified against a live n8n Cloud execution with a multi-turn Chat Model: the previously-hidden second generation (truncated at finish_reason: length, 800 completion tokens) now returns correctly across full/summary/filtered modes. Also surfaced a genuine, previously-invisible error on a middle run of an Output Parser node that error mode had never reported before this fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit d227b54) * fix: merge execution run data by connection type and bound inputs Follow-ups on #965 after review: - share the runData helpers between the two processors (src/services/execution-run-data.ts) - merge branches keyed by connection type and port, so runs that populate different types (main, ai_tool) no longer collapse into one index; null ports stay null - append with a loop instead of push(...items): a 150k-item branch overflowed the call stack - merge only up to the item limit in summary/filtered mode; totals come from the counts - apply the item limit to inputs too and report inputMetadata, since AI sub-nodes carry whole prompts in inputOverride - preview reports the last failing run's error like the other modes; executionTime is the sum across runs Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQmLW2QcubiwyuemrLs62d * chore: release 2.80.1 Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQmLW2QcubiwyuemrLs62d --------- Co-authored-by: JohanDuque <johand.duque@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent a77d186 commit 29e3f25

11 files changed

Lines changed: 735 additions & 78 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.80.1] - 2026-09-03
11+
12+
### Fixed
13+
14+
- **`n8n_executions` now returns the data of AI Agent sub-nodes and of every run of a node** ([#965](https://github.com/czlonkowski/n8n-mcp/pull/965)). The execution processors read only `runData[node][0].data.main`, so a Chat Model, Tool, Memory or Output Parser node, whose task data lives under `ai_*` connection types, showed no items, and a node invoked more than once in an execution (an agent's model, called once per tool round) lost every run after the first. Items are now merged across runs, keyed by connection type and output port, so a node whose runs populate different types keeps them apart, and a port n8n recorded as `null` stays `null`. The last failing run's error is reported in every mode, `executionTime` is the sum across runs, and `preview` mode reads the first item without merging. In `summary` and `filtered` modes only as many items as the limit are merged, and inputs (`includeInputData`), which for AI sub-nodes carry whole prompts, are truncated with the same limit and described by a new `inputMetadata` field. The error processor reads the same helpers, so `error` mode's upstream context and execution path include AI sub-nodes too.
15+
1016
## [2.80.0] - 2026-09-03
1117

1218
### Fixed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "n8n-mcp",
3-
"version": "2.80.0",
3+
"version": "2.80.1",
44
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

package.runtime.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "n8n-mcp-runtime",
3-
"version": "2.80.0",
3+
"version": "2.80.1",
44
"description": "n8n MCP Server Runtime Dependencies Only",
55
"private": true,
66
"dependencies": {

src/mcp/tool-docs/workflow_management/n8n-executions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const n8nExecutionsDoc: ToolDocumentation = {
4040
id: { type: 'string', required: false, description: 'Execution ID. Required for action=delete; for action=get, omitting it lists executions instead' },
4141
mode: { type: 'string', required: false, description: 'For action=get: "preview", "summary" (default), "filtered", "full", "error"' },
4242
nodeNames: { type: 'array', required: false, description: 'For action=get with mode=filtered: Filter to specific nodes by name' },
43-
itemsLimit: { type: 'number', required: false, description: 'For action=get with mode=filtered: Items per node (0=structure, 2=default, -1=unlimited)' },
43+
itemsLimit: { type: 'number', required: false, description: 'For action=get with mode=summary or filtered: Items per node (0=structure, 2=default, -1=unlimited); applies to input data too when includeInputData is set' },
4444
includeInputData: { type: 'boolean', required: false, description: 'For action=get: Include input data in addition to output (default: false)' },
4545
errorItemsLimit: { type: 'number', required: false, description: 'For action=get with mode=error: Sample items from upstream (default: 2, max: 100)' },
4646
includeStackTrace: { type: 'boolean', required: false, description: 'For action=get with mode=error: Include full stack trace (default: false, shows truncated)' },

src/services/error-execution-processor.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ import {
1818
ErrorSuggestion,
1919
} from '../types/n8n-api';
2020
import { logger } from '../utils/logger';
21+
import {
22+
countRunItems,
23+
getRunError,
24+
latestStartTime,
25+
hasRunOutputData,
26+
sampleRunItems,
27+
totalExecutionTime,
28+
} from './execution-run-data';
2129

2230
/**
2331
* Options for error processing
@@ -132,7 +140,7 @@ function extractPrimaryError(
132140

133141
// Also check runData for node-level errors
134142
const nodeRunData = runData[nodeName];
135-
const nodeError = nodeRunData?.[0]?.error;
143+
const nodeError = getRunError(nodeRunData);
136144

137145
const stackTrace = (error?.stack || nodeError?.stack) as string | undefined;
138146

@@ -177,12 +185,13 @@ function extractUpstreamContext(
177185
.filter(([name, data]) => {
178186
if (name === errorNodeName) return false;
179187
const runs = data as any[];
180-
return runs?.[0]?.data?.main?.[0]?.length > 0 && !runs?.[0]?.error;
188+
return countRunItems(runs) > 0 && !getRunError(runs);
181189
})
182190
.map(([name, data]) => ({
183191
name,
184-
executionTime: (data as any[])?.[0]?.executionTime || 0,
185-
startTime: (data as any[])?.[0]?.startTime || 0
192+
executionTime: totalExecutionTime(data) ?? 0,
193+
// The most recent run decides recency for a node that ran more than once
194+
startTime: latestStartTime(data)
186195
}))
187196
.sort((a, b) => b.startTime - a.startTime);
188197

@@ -252,9 +261,10 @@ function extractNodeOutput(
252261
itemsLimit: number
253262
): ErrorAnalysis['upstreamContext'] | undefined {
254263
const nodeData = runData[nodeName];
255-
if (!nodeData?.[0]?.data?.main?.[0]) return undefined;
264+
if (!hasRunOutputData(nodeData)) return undefined;
256265

257-
const items = nodeData[0].data.main[0];
266+
// Merge only the samples; the count covers every port and comes from a pass that copies nothing.
267+
const items = sampleRunItems(nodeData, Math.max(itemsLimit, 1));
258268

259269
// Sanitize sample items to remove sensitive data
260270
const rawSamples = items.slice(0, itemsLimit);
@@ -263,7 +273,7 @@ function extractNodeOutput(
263273
return {
264274
nodeName,
265275
nodeType: '', // Will be enriched if workflow available
266-
itemCount: items.length,
276+
itemCount: countRunItems(nodeData),
267277
sampleItems: sanitizedSamples,
268278
dataStructure: extractStructure(items[0])
269279
};
@@ -287,14 +297,14 @@ function buildExecutionPath(
287297
for (const nodeName of upstreamNodes) {
288298
const nodeData = runData[nodeName];
289299
const runs = nodeData as any[] | undefined;
290-
const hasError = runs?.[0]?.error;
291-
const itemCount = runs?.[0]?.data?.main?.[0]?.length || 0;
300+
const hasError = getRunError(runs);
301+
const itemCount = countRunItems(runs);
292302

293303
path.push({
294304
nodeName,
295305
status: hasError ? 'error' : (runs ? 'success' : 'skipped'),
296306
itemCount,
297-
executionTime: runs?.[0]?.executionTime
307+
executionTime: totalExecutionTime(runs)
298308
});
299309
}
300310

@@ -304,24 +314,24 @@ function buildExecutionPath(
304314
nodeName: errorNodeName,
305315
status: 'error',
306316
itemCount: 0,
307-
executionTime: errorNodeData?.[0]?.executionTime
317+
executionTime: totalExecutionTime(errorNodeData)
308318
});
309319
} else {
310320
// Without workflow, list all executed nodes by execution order (best effort)
311321
const nodesByTime = Object.entries(runData)
312322
.map(([name, data]) => ({
313323
name,
314324
data: data as any[],
315-
startTime: (data as any[])?.[0]?.startTime || 0
325+
startTime: latestStartTime(data)
316326
}))
317327
.sort((a, b) => a.startTime - b.startTime);
318328

319329
for (const { name, data } of nodesByTime) {
320330
path.push({
321331
nodeName: name,
322-
status: data?.[0]?.error ? 'error' : 'success',
323-
itemCount: data?.[0]?.data?.main?.[0]?.length || 0,
324-
executionTime: data?.[0]?.executionTime
332+
status: getRunError(data) ? 'error' : 'success',
333+
itemCount: countRunItems(data),
334+
executionTime: totalExecutionTime(data)
325335
});
326336
}
327337
}
@@ -342,7 +352,7 @@ function findAdditionalErrors(
342352
if (nodeName === primaryErrorNode) continue;
343353

344354
const runs = data as any[];
345-
const error = runs?.[0]?.error;
355+
const error = getRunError(runs);
346356
if (error) {
347357
additional.push({
348358
nodeName,

src/services/execution-processor.ts

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ import {
2525
} from '../types/n8n-api';
2626
import { logger } from '../utils/logger';
2727
import { processErrorExecution } from './error-execution-processor';
28+
import {
29+
extractConnectionBranches,
30+
firstRunItem,
31+
getRunError,
32+
mergeRunBranches,
33+
totalExecutionTime,
34+
type RunBranch,
35+
} from './execution-run-data';
2836

2937
/**
3038
* Size estimation and threshold constants
@@ -116,15 +124,11 @@ function countItems(nodeData: unknown): { input: number; output: number } {
116124
}
117125

118126
for (const run of nodeData) {
119-
if (run?.data?.main) {
120-
const mainData = run.data.main;
121-
if (Array.isArray(mainData)) {
122-
for (const output of mainData) {
123-
if (Array.isArray(output)) {
124-
counts.output += output.length;
125-
}
126-
}
127-
}
127+
for (const output of extractConnectionBranches(run?.data)) {
128+
counts.output += output?.length ?? 0;
129+
}
130+
for (const input of extractConnectionBranches(run?.inputOverride)) {
131+
counts.input += input?.length ?? 0;
128132
}
129133
}
130134

@@ -167,14 +171,11 @@ export function generatePreview(execution: Execution): {
167171
const nodeData = runData[nodeName];
168172
const itemCounts = countItems(nodeData);
169173

170-
// Extract structure from first run's first output item
174+
// Structure of the first output item any run produced
171175
let dataStructure: Record<string, unknown> = {};
172-
if (Array.isArray(nodeData) && nodeData.length > 0) {
173-
const firstRun = nodeData[0];
174-
const firstItem = firstRun?.data?.main?.[0]?.[0];
175-
if (firstItem) {
176-
dataStructure = extractStructure(firstItem) as Record<string, unknown>;
177-
}
176+
const firstItem = firstRunItem(nodeData);
177+
if (firstItem) {
178+
dataStructure = extractStructure(firstItem) as Record<string, unknown>;
178179
}
179180

180181
const nodeSize = estimateDataSize(nodeData);
@@ -186,15 +187,10 @@ export function generatePreview(execution: Execution): {
186187
estimatedSizeKB: nodeSize,
187188
};
188189

189-
// Check for errors
190-
if (Array.isArray(nodeData)) {
191-
for (const run of nodeData) {
192-
if (run.error) {
193-
nodePreview.status = 'error';
194-
nodePreview.error = extractErrorMessage(run.error);
195-
break;
196-
}
197-
}
190+
const runError = getRunError(nodeData);
191+
if (runError) {
192+
nodePreview.status = 'error';
193+
nodePreview.error = extractErrorMessage(runError);
198194
}
199195

200196
preview.nodes[nodeName] = nodePreview;
@@ -256,33 +252,38 @@ function generateRecommendation(
256252
* Truncate items array with metadata
257253
*/
258254
function truncateItems(
259-
items: unknown[][],
260-
limit: number
255+
items: RunBranch[],
256+
limit: number,
257+
knownTotal?: number
261258
): {
262-
truncated: unknown[][];
259+
truncated: RunBranch[];
263260
metadata: { totalItems: number; itemsShown: number; truncated: boolean };
264261
} {
265262
if (!Array.isArray(items) || items.length === 0) {
266263
return {
267264
truncated: items || [],
268265
metadata: {
269-
totalItems: 0,
266+
totalItems: knownTotal ?? 0,
270267
itemsShown: 0,
271-
truncated: false,
268+
truncated: (knownTotal ?? 0) > 0,
272269
},
273270
};
274271
}
275272

276-
let totalItems = 0;
277-
for (const output of items) {
278-
if (Array.isArray(output)) {
279-
totalItems += output.length;
273+
// `knownTotal` lets the caller pass branches merged only up to the limit.
274+
let totalItems = knownTotal ?? 0;
275+
if (knownTotal === undefined) {
276+
for (const output of items) {
277+
if (Array.isArray(output)) {
278+
totalItems += output.length;
279+
}
280280
}
281281
}
282282

283283
// Special case: limit = 0 means structure only
284284
if (limit === 0) {
285285
const structureOnly = items.map(output => {
286+
if (output === null) return null;
286287
if (!Array.isArray(output) || output.length === 0) {
287288
return [];
288289
}
@@ -294,7 +295,7 @@ function truncateItems(
294295
metadata: {
295296
totalItems,
296297
itemsShown: 0,
297-
truncated: true,
298+
truncated: totalItems > 0,
298299
},
299300
};
300301
}
@@ -312,7 +313,7 @@ function truncateItems(
312313
}
313314

314315
// Apply limit
315-
const result: unknown[][] = [];
316+
const result: RunBranch[] = [];
316317
let itemsShown = 0;
317318

318319
for (const output of items) {
@@ -460,42 +461,40 @@ export function filterExecutionData(
460461
continue;
461462
}
462463

463-
// Get first run data
464-
const firstRun = nodeData[0];
464+
// Every field aggregates across all runs of the node.
465465
const itemCounts = countItems(nodeData);
466466
totalItems += itemCounts.output;
467467

468468
const nodeResult: FilteredNodeData = {
469-
executionTime: firstRun.executionTime,
469+
executionTime: totalExecutionTime(nodeData),
470470
itemsInput: itemCounts.input,
471471
itemsOutput: itemCounts.output,
472472
status: 'success',
473473
};
474474

475-
// Check for errors
476-
if (firstRun.error) {
475+
const runError = getRunError(nodeData);
476+
if (runError) {
477477
nodeResult.status = 'error';
478-
nodeResult.error = extractErrorMessage(firstRun.error);
478+
nodeResult.error = extractErrorMessage(runError);
479479
}
480480

481-
// Handle full mode - include all data
481+
// Merge only as many items per branch as will be shown; the counts above supply the totals.
482+
// Structure-only (limit 0) still needs the first item of each branch.
483+
const maxPerBranch = mode === 'full' || itemsLimit < 0 ? -1 : Math.max(itemsLimit, 1);
484+
const outputData = mergeRunBranches(nodeData, 'data', maxPerBranch);
485+
482486
if (mode === 'full') {
483487
nodeResult.data = {
484-
output: firstRun.data?.main || [],
488+
output: outputData,
485489
metadata: {
486490
totalItems: itemCounts.output,
487491
itemsShown: itemCounts.output,
488492
truncated: false,
489493
},
490494
};
491-
492-
if (includeInputData && firstRun.inputData) {
493-
nodeResult.data.input = firstRun.inputData;
494-
}
495495
} else {
496-
// Summary or filtered mode - apply limits
497-
const outputData = firstRun.data?.main || [];
498-
const { truncated, metadata } = truncateItems(outputData, itemsLimit);
496+
// Summary or filtered mode - apply item limits
497+
const { truncated, metadata } = truncateItems(outputData, itemsLimit, itemCounts.output);
499498

500499
if (metadata.truncated) {
501500
hasMoreData = true;
@@ -505,9 +504,19 @@ export function filterExecutionData(
505504
output: truncated,
506505
metadata,
507506
};
507+
}
508508

509-
if (includeInputData && firstRun.inputData) {
510-
nodeResult.data.input = firstRun.inputData;
509+
if (includeInputData && itemCounts.input > 0) {
510+
const inputData = mergeRunBranches(nodeData, 'inputOverride', maxPerBranch);
511+
if (mode === 'full') {
512+
nodeResult.data.input = inputData;
513+
} else {
514+
const { truncated, metadata } = truncateItems(inputData, itemsLimit, itemCounts.input);
515+
if (metadata.truncated) {
516+
hasMoreData = true;
517+
}
518+
nodeResult.data.input = truncated;
519+
nodeResult.data.inputMetadata = metadata;
511520
}
512521
}
513522

0 commit comments

Comments
 (0)