mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-08 06:13:07 +00:00
fix: resolve SplitInBatches output confusion for AI assistants (#97)
## Problem AI assistants were consistently connecting SplitInBatches node outputs backwards because: - Output index 0 = "done" (runs after loop completes) - Output index 1 = "loop" (processes items inside loop) This counterintuitive ordering caused incorrect workflow connections. ## Solution Enhanced the n8n-mcp system to expose and clarify output information: ### Database & Schema - Added `outputs` and `output_names` columns to nodes table - Updated NodeRepository to store/retrieve output information ### Node Parsing - Enhanced NodeParser to extract outputs and outputNames from nodes - Properly handles versioned nodes like SplitInBatchesV3 ### MCP Server - Modified getNodeInfo to return detailed output descriptions - Added connection guidance for each output - Special handling for loop nodes (SplitInBatches, IF, Switch) ### Documentation - Enhanced DocsMapper to inject critical output guidance - Added warnings about counterintuitive output ordering - Provides correct connection patterns for loop nodes ### Workflow Validation - Added validateSplitInBatchesConnection method - Detects reversed connections and provides specific errors - Added checkForLoopBack with depth limit to prevent stack overflow - Smart heuristics to identify likely connection mistakes ## Testing - Created comprehensive test suite (81 tests) - Unit tests for all modified components - Edge case handling for malformed data - Performance testing with large workflows ## Impact AI assistants will now: - See explicit output indices and names (e.g., "Output 0: done") - Receive clear connection guidance - Get validation errors when connections are reversed - Have enhanced documentation explaining the correct pattern Fixes #97 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -627,6 +627,9 @@ export class WorkflowValidator {
|
||||
result: WorkflowValidationResult,
|
||||
outputType: 'main' | 'error' | 'ai_tool'
|
||||
): void {
|
||||
// Get source node for special validation
|
||||
const sourceNode = nodeMap.get(sourceName);
|
||||
|
||||
outputs.forEach((outputConnections, outputIndex) => {
|
||||
if (!outputConnections) return;
|
||||
|
||||
@@ -641,12 +644,26 @@ export class WorkflowValidator {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special validation for SplitInBatches node
|
||||
if (sourceNode && sourceNode.type === 'n8n-nodes-base.splitInBatches') {
|
||||
this.validateSplitInBatchesConnection(
|
||||
sourceNode,
|
||||
outputIndex,
|
||||
connection,
|
||||
nodeMap,
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
// Check for self-referencing connections
|
||||
if (connection.node === sourceName) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
message: `Node "${sourceName}" has a self-referencing connection. This can cause infinite loops.`
|
||||
});
|
||||
// This is only a warning for non-loop nodes
|
||||
if (sourceNode && sourceNode.type !== 'n8n-nodes-base.splitInBatches') {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
message: `Node "${sourceName}" has a self-referencing connection. This can cause infinite loops.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const targetNode = nodeMap.get(connection.node);
|
||||
@@ -1470,4 +1487,133 @@ export class WorkflowValidator {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SplitInBatches node connections for common mistakes
|
||||
*/
|
||||
private validateSplitInBatchesConnection(
|
||||
sourceNode: WorkflowNode,
|
||||
outputIndex: number,
|
||||
connection: { node: string; type: string; index: number },
|
||||
nodeMap: Map<string, WorkflowNode>,
|
||||
result: WorkflowValidationResult
|
||||
): void {
|
||||
const targetNode = nodeMap.get(connection.node);
|
||||
if (!targetNode) return;
|
||||
|
||||
// Check if connections appear to be reversed
|
||||
// Output 0 = "done", Output 1 = "loop"
|
||||
|
||||
if (outputIndex === 0) {
|
||||
// This is the "done" output (index 0)
|
||||
// Check if target looks like it should be in the loop
|
||||
const targetType = targetNode.type.toLowerCase();
|
||||
const targetName = targetNode.name.toLowerCase();
|
||||
|
||||
// Common patterns that suggest this node should be inside the loop
|
||||
if (targetType.includes('function') ||
|
||||
targetType.includes('code') ||
|
||||
targetType.includes('item') ||
|
||||
targetName.includes('process') ||
|
||||
targetName.includes('transform') ||
|
||||
targetName.includes('handle')) {
|
||||
|
||||
// Check if this node connects back to the SplitInBatches
|
||||
const hasLoopBack = this.checkForLoopBack(targetNode.name, sourceNode.name, nodeMap);
|
||||
|
||||
if (hasLoopBack) {
|
||||
result.errors.push({
|
||||
type: 'error',
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.name,
|
||||
message: `SplitInBatches outputs appear reversed! Node "${targetNode.name}" is connected to output 0 ("done") but connects back to the loop. It should be connected to output 1 ("loop") instead. Remember: Output 0 = "done" (post-loop), Output 1 = "loop" (inside loop).`
|
||||
});
|
||||
} else {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.name,
|
||||
message: `Node "${targetNode.name}" is connected to the "done" output (index 0) but appears to be a processing node. Consider connecting it to the "loop" output (index 1) if it should process items inside the loop.`
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (outputIndex === 1) {
|
||||
// This is the "loop" output (index 1)
|
||||
// Check if target looks like it should be after the loop
|
||||
const targetType = targetNode.type.toLowerCase();
|
||||
const targetName = targetNode.name.toLowerCase();
|
||||
|
||||
// Common patterns that suggest this node should be after the loop
|
||||
if (targetType.includes('aggregate') ||
|
||||
targetType.includes('merge') ||
|
||||
targetType.includes('email') ||
|
||||
targetType.includes('slack') ||
|
||||
targetName.includes('final') ||
|
||||
targetName.includes('complete') ||
|
||||
targetName.includes('summary') ||
|
||||
targetName.includes('report')) {
|
||||
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.name,
|
||||
message: `Node "${targetNode.name}" is connected to the "loop" output (index 1) but appears to be a post-processing node. Consider connecting it to the "done" output (index 0) if it should run after all iterations complete.`
|
||||
});
|
||||
}
|
||||
|
||||
// Check if loop output doesn't eventually connect back
|
||||
const hasLoopBack = this.checkForLoopBack(targetNode.name, sourceNode.name, nodeMap);
|
||||
if (!hasLoopBack) {
|
||||
result.warnings.push({
|
||||
type: 'warning',
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.name,
|
||||
message: `The "loop" output connects to "${targetNode.name}" but doesn't connect back to the SplitInBatches node. The last node in the loop should connect back to complete the iteration.`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node eventually connects back to a target node
|
||||
*/
|
||||
private checkForLoopBack(
|
||||
startNode: string,
|
||||
targetNode: string,
|
||||
nodeMap: Map<string, WorkflowNode>,
|
||||
visited: Set<string> = new Set(),
|
||||
maxDepth: number = 50
|
||||
): boolean {
|
||||
if (maxDepth <= 0) return false; // Prevent stack overflow
|
||||
if (visited.has(startNode)) return false;
|
||||
visited.add(startNode);
|
||||
|
||||
const node = nodeMap.get(startNode);
|
||||
if (!node) return false;
|
||||
|
||||
// Check direct connections from this node
|
||||
const connections = (node as any).connections;
|
||||
if (!connections) return false;
|
||||
|
||||
for (const [outputType, outputs] of Object.entries(connections)) {
|
||||
if (!Array.isArray(outputs)) continue;
|
||||
|
||||
for (const outputConnections of outputs) {
|
||||
if (!Array.isArray(outputConnections)) continue;
|
||||
|
||||
for (const conn of outputConnections) {
|
||||
if (conn.node === targetNode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recursively check connected nodes
|
||||
if (this.checkForLoopBack(conn.node, targetNode, nodeMap, visited, maxDepth - 1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user