mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
feat: remove unnecessary 5-operation limit from n8n_update_partial_workflow
The 5-operation limit was overly conservative and unnecessary. Analysis showed: - Workflow is cloned before modifications (no original mutation) - All operations validated before any are applied (true atomicity) - First error causes immediate return (no partial state possible) - Two-pass processing handles dependencies correctly Changes: - Remove hard-coded 5-operation limit check from workflow-diff-engine.ts - Update tool descriptions and documentation to reflect unlimited operations - Add tests verifying 50 and 100+ operations work successfully - Add example showing 26 operations in single request The system already ensures complete transactional integrity regardless of operation count. Bottleneck is workflow size, not operation count. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -296,6 +296,193 @@ The `n8n_update_partial_workflow` tool allows you to make targeted changes to wo
|
||||
}
|
||||
```
|
||||
|
||||
### Example 5: Large Batch Workflow Refactoring
|
||||
Demonstrates handling many operations in a single request - no longer limited to 5 operations!
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "workflow-batch",
|
||||
"operations": [
|
||||
// Add 10 processing nodes
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Filter Active Users",
|
||||
"type": "n8n-nodes-base.filter",
|
||||
"position": [400, 200],
|
||||
"parameters": { "conditions": { "boolean": [{ "value1": "={{$json.active}}", "value2": true }] } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Transform User Data",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"position": [600, 200],
|
||||
"parameters": { "values": { "string": [{ "name": "formatted_name", "value": "={{$json.firstName}} {{$json.lastName}}" }] } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Validate Email",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"position": [800, 200],
|
||||
"parameters": { "conditions": { "string": [{ "value1": "={{$json.email}}", "operation": "contains", "value2": "@" }] } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Enrich with API",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"position": [1000, 150],
|
||||
"parameters": { "url": "https://api.example.com/enrich", "method": "POST" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Log Invalid Emails",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"position": [1000, 350],
|
||||
"parameters": { "jsCode": "console.log('Invalid email:', $json.email);\nreturn $json;" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Merge Results",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"position": [1200, 250]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Deduplicate",
|
||||
"type": "n8n-nodes-base.removeDuplicates",
|
||||
"position": [1400, 250],
|
||||
"parameters": { "propertyName": "id" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Sort by Date",
|
||||
"type": "n8n-nodes-base.sort",
|
||||
"position": [1600, 250],
|
||||
"parameters": { "sortFieldsUi": { "sortField": [{ "fieldName": "created_at", "order": "descending" }] } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Batch for DB",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"position": [1800, 250],
|
||||
"parameters": { "batchSize": 100 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addNode",
|
||||
"node": {
|
||||
"name": "Save to Database",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"position": [2000, 250],
|
||||
"parameters": { "operation": "insert", "table": "processed_users" }
|
||||
}
|
||||
},
|
||||
// Connect all the nodes
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Get Users",
|
||||
"target": "Filter Active Users"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Filter Active Users",
|
||||
"target": "Transform User Data"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Transform User Data",
|
||||
"target": "Validate Email"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Validate Email",
|
||||
"sourceOutput": "true",
|
||||
"target": "Enrich with API"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Validate Email",
|
||||
"sourceOutput": "false",
|
||||
"target": "Log Invalid Emails"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Enrich with API",
|
||||
"target": "Merge Results"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Log Invalid Emails",
|
||||
"target": "Merge Results",
|
||||
"targetInput": "input2"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Merge Results",
|
||||
"target": "Deduplicate"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Deduplicate",
|
||||
"target": "Sort by Date"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Sort by Date",
|
||||
"target": "Batch for DB"
|
||||
},
|
||||
{
|
||||
"type": "addConnection",
|
||||
"source": "Batch for DB",
|
||||
"target": "Save to Database"
|
||||
},
|
||||
// Update workflow metadata
|
||||
{
|
||||
"type": "updateName",
|
||||
"name": "User Processing Pipeline v2"
|
||||
},
|
||||
{
|
||||
"type": "updateSettings",
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"timezone": "UTC",
|
||||
"saveDataSuccessExecution": "all"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "addTag",
|
||||
"tag": "production"
|
||||
},
|
||||
{
|
||||
"type": "addTag",
|
||||
"tag": "user-processing"
|
||||
},
|
||||
{
|
||||
"type": "addTag",
|
||||
"tag": "v2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This example shows 26 operations in a single request, creating a complete data processing pipeline with proper error handling, validation, and batch processing.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Descriptive Names**: Always provide clear node names and descriptions for operations
|
||||
|
||||
@@ -4,18 +4,18 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
||||
name: 'n8n_update_partial_workflow',
|
||||
category: 'workflow_management',
|
||||
essentials: {
|
||||
description: 'Update workflow incrementally with diff operations. Max 5 ops. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag.',
|
||||
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag.',
|
||||
keyParameters: ['id', 'operations'],
|
||||
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "updateNode", ...}]})',
|
||||
performance: 'Fast (50-200ms)',
|
||||
tips: [
|
||||
'Use for targeted changes',
|
||||
'Supports up to 5 operations',
|
||||
'Supports multiple operations in one call',
|
||||
'Validate with validateOnly first'
|
||||
]
|
||||
},
|
||||
full: {
|
||||
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 13 operation types for precise modifications. Operations are validated and applied atomically - all succeed or none are applied. Maximum 5 operations per call for safety.
|
||||
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 13 operation types for precise modifications. Operations are validated and applied atomically - all succeed or none are applied.
|
||||
|
||||
## Available Operations:
|
||||
|
||||
@@ -42,7 +42,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
||||
operations: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
description: 'Array of diff operations. Each must have "type" field and operation-specific properties. Max 5 operations. Nodes can be referenced by ID or name.'
|
||||
description: 'Array of diff operations. Each must have "type" field and operation-specific properties. Nodes can be referenced by ID or name.'
|
||||
},
|
||||
validateOnly: { type: 'boolean', description: 'If true, only validate operations without applying them' }
|
||||
},
|
||||
@@ -64,12 +64,10 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
|
||||
bestPractices: [
|
||||
'Use validateOnly to test operations',
|
||||
'Group related changes in one call',
|
||||
'Keep operations under 5 for clarity',
|
||||
'Check operation order for dependencies'
|
||||
],
|
||||
pitfalls: [
|
||||
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access',
|
||||
'Maximum 5 operations per call - split larger updates',
|
||||
'Operations validated together - all must be valid',
|
||||
'Order matters for dependent operations (e.g., must add node before connecting to it)',
|
||||
'Node references accept ID or name, but name must be unique',
|
||||
|
||||
@@ -160,7 +160,7 @@ export const n8nManagementTools: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
name: 'n8n_update_partial_workflow',
|
||||
description: `Update workflow incrementally with diff operations. Max 5 ops. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
||||
description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: true, // Allow any extra properties Claude Desktop might add
|
||||
|
||||
@@ -41,17 +41,6 @@ export class WorkflowDiffEngine {
|
||||
request: WorkflowDiffRequest
|
||||
): Promise<WorkflowDiffResult> {
|
||||
try {
|
||||
// Limit operations to keep complexity manageable
|
||||
if (request.operations.length > 5) {
|
||||
return {
|
||||
success: false,
|
||||
errors: [{
|
||||
operation: -1,
|
||||
message: 'Too many operations. Maximum 5 operations allowed per request to ensure transactional integrity.'
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// Clone workflow to avoid modifying original
|
||||
const workflowCopy = JSON.parse(JSON.stringify(workflow));
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
|
||||
import { createWorkflow, WorkflowBuilder } from '@tests/utils/builders/workflow.builder';
|
||||
import {
|
||||
import {
|
||||
WorkflowDiffRequest,
|
||||
WorkflowDiffOperation,
|
||||
AddNodeOperation,
|
||||
RemoveNodeOperation,
|
||||
UpdateNodeOperation,
|
||||
@@ -60,9 +61,10 @@ describe('WorkflowDiffEngine', () => {
|
||||
baseWorkflow.connections = newConnections;
|
||||
});
|
||||
|
||||
describe('Operation Limits', () => {
|
||||
it('should reject more than 5 operations', async () => {
|
||||
const operations = Array(6).fill(null).map((_: any, i: number) => ({
|
||||
describe('Large Operation Batches', () => {
|
||||
it('should handle many operations successfully', async () => {
|
||||
// Test with 50 operations
|
||||
const operations = Array(50).fill(null).map((_: any, i: number) => ({
|
||||
type: 'updateName',
|
||||
name: `Name ${i}`
|
||||
} as UpdateNameOperation));
|
||||
@@ -73,10 +75,47 @@ describe('WorkflowDiffEngine', () => {
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors![0].message).toContain('Too many operations');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.operationsApplied).toBe(50);
|
||||
expect(result.workflow!.name).toBe('Name 49'); // Last operation wins
|
||||
});
|
||||
|
||||
it('should handle 100+ mixed operations', async () => {
|
||||
const operations: WorkflowDiffOperation[] = [
|
||||
// Add 30 nodes
|
||||
...Array(30).fill(null).map((_: any, i: number) => ({
|
||||
type: 'addNode',
|
||||
node: {
|
||||
name: `Node${i}`,
|
||||
type: 'n8n-nodes-base.code',
|
||||
position: [i * 100, 300],
|
||||
parameters: {}
|
||||
}
|
||||
} as AddNodeOperation)),
|
||||
// Update names 30 times
|
||||
...Array(30).fill(null).map((_: any, i: number) => ({
|
||||
type: 'updateName',
|
||||
name: `Workflow Version ${i}`
|
||||
} as UpdateNameOperation)),
|
||||
// Add 40 tags
|
||||
...Array(40).fill(null).map((_: any, i: number) => ({
|
||||
type: 'addTag',
|
||||
tag: `tag${i}`
|
||||
} as AddTagOperation))
|
||||
];
|
||||
|
||||
const request: WorkflowDiffRequest = {
|
||||
id: 'test-workflow',
|
||||
operations
|
||||
};
|
||||
|
||||
const result = await diffEngine.applyDiff(baseWorkflow, request);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.operationsApplied).toBe(100);
|
||||
expect(result.workflow!.nodes.length).toBeGreaterThan(30);
|
||||
expect(result.workflow!.name).toBe('Workflow Version 29');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user