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:
czlonkowski
2025-09-24 14:42:17 +02:00
parent ceb082efca
commit 60f78d5783
5 changed files with 239 additions and 26 deletions

View File

@@ -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 ## Best Practices
1. **Use Descriptive Names**: Always provide clear node names and descriptions for operations 1. **Use Descriptive Names**: Always provide clear node names and descriptions for operations

View File

@@ -4,18 +4,18 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
name: 'n8n_update_partial_workflow', name: 'n8n_update_partial_workflow',
category: 'workflow_management', category: 'workflow_management',
essentials: { 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'], keyParameters: ['id', 'operations'],
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "updateNode", ...}]})', example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "updateNode", ...}]})',
performance: 'Fast (50-200ms)', performance: 'Fast (50-200ms)',
tips: [ tips: [
'Use for targeted changes', 'Use for targeted changes',
'Supports up to 5 operations', 'Supports multiple operations in one call',
'Validate with validateOnly first' 'Validate with validateOnly first'
] ]
}, },
full: { 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: ## Available Operations:
@@ -42,7 +42,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
operations: { operations: {
type: 'array', type: 'array',
required: true, 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' } validateOnly: { type: 'boolean', description: 'If true, only validate operations without applying them' }
}, },
@@ -64,12 +64,10 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
bestPractices: [ bestPractices: [
'Use validateOnly to test operations', 'Use validateOnly to test operations',
'Group related changes in one call', 'Group related changes in one call',
'Keep operations under 5 for clarity',
'Check operation order for dependencies' 'Check operation order for dependencies'
], ],
pitfalls: [ pitfalls: [
'**REQUIRES N8N_API_URL and N8N_API_KEY environment variables** - will not work without n8n API access', '**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', 'Operations validated together - all must be valid',
'Order matters for dependent operations (e.g., must add node before connecting to it)', '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', 'Node references accept ID or name, but name must be unique',

View File

@@ -160,7 +160,7 @@ export const n8nManagementTools: ToolDefinition[] = [
}, },
{ {
name: 'n8n_update_partial_workflow', 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: { inputSchema: {
type: 'object', type: 'object',
additionalProperties: true, // Allow any extra properties Claude Desktop might add additionalProperties: true, // Allow any extra properties Claude Desktop might add

View File

@@ -41,17 +41,6 @@ export class WorkflowDiffEngine {
request: WorkflowDiffRequest request: WorkflowDiffRequest
): Promise<WorkflowDiffResult> { ): Promise<WorkflowDiffResult> {
try { 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 // Clone workflow to avoid modifying original
const workflowCopy = JSON.parse(JSON.stringify(workflow)); const workflowCopy = JSON.parse(JSON.stringify(workflow));

View File

@@ -3,6 +3,7 @@ import { WorkflowDiffEngine } from '@/services/workflow-diff-engine';
import { createWorkflow, WorkflowBuilder } from '@tests/utils/builders/workflow.builder'; import { createWorkflow, WorkflowBuilder } from '@tests/utils/builders/workflow.builder';
import { import {
WorkflowDiffRequest, WorkflowDiffRequest,
WorkflowDiffOperation,
AddNodeOperation, AddNodeOperation,
RemoveNodeOperation, RemoveNodeOperation,
UpdateNodeOperation, UpdateNodeOperation,
@@ -60,9 +61,10 @@ describe('WorkflowDiffEngine', () => {
baseWorkflow.connections = newConnections; baseWorkflow.connections = newConnections;
}); });
describe('Operation Limits', () => { describe('Large Operation Batches', () => {
it('should reject more than 5 operations', async () => { it('should handle many operations successfully', async () => {
const operations = Array(6).fill(null).map((_: any, i: number) => ({ // Test with 50 operations
const operations = Array(50).fill(null).map((_: any, i: number) => ({
type: 'updateName', type: 'updateName',
name: `Name ${i}` name: `Name ${i}`
} as UpdateNameOperation)); } as UpdateNameOperation));
@@ -74,9 +76,46 @@ describe('WorkflowDiffEngine', () => {
const result = await diffEngine.applyDiff(baseWorkflow, request); const result = await diffEngine.applyDiff(baseWorkflow, request);
expect(result.success).toBe(false); expect(result.success).toBe(true);
expect(result.errors).toHaveLength(1); expect(result.operationsApplied).toBe(50);
expect(result.errors![0].message).toContain('Too many operations'); 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');
}); });
}); });