fix: use 'updates' property consistently in updateNode operations

- Changed UpdateNodeOperation interface to use 'updates' instead of 'changes'
- Updated UpdateConnectionOperation for consistency
- Fixed implementation in workflow-diff-engine.ts
- Updated Zod schema validation
- Fixed documentation and examples
- Updated tests to match new property name

This resolves GitHub issues #159 and #168 where partial workflow updates
were failing, forcing AI agents to fall back to expensive full updates.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-17 23:22:51 +02:00
parent 0ef69fbf75
commit 17530c0f72
5 changed files with 18 additions and 18 deletions

View File

@@ -21,7 +21,7 @@ const workflowDiffSchema = z.object({
node: z.any().optional(), node: z.any().optional(),
nodeId: z.string().optional(), nodeId: z.string().optional(),
nodeName: z.string().optional(), nodeName: z.string().optional(),
changes: z.any().optional(), updates: z.any().optional(),
position: z.tuple([z.number(), z.number()]).optional(), position: z.tuple([z.number(), z.number()]).optional(),
// Connection operations // Connection operations
source: z.string().optional(), source: z.string().optional(),

View File

@@ -48,7 +48,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
}, },
returns: 'Updated workflow object or validation results if validateOnly=true', returns: 'Updated workflow object or validation results if validateOnly=true',
examples: [ examples: [
'// Update node parameter\nn8n_update_partial_workflow({id: "abc", operations: [{type: "updateNode", nodeName: "HTTP Request", changes: {"parameters.url": "https://api.example.com"}}]})', '// Update node parameter\nn8n_update_partial_workflow({id: "abc", operations: [{type: "updateNode", nodeName: "HTTP Request", updates: {"parameters.url": "https://api.example.com"}}]})',
'// Add connection between nodes\nn8n_update_partial_workflow({id: "xyz", operations: [{type: "addConnection", source: "Webhook", target: "Slack", sourceOutput: "main", targetInput: "main"}]})', '// Add connection between nodes\nn8n_update_partial_workflow({id: "xyz", operations: [{type: "addConnection", source: "Webhook", target: "Slack", sourceOutput: "main", targetInput: "main"}]})',
'// Multiple operations in one call\nn8n_update_partial_workflow({id: "123", operations: [\n {type: "addNode", node: {name: "Transform", type: "n8n-nodes-base.code", position: [400, 300]}},\n {type: "addConnection", source: "Webhook", target: "Transform"},\n {type: "updateSettings", settings: {timezone: "America/New_York"}}\n]})', '// Multiple operations in one call\nn8n_update_partial_workflow({id: "123", operations: [\n {type: "addNode", node: {name: "Transform", type: "n8n-nodes-base.code", position: [400, 300]}},\n {type: "addConnection", source: "Webhook", target: "Transform"},\n {type: "updateSettings", settings: {timezone: "America/New_York"}}\n]})',
'// Validate before applying\nn8n_update_partial_workflow({id: "456", operations: [{type: "removeNode", nodeName: "Old Process"}], validateOnly: true})' '// Validate before applying\nn8n_update_partial_workflow({id: "456", operations: [{type: "removeNode", nodeName: "Old Process"}], validateOnly: true})'
@@ -73,7 +73,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
'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',
'Dot notation for nested updates: use "parameters.url" not nested objects' 'Use "updates" property for updateNode operations: {type: "updateNode", updates: {...}}'
], ],
relatedTools: ['n8n_update_full_workflow', 'n8n_get_workflow', 'validate_workflow', 'tools_documentation'] relatedTools: ['n8n_update_full_workflow', 'n8n_get_workflow', 'validate_workflow', 'tools_documentation']
} }

View File

@@ -453,8 +453,8 @@ export class WorkflowDiffEngine {
const node = this.findNode(workflow, operation.nodeId, operation.nodeName); const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
if (!node) return; if (!node) return;
// Apply changes using dot notation // Apply updates using dot notation
Object.entries(operation.changes).forEach(([path, value]) => { Object.entries(operation.updates).forEach(([path, value]) => {
this.setNestedProperty(node, path, value); this.setNestedProperty(node, path, value);
}); });
} }
@@ -545,18 +545,18 @@ export class WorkflowDiffEngine {
type: 'removeConnection', type: 'removeConnection',
source: operation.source, source: operation.source,
target: operation.target, target: operation.target,
sourceOutput: operation.changes.sourceOutput, sourceOutput: operation.updates.sourceOutput,
targetInput: operation.changes.targetInput targetInput: operation.updates.targetInput
}); });
this.applyAddConnection(workflow, { this.applyAddConnection(workflow, {
type: 'addConnection', type: 'addConnection',
source: operation.source, source: operation.source,
target: operation.target, target: operation.target,
sourceOutput: operation.changes.sourceOutput, sourceOutput: operation.updates.sourceOutput,
targetInput: operation.changes.targetInput, targetInput: operation.updates.targetInput,
sourceIndex: operation.changes.sourceIndex, sourceIndex: operation.updates.sourceIndex,
targetIndex: operation.changes.targetIndex targetIndex: operation.updates.targetIndex
}); });
} }

View File

@@ -31,7 +31,7 @@ export interface UpdateNodeOperation extends DiffOperation {
type: 'updateNode'; type: 'updateNode';
nodeId?: string; // Can use either ID or name nodeId?: string; // Can use either ID or name
nodeName?: string; nodeName?: string;
changes: { updates: {
[path: string]: any; // Dot notation paths like 'parameters.url' [path: string]: any; // Dot notation paths like 'parameters.url'
}; };
} }
@@ -78,7 +78,7 @@ export interface UpdateConnectionOperation extends DiffOperation {
type: 'updateConnection'; type: 'updateConnection';
source: string; source: string;
target: string; target: string;
changes: { updates: {
sourceOutput?: string; sourceOutput?: string;
targetInput?: string; targetInput?: string;
sourceIndex?: number; sourceIndex?: number;

View File

@@ -281,7 +281,7 @@ describe('WorkflowDiffEngine', () => {
const operation: UpdateNodeOperation = { const operation: UpdateNodeOperation = {
type: 'updateNode', type: 'updateNode',
nodeId: 'http-1', nodeId: 'http-1',
changes: { updates: {
'parameters.method': 'POST', 'parameters.method': 'POST',
'parameters.url': 'https://new-api.example.com' 'parameters.url': 'https://new-api.example.com'
} }
@@ -304,7 +304,7 @@ describe('WorkflowDiffEngine', () => {
const operation: UpdateNodeOperation = { const operation: UpdateNodeOperation = {
type: 'updateNode', type: 'updateNode',
nodeName: 'Slack', nodeName: 'Slack',
changes: { updates: {
'parameters.resource': 'channel', 'parameters.resource': 'channel',
'parameters.operation': 'create', 'parameters.operation': 'create',
'credentials.slackApi.name': 'New Slack Account' 'credentials.slackApi.name': 'New Slack Account'
@@ -329,7 +329,7 @@ describe('WorkflowDiffEngine', () => {
const operation: UpdateNodeOperation = { const operation: UpdateNodeOperation = {
type: 'updateNode', type: 'updateNode',
nodeId: 'non-existent', nodeId: 'non-existent',
changes: { updates: {
'parameters.test': 'value' 'parameters.test': 'value'
} }
}; };
@@ -617,7 +617,7 @@ describe('WorkflowDiffEngine', () => {
type: 'updateConnection', type: 'updateConnection',
source: 'IF', source: 'IF',
target: 'slack-1', target: 'slack-1',
changes: { updates: {
sourceOutput: 'false', sourceOutput: 'false',
sourceIndex: 0, sourceIndex: 0,
targetIndex: 0 targetIndex: 0
@@ -1039,7 +1039,7 @@ describe('WorkflowDiffEngine', () => {
const operation: UpdateNodeOperation = { const operation: UpdateNodeOperation = {
type: 'updateNode', type: 'updateNode',
nodeId: 'Webhook', // Using name as ID nodeId: 'Webhook', // Using name as ID
changes: { updates: {
'parameters.path': 'new-webhook-path' 'parameters.path': 'new-webhook-path'
} }
}; };