From eab3cc858eb9658ac02626cee9141933d29c124e Mon Sep 17 00:00:00 2001 From: czlonkowski <56956555+czlonkowski@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:39:24 +0200 Subject: [PATCH] fix: comprehensive error handling and node-level properties validation (fixes #26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: AI agents were placing error handling properties inside `parameters` instead of at node level Major changes: - Enhanced workflow validator to check for ALL node-level properties (expanded from 6 to 11) - Added validation for onError property values and deprecation warnings for continueOnFail - Updated all examples to use modern error handling (onError instead of continueOnFail) - Added comprehensive node-level properties documentation in tools_documentation - Enhanced MCP tool documentation for n8n_create_workflow and n8n_update_partial_workflow - Added test script demonstrating correct node-level property usage Node-level properties now validated: - credentials, disabled, notes, notesInFlow, executeOnce - onError, retryOnFail, maxTries, waitBetweenTries, alwaysOutputData - continueOnFail (deprecated) Validation improvements: - Detects misplaced properties and provides clear fix examples - Shows complete node structure when properties are incorrectly placed - Type validation for all node-level boolean and string properties - Smart error messages with correct placement guidance šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/CHANGELOG.md | 55 +++ package.json | 3 +- src/mcp/tools-documentation.ts | 169 ++++++++-- src/scripts/test-node-level-properties.ts | 200 +++++++++++ src/services/config-validator.ts | 4 +- src/services/enhanced-config-validator.ts | 61 +++- src/services/example-generator.ts | 195 ++++++++++- src/services/node-specific-validators.ts | 394 +++++++++++++++++----- src/services/task-templates.ts | 351 ++++++++++++++++++- src/services/workflow-diff-engine.ts | 1 + src/services/workflow-validator.ts | 340 +++++++++++++++++++ src/types/n8n-api.ts | 3 +- 12 files changed, 1638 insertions(+), 138 deletions(-) create mode 100755 src/scripts/test-node-level-properties.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c456085..61ebd47 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,61 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.7.11] - 2025-07-09 + +### Fixed +- **Issue #26**: Fixed critical issue where AI agents were placing error handling properties inside `parameters` instead of at node level + - Root cause: AI agents were confused by examples showing `parameters.path` updates and assumed all properties followed the same pattern + - Error handling properties (`onError`, `retryOnFail`, `maxTries`, `waitBetweenTries`, `alwaysOutputData`) must be placed at the NODE level + - Other node-level properties (`executeOnce`, `disabled`, `notes`, `notesInFlow`, `credentials`) were previously undocumented for AI agents + - Updated `n8n_create_workflow` and `n8n_update_partial_workflow` documentation with explicit examples and warnings + - Verified fix with workflows tGyHrsBNWtaK0inQ, usVP2XRXhI35m3Ts, and swuogdCCmNY7jj71 + +### Added +- **Comprehensive Node-Level Properties Reference** in tools documentation (`tools_documentation()`) + - Documents ALL available node-level properties with explanations + - Shows correct placement and usage for each property + - Provides complete example node configuration + - Accessible via `tools_documentation({depth: "full"})` for AI agents +- **Enhanced Workflow Validation** for additional node-level properties + - Now validates `executeOnce`, `disabled`, `notes`, `notesInFlow` types + - Checks for misplacement of ALL node-level properties (expanded from 6 to 11) + - Provides clear error messages with correct examples when properties are misplaced + - Shows specific fix with example node structure +- **Test Script** `test-node-level-properties.ts` demonstrating correct usage + - Shows all node-level properties in proper configuration + - Demonstrates common mistakes to avoid + - Validates workflow configurations + +### Enhanced +- **MCP Tool Documentation** significantly improved: + - `n8n_create_workflow` now includes complete node example with all properties + - `n8n_update_partial_workflow` shows difference between node-level vs parameter updates + - Added "CRITICAL" warnings about property placement + - Updated best practices and common pitfalls sections +- **Workflow Validator** improvements: + - Expanded property checking from 6 to 11 node-level properties + - Better error messages showing complete correct structure + - Type validation for all node-level boolean and string properties +- **Type Definitions** updated: + - Added `notesInFlow` to WorkflowNode interface in workflow-validator.ts + - Fixed credentials type from `Record` to `Record` in n8n-api.ts + +## [2.7.10] - 2025-07-09 + +### Documentation Update +- Added comprehensive documentation on how to update error handling properties using `n8n_update_partial_workflow` +- Error handling properties can be updated at the node level using the workflow diff engine: + - `continueOnFail`: boolean - Whether to continue workflow on node failure + - `onError`: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow' - Error handling strategy + - `retryOnFail`: boolean - Whether to retry on failure + - `maxTries`: number - Maximum retry attempts + - `waitBetweenTries`: number - Milliseconds to wait between retries + - `alwaysOutputData`: boolean - Always output data even on error +- Added test script demonstrating error handling property updates +- Updated WorkflowNode type to include `onError` property in n8n-api types +- Workflow diff engine now properly handles all error handling properties + ## [2.7.10] - 2025-07-07 ### Added diff --git a/package.json b/package.json index 9a7e816..11e97f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.7.10", + "version": "2.7.11", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "bin": { @@ -36,6 +36,7 @@ "test:n8n-manager": "node dist/scripts/test-n8n-manager-integration.js", "test:n8n-validate-workflow": "node dist/scripts/test-n8n-validate-workflow.js", "test:typeversion-validation": "node dist/scripts/test-typeversion-validation.js", + "test:error-handling": "node dist/scripts/test-error-handling-validation.js", "test:workflow-diff": "node dist/scripts/test-workflow-diff.js", "test:transactional-diff": "node dist/scripts/test-transactional-diff.js", "test:tools-documentation": "node dist/scripts/test-tools-documentation.js", diff --git a/src/mcp/tools-documentation.ts b/src/mcp/tools-documentation.ts index d65cf6a..d5e523e 100644 --- a/src/mcp/tools-documentation.ts +++ b/src/mcp/tools-documentation.ts @@ -315,11 +315,12 @@ export const toolsDocumentation: Record = { performance: 'API call - depends on n8n instance', tips: [ 'ALWAYS use node names in connections, never IDs', + 'Error handling properties go at NODE level, not inside parameters!', 'Requires N8N_API_URL and N8N_API_KEY configuration' ] }, full: { - description: 'Creates a new workflow in your n8n instance via API. Requires proper API configuration. Returns the created workflow with assigned ID.', + description: 'Creates a new workflow in your n8n instance via API. Requires proper API configuration. Returns the created workflow with assigned ID.\n\nāš ļø CRITICAL: Error handling properties (onError, retryOnFail, etc.) are NODE-LEVEL properties, not inside parameters!', parameters: { name: { type: 'string', description: 'Workflow name', required: true }, nodes: { type: 'array', description: 'Array of node configurations', required: true }, @@ -329,14 +330,61 @@ export const toolsDocumentation: Record = { }, returns: 'Created workflow object with id, name, nodes, connections, and metadata', examples: [ - `n8n_create_workflow({ - name: "Slack Notification", + `// Basic workflow with proper error handling +n8n_create_workflow({ + name: "Slack Notification with Error Handling", nodes: [ - {id: "1", name: "Webhook", type: "n8n-nodes-base.webhook", position: [250, 300]}, - {id: "2", name: "Slack", type: "n8n-nodes-base.slack", position: [450, 300], parameters: {...}} + { + id: "1", + name: "Webhook", + type: "n8n-nodes-base.webhook", + typeVersion: 2, + position: [250, 300], + parameters: { + path: "/webhook", + method: "POST" + }, + // āœ… CORRECT - Error handling at node level + onError: "continueRegularOutput" + }, + { + id: "2", + name: "Database Query", + type: "n8n-nodes-base.postgres", + typeVersion: 2.4, + position: [450, 300], + parameters: { + operation: "executeQuery", + query: "SELECT * FROM users" + }, + // āœ… CORRECT - Error handling at node level + onError: "continueErrorOutput", + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000 + }, + { + id: "3", + name: "Error Handler", + type: "n8n-nodes-base.slack", + typeVersion: 2.2, + position: [650, 450], + parameters: { + resource: "message", + operation: "post", + channel: "#errors", + text: "Database query failed!" + } + } ], connections: { - "Webhook": {main: [[{node: "Slack", type: "main", index: 0}]]} + "Webhook": { + main: [[{node: "Database Query", type: "main", index: 0}]] + }, + "Database Query": { + main: [[{node: "Success Handler", type: "main", index: 0}]], + error: [[{node: "Error Handler", type: "main", index: 0}]] // Error output + } } })` ], @@ -344,17 +392,20 @@ export const toolsDocumentation: Record = { 'Deploying workflows programmatically', 'Automating workflow creation', 'Migrating workflows between instances', - 'Creating workflows from templates' + 'Creating workflows from templates', + 'Building error-resilient workflows' ], performance: 'Depends on n8n instance and network. Typically 100-500ms.', bestPractices: [ 'CRITICAL: Use node NAMES in connections, not IDs', + 'CRITICAL: Place error handling at NODE level, not in parameters', 'Validate workflow before creating', 'Use meaningful workflow names', - 'Check n8n_health_check before creating', - 'Handle API errors gracefully' + 'Add error handling to external service nodes', + 'Check n8n_health_check before creating' ], pitfalls: [ + 'Placing error handling properties inside parameters object', 'Using node IDs in connections breaks UI display', 'Workflow not automatically activated', 'Tags must exist (use tag IDs not names)', @@ -370,15 +421,16 @@ export const toolsDocumentation: Record = { essentials: { description: 'Update workflows using diff operations - only send changes, not entire workflow', keyParameters: ['id', 'operations'], - example: 'n8n_update_partial_workflow({id: "123", operations: [{type: "updateNode", nodeId: "Slack", updates: {...}}]})', + example: 'n8n_update_partial_workflow({id: "123", operations: [{type: "updateNode", nodeName: "Slack", changes: {onError: "continueRegularOutput"}}]})', performance: '80-90% more efficient than full updates', tips: [ 'Maximum 5 operations per request', - 'Can reference nodes by name or ID' + 'Can reference nodes by name or ID', + 'Error handling properties go at NODE level, not inside parameters!' ] }, full: { - description: 'Update existing workflows using diff operations. Much more efficient than full updates as it only sends the changes. Supports 13 different operation types.', + description: 'Update existing workflows using diff operations. Much more efficient than full updates as it only sends the changes. Supports 13 different operation types.\n\nāš ļø CRITICAL: Error handling properties (onError, retryOnFail, maxTries, etc.) are NODE-LEVEL properties, not parameters!', parameters: { id: { type: 'string', description: 'Workflow ID to update', required: true }, operations: { type: 'array', description: 'Array of diff operations (max 5)', required: true }, @@ -386,29 +438,50 @@ export const toolsDocumentation: Record = { }, returns: 'Updated workflow with applied changes and operation results', examples: [ - `// Update node parameters + `// Update node parameters (properties inside parameters object) n8n_update_partial_workflow({ id: "123", operations: [{ type: "updateNode", - nodeId: "Slack", - updates: {parameters: {channel: "general"}} + nodeName: "Slack", + changes: { + "parameters.channel": "#general", // Nested property + "parameters.text": "Hello world" // Nested property + } }] })`, - `// Add connection between nodes + `// Update error handling (NODE-LEVEL properties, NOT inside parameters!) +n8n_update_partial_workflow({ + id: "123", + operations: [{ + type: "updateNode", + nodeName: "HTTP Request", + changes: { + onError: "continueErrorOutput", // āœ… Correct - node level + retryOnFail: true, // āœ… Correct - node level + maxTries: 3, // āœ… Correct - node level + waitBetweenTries: 2000 // āœ… Correct - node level + } + }] +})`, + `// WRONG - Don't put error handling inside parameters! +// āŒ BAD: changes: {"parameters.onError": "continueErrorOutput"} +// āœ… GOOD: changes: {onError: "continueErrorOutput"}`, + `// Add error connection between nodes n8n_update_partial_workflow({ id: "123", operations: [{ type: "addConnection", - from: "HTTP Request", - to: "Slack", - fromOutput: "main", - toInput: "main" + source: "Database Query", + target: "Error Handler", + sourceOutput: "error", // Error output + targetInput: "main" }] })` ], useCases: [ 'Updating node configurations', + 'Adding error handling to nodes', 'Adding/removing connections', 'Enabling/disabling nodes', 'Moving nodes in canvas', @@ -416,13 +489,14 @@ n8n_update_partial_workflow({ ], performance: 'Very efficient - only sends changes. 80-90% less data than full updates.', bestPractices: [ + 'Error handling properties (onError, retryOnFail, etc.) go at NODE level, not in parameters', + 'Use dot notation for nested properties: "parameters.url"', 'Batch related operations together', 'Use validateOnly:true to test first', - 'Reference nodes by name for clarity', - 'Keep under 5 operations per request', - 'Check operation results for success' + 'Reference nodes by name for clarity' ], pitfalls: [ + 'Placing error handling properties inside parameters (common mistake!)', 'Maximum 5 operations per request', 'Some operations have dependencies', 'Node must exist for update operations', @@ -578,6 +652,55 @@ validate_workflow(workflow) n8n_create_workflow(workflow) \`\`\` +### Node-Level Properties Reference +āš ļø **CRITICAL**: These properties go at the NODE level, not inside parameters! + +\`\`\`javascript +{ + // Required properties + "id": "unique_id", + "name": "Node Name", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [450, 300], + "parameters": { /* operation-specific params */ }, + + // Optional properties (all at node level!) + "credentials": { + "postgres": { + "id": "cred-id", + "name": "My Postgres" + } + }, + "disabled": false, // Disable node execution + "notes": "Internal note", // Node documentation + "notesInFlow": true, // Show notes on canvas + "executeOnce": true, // Execute only once per run + + // Error handling (at node level!) + "onError": "continueErrorOutput", // or "continueRegularOutput", "stopWorkflow" + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 2000, + "alwaysOutputData": true, + + // Deprecated (use onError instead) + "continueOnFail": false +} +\`\`\` + +**Common properties explained:** +- **credentials**: Links to credential sets (use credential ID and name) +- **disabled**: Node won't execute when true +- **notes**: Internal documentation for the node +- **notesInFlow**: Display notes on workflow canvas +- **executeOnce**: Execute node only once even with multiple input items +- **onError**: Modern error handling - what to do on failure +- **retryOnFail**: Automatically retry failed executions +- **maxTries**: Number of retry attempts (with retryOnFail) +- **waitBetweenTries**: Milliseconds between retries +- **alwaysOutputData**: Output data even on error (for debugging) + ### Using AI Tools Any node can be an AI tool! Connect it to an AI Agent's ai_tool port: \`\`\`javascript diff --git a/src/scripts/test-node-level-properties.ts b/src/scripts/test-node-level-properties.ts new file mode 100755 index 0000000..c85f38f --- /dev/null +++ b/src/scripts/test-node-level-properties.ts @@ -0,0 +1,200 @@ +#!/usr/bin/env node + +/** + * Test script demonstrating all node-level properties in n8n workflows + * Shows correct placement and usage of properties that must be at node level + */ + +import { createDatabaseAdapter } from '../database/database-adapter.js'; +import { NodeRepository } from '../database/node-repository.js'; +import { WorkflowValidator } from '../services/workflow-validator.js'; +import { WorkflowDiffEngine } from '../services/workflow-diff-engine.js'; +import { join } from 'path'; + +async function main() { + console.log('šŸ” Testing Node-Level Properties Configuration\n'); + + // Initialize database + const dbPath = join(process.cwd(), 'nodes.db'); + const dbAdapter = await createDatabaseAdapter(dbPath); + const nodeRepository = new NodeRepository(dbAdapter); + const EnhancedConfigValidator = (await import('../services/enhanced-config-validator.js')).EnhancedConfigValidator; + const validator = new WorkflowValidator(nodeRepository, EnhancedConfigValidator); + const diffEngine = new WorkflowDiffEngine(); + + // Example 1: Complete node with all properties + console.log('1ļøāƒ£ Complete Node Configuration Example:'); + const completeNode = { + id: 'node_1', + name: 'Database Query', + type: 'n8n-nodes-base.postgres', + typeVersion: 2.6, + position: [450, 300] as [number, number], + + // Operation parameters (inside parameters) + parameters: { + operation: 'executeQuery', + query: 'SELECT * FROM users WHERE active = true' + }, + + // Node-level properties (NOT inside parameters!) + credentials: { + postgres: { + id: 'cred_123', + name: 'Production Database' + } + }, + disabled: false, + notes: 'This node queries active users from the production database', + notesInFlow: true, + executeOnce: true, + + // Error handling (also at node level!) + onError: 'continueErrorOutput' as const, + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000, + alwaysOutputData: true + }; + + console.log(JSON.stringify(completeNode, null, 2)); + console.log('\nāœ… All properties are at the correct level!\n'); + + // Example 2: Workflow with properly configured nodes + console.log('2ļøāƒ£ Complete Workflow Example:'); + const workflow = { + name: 'Production Data Processing', + nodes: [ + { + id: 'trigger_1', + name: 'Every Hour', + type: 'n8n-nodes-base.scheduleTrigger', + typeVersion: 1.2, + position: [250, 300] as [number, number], + parameters: { + rule: { interval: [{ field: 'hours', hoursInterval: 1 }] } + }, + notes: 'Runs every hour to check for new data', + notesInFlow: true + }, + completeNode, + { + id: 'error_handler', + name: 'Error Notification', + type: 'n8n-nodes-base.slack', + typeVersion: 2.3, + position: [650, 450] as [number, number], + parameters: { + resource: 'message', + operation: 'post', + channel: '#alerts', + text: 'Database query failed!' + }, + credentials: { + slackApi: { + id: 'cred_456', + name: 'Alert Slack' + } + }, + executeOnce: true, + onError: 'continueRegularOutput' as const + } + ], + connections: { + 'Every Hour': { + main: [[{ node: 'Database Query', type: 'main', index: 0 }]] + }, + 'Database Query': { + main: [[{ node: 'Process Data', type: 'main', index: 0 }]], + error: [[{ node: 'Error Notification', type: 'main', index: 0 }]] + } + } + }; + + // Validate the workflow + console.log('\n3ļøāƒ£ Validating Workflow:'); + const result = await validator.validateWorkflow(workflow as any, { profile: 'strict' }); + console.log(`Valid: ${result.valid}`); + console.log(`Errors: ${result.errors.length}`); + console.log(`Warnings: ${result.warnings.length}`); + + if (result.errors.length > 0) { + console.log('\nErrors:'); + result.errors.forEach((err: any) => console.log(`- ${err.message}`)); + } + + // Example 3: Using workflow diff to update node-level properties + console.log('\n4ļøāƒ£ Updating Node-Level Properties with Diff Engine:'); + const operations = [ + { + type: 'updateNode' as const, + nodeName: 'Database Query', + changes: { + // Update operation parameters + 'parameters.query': 'SELECT * FROM users WHERE active = true AND created_at > NOW() - INTERVAL \'7 days\'', + + // Update node-level properties (no 'parameters.' prefix!) + 'onError': 'stopWorkflow', + 'executeOnce': false, + 'notes': 'Updated to only query users from last 7 days', + 'maxTries': 5, + 'disabled': false + } + } + ]; + + console.log('Operations:'); + console.log(JSON.stringify(operations, null, 2)); + + // Example 4: Common mistakes to avoid + console.log('\n5ļøāƒ£ āŒ COMMON MISTAKES TO AVOID:'); + + const wrongNode = { + id: 'wrong_1', + name: 'Wrong Configuration', + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [250, 300] as [number, number], + parameters: { + method: 'POST', + url: 'https://api.example.com', + // āŒ WRONG - These should NOT be inside parameters! + onError: 'continueErrorOutput', + retryOnFail: true, + executeOnce: true, + notes: 'This is wrong!', + credentials: { httpAuth: { id: '123' } } + } + }; + + console.log('āŒ Wrong (properties inside parameters):'); + console.log(JSON.stringify(wrongNode.parameters, null, 2)); + + // Validate wrong configuration + const wrongWorkflow = { + name: 'Wrong Example', + nodes: [wrongNode], + connections: {} + }; + + const wrongResult = await validator.validateWorkflow(wrongWorkflow as any); + console.log('\nValidation of wrong configuration:'); + wrongResult.errors.forEach((err: any) => console.log(`āŒ ERROR: ${err.message}`)); + + console.log('\nāœ… Summary of Node-Level Properties:'); + console.log('- credentials: Link to credential sets'); + console.log('- disabled: Disable node execution'); + console.log('- notes: Internal documentation'); + console.log('- notesInFlow: Show notes on canvas'); + console.log('- executeOnce: Execute only once per run'); + console.log('- onError: Error handling strategy'); + console.log('- retryOnFail: Enable automatic retries'); + console.log('- maxTries: Number of retry attempts'); + console.log('- waitBetweenTries: Delay between retries'); + console.log('- alwaysOutputData: Output data on error'); + console.log('- continueOnFail: (deprecated - use onError)'); + + console.log('\nšŸŽÆ Remember: All these properties go at the NODE level, not inside parameters!'); +} + +main().catch(console.error); \ No newline at end of file diff --git a/src/services/config-validator.ts b/src/services/config-validator.ts index 125d5df..12278ed 100644 --- a/src/services/config-validator.ts +++ b/src/services/config-validator.ts @@ -16,14 +16,14 @@ export interface ValidationResult { } export interface ValidationError { - type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible'; + type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible' | 'invalid_configuration'; property: string; message: string; fix?: string; } export interface ValidationWarning { - type: 'missing_common' | 'deprecated' | 'inefficient' | 'security'; + type: 'missing_common' | 'deprecated' | 'inefficient' | 'security' | 'best_practice' | 'invalid_value'; property?: string; message: string; suggestion?: string; diff --git a/src/services/enhanced-config-validator.ts b/src/services/enhanced-config-validator.ts index 0c3f22d..41fba57 100644 --- a/src/services/enhanced-config-validator.ts +++ b/src/services/enhanced-config-validator.ts @@ -491,9 +491,11 @@ export class EnhancedConfigValidator extends ConfigValidator { case 'strict': // Keep everything, add more suggestions if (result.warnings.length === 0 && result.errors.length === 0) { - result.suggestions.push('Consider adding error handling and timeout configuration'); + result.suggestions.push('Consider adding error handling with onError property and timeout configuration'); result.suggestions.push('Add authentication if connecting to external services'); } + // Require error handling for external service nodes + this.enforceErrorHandlingForProfile(result, profile); break; case 'ai-friendly': @@ -503,7 +505,64 @@ export class EnhancedConfigValidator extends ConfigValidator { result.warnings = result.warnings.filter(w => w.type !== 'inefficient' || !w.property?.startsWith('_') ); + // Add error handling suggestions for AI-friendly profile + this.addErrorHandlingSuggestions(result); break; } } + + /** + * Enforce error handling requirements based on profile + */ + private static enforceErrorHandlingForProfile( + result: EnhancedValidationResult, + profile: ValidationProfile + ): void { + // Only enforce for strict profile on external service nodes + if (profile !== 'strict') return; + + const nodeType = result.operation?.resource || ''; + const errorProneTypes = ['httpRequest', 'webhook', 'database', 'api', 'slack', 'email', 'openai']; + + if (errorProneTypes.some(type => nodeType.toLowerCase().includes(type))) { + // Add general warning for strict profile + // The actual error handling validation is done in node-specific validators + result.warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'External service nodes should have error handling configured', + suggestion: 'Add onError: "continueRegularOutput" or "stopWorkflow" with retryOnFail: true for resilience' + }); + } + } + + /** + * Add error handling suggestions for AI-friendly profile + */ + private static addErrorHandlingSuggestions( + result: EnhancedValidationResult + ): void { + // Check if there are any network/API related errors + const hasNetworkErrors = result.errors.some(e => + e.message.toLowerCase().includes('url') || + e.message.toLowerCase().includes('endpoint') || + e.message.toLowerCase().includes('api') + ); + + if (hasNetworkErrors) { + result.suggestions.push( + 'For API calls, consider adding onError: "continueRegularOutput" with retryOnFail: true and maxTries: 3' + ); + } + + // Check for webhook configurations + const isWebhook = result.operation?.resource === 'webhook' || + result.errors.some(e => e.message.toLowerCase().includes('webhook')); + + if (isWebhook) { + result.suggestions.push( + 'Webhooks should use onError: "continueRegularOutput" to ensure responses are always sent' + ); + } + } } \ No newline at end of file diff --git a/src/services/example-generator.ts b/src/services/example-generator.ts index 405e1f5..6d11430 100644 --- a/src/services/example-generator.ts +++ b/src/services/example-generator.ts @@ -47,7 +47,13 @@ export class ExampleGenerator { sendBody: true, contentType: 'json', specifyBody: 'json', - jsonBody: '{\n "action": "update",\n "data": {}\n}' + jsonBody: '{\n "action": "update",\n "data": {}\n}', + // Error handling for API calls + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 1000, + alwaysOutputData: true } }, @@ -62,7 +68,10 @@ export class ExampleGenerator { httpMethod: 'POST', responseMode: 'lastNode', responseData: 'allEntries', - responseCode: 200 + responseCode: 200, + // Webhooks should continue on fail to avoid blocking responses + onError: 'continueRegularOutput', + alwaysOutputData: true } }, @@ -220,7 +229,12 @@ DO UPDATE SET RETURNING *;`, additionalFields: { queryParams: '={{ $json.name }},{{ $json.email }},active' - } + }, + // Database operations should retry on connection errors + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000, + onError: 'continueErrorOutput' } }, @@ -258,7 +272,13 @@ RETURNING *;`, options: { maxTokens: 150, temperature: 0.7 - } + }, + // AI calls should handle rate limits and transient errors + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 5000, + onError: 'continueRegularOutput', + alwaysOutputData: true } }, @@ -325,7 +345,12 @@ RETURNING *;`, ] } } - ] + ], + // Messaging services should handle rate limits + retryOnFail: true, + maxTries: 2, + waitBetweenTries: 3000, + onError: 'continueRegularOutput' } }, @@ -347,7 +372,12 @@ RETURNING *;`,

Best regards,
The Team

`, options: { ccEmail: 'admin@company.com' - } + }, + // Email sending should handle transient failures + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000, + onError: 'continueRegularOutput' } }, @@ -427,7 +457,12 @@ return processedItems;` options: { upsert: true, returnNewDocument: true - } + }, + // NoSQL operations should handle connection issues + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 1000, + onError: 'continueErrorOutput' } }, @@ -443,7 +478,12 @@ return processedItems;` columns: 'customer_id,product_id,quantity,order_date', options: { queryBatching: 'independently' - } + }, + // Database writes should handle connection errors + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000, + onError: 'stopWorkflow' } }, @@ -514,6 +554,145 @@ return processedItems;` assignees: ['maintainer'], labels: ['bug', 'needs-triage'] } + }, + + // Error Handling Examples and Patterns + 'error-handling.modern-patterns': { + minimal: { + // Basic error handling - continue on error + onError: 'continueRegularOutput' + }, + common: { + // Use error output for special handling + onError: 'continueErrorOutput', + alwaysOutputData: true + }, + advanced: { + // Stop workflow on critical errors + onError: 'stopWorkflow', + // But retry first + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000 + } + }, + + 'error-handling.api-with-retry': { + minimal: { + url: 'https://api.example.com/data', + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 1000 + }, + common: { + method: 'GET', + url: 'https://api.example.com/users/{{ $json.userId }}', + retryOnFail: true, + maxTries: 5, + waitBetweenTries: 2000, + alwaysOutputData: true, + // Headers for better debugging + sendHeaders: true, + headerParameters: { + parameters: [ + { + name: 'X-Request-ID', + value: '={{ $workflow.id }}-{{ $execution.id }}' + } + ] + } + }, + advanced: { + method: 'POST', + url: 'https://api.example.com/critical-operation', + sendBody: true, + contentType: 'json', + specifyBody: 'json', + jsonBody: '{{ JSON.stringify($json) }}', + // Exponential backoff pattern + retryOnFail: true, + maxTries: 5, + waitBetweenTries: 1000, + // Always output for debugging + alwaysOutputData: true, + // Stop workflow on error for critical operations + onError: 'stopWorkflow' + } + }, + + 'error-handling.fault-tolerant': { + minimal: { + // For non-critical operations + onError: 'continueRegularOutput' + }, + common: { + // Data processing that shouldn't stop the workflow + onError: 'continueRegularOutput', + alwaysOutputData: true + }, + advanced: { + // Combination for resilient processing + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 2, + waitBetweenTries: 500, + alwaysOutputData: true + } + }, + + 'error-handling.database-patterns': { + minimal: { + // Database reads can continue on error + onError: 'continueRegularOutput', + alwaysOutputData: true + }, + common: { + // Database writes should retry then stop + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000, + onError: 'stopWorkflow' + }, + advanced: { + // Transaction-safe operations + onError: 'continueErrorOutput', + retryOnFail: false, // Don't retry transactions + alwaysOutputData: true + } + }, + + 'error-handling.webhook-patterns': { + minimal: { + // Always respond to webhooks + onError: 'continueRegularOutput', + alwaysOutputData: true + }, + common: { + // Process errors separately + onError: 'continueErrorOutput', + alwaysOutputData: true, + // Add custom error response + responseCode: 200, + responseData: 'allEntries' + } + }, + + 'error-handling.ai-patterns': { + minimal: { + // AI calls should handle rate limits + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 5000, + onError: 'continueRegularOutput' + }, + common: { + // Exponential backoff for rate limits + retryOnFail: true, + maxTries: 5, + waitBetweenTries: 2000, + onError: 'continueRegularOutput', + alwaysOutputData: true + } } }; diff --git a/src/services/node-specific-validators.ts b/src/services/node-specific-validators.ts index 1c82735..377fb82 100644 --- a/src/services/node-specific-validators.ts +++ b/src/services/node-specific-validators.ts @@ -20,7 +20,7 @@ export class NodeSpecificValidators { * Validate Slack node configuration with operation awareness */ static validateSlack(context: NodeValidationContext): void { - const { config, errors, warnings, suggestions } = context; + const { config, errors, warnings, suggestions, autofix } = context; const { resource, operation } = config; // Message operations @@ -62,6 +62,30 @@ export class NodeSpecificValidators { }); } } + + // Error handling for Slack operations + if (!config.onError && !config.retryOnFail && !config.continueOnFail) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'Slack API can have rate limits and transient failures', + suggestion: 'Add onError: "continueRegularOutput" with retryOnFail for resilience' + }); + autofix.onError = 'continueRegularOutput'; + autofix.retryOnFail = true; + autofix.maxTries = 2; + autofix.waitBetweenTries = 3000; // Slack rate limits + } + + // Check for deprecated continueOnFail + if (config.continueOnFail !== undefined) { + warnings.push({ + type: 'deprecated', + property: 'continueOnFail', + message: 'continueOnFail is deprecated. Use onError instead', + suggestion: 'Replace with onError: "continueRegularOutput"' + }); + } } private static validateSlackSendMessage(context: NodeValidationContext): void { @@ -376,7 +400,7 @@ export class NodeSpecificValidators { * Validate OpenAI node configuration */ static validateOpenAI(context: NodeValidationContext): void { - const { config, errors, warnings, suggestions } = context; + const { config, errors, warnings, suggestions, autofix } = context; const { resource, operation } = config; if (resource === 'chat' && operation === 'create') { @@ -433,13 +457,38 @@ export class NodeSpecificValidators { } } } + + // Error handling for AI API calls + if (!config.onError && !config.retryOnFail && !config.continueOnFail) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'AI APIs have rate limits and can return errors', + suggestion: 'Add onError: "continueRegularOutput" with retryOnFail and longer wait times' + }); + autofix.onError = 'continueRegularOutput'; + autofix.retryOnFail = true; + autofix.maxTries = 3; + autofix.waitBetweenTries = 5000; // Longer wait for rate limits + autofix.alwaysOutputData = true; + } + + // Check for deprecated continueOnFail + if (config.continueOnFail !== undefined) { + warnings.push({ + type: 'deprecated', + property: 'continueOnFail', + message: 'continueOnFail is deprecated. Use onError instead', + suggestion: 'Replace with onError: "continueRegularOutput"' + }); + } } /** * Validate MongoDB node configuration */ static validateMongoDB(context: NodeValidationContext): void { - const { config, errors, warnings } = context; + const { config, errors, warnings, autofix } = context; const { operation } = config; // Collection is always required @@ -501,91 +550,44 @@ export class NodeSpecificValidators { } break; } + + // Error handling for MongoDB operations + if (!config.onError && !config.retryOnFail && !config.continueOnFail) { + if (operation === 'find') { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'MongoDB queries can fail due to connection issues', + suggestion: 'Add onError: "continueRegularOutput" with retryOnFail' + }); + autofix.onError = 'continueRegularOutput'; + autofix.retryOnFail = true; + autofix.maxTries = 3; + } else if (['insert', 'update', 'delete'].includes(operation)) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'MongoDB write operations should handle errors carefully', + suggestion: 'Add onError: "continueErrorOutput" to handle write failures separately' + }); + autofix.onError = 'continueErrorOutput'; + autofix.retryOnFail = true; + autofix.maxTries = 2; + autofix.waitBetweenTries = 1000; + } + } + + // Check for deprecated continueOnFail + if (config.continueOnFail !== undefined) { + warnings.push({ + type: 'deprecated', + property: 'continueOnFail', + message: 'continueOnFail is deprecated. Use onError instead', + suggestion: 'Replace with onError: "continueRegularOutput" or "continueErrorOutput"' + }); + } } - /** - * Validate Webhook node configuration - */ - static validateWebhook(context: NodeValidationContext): void { - const { config, errors, warnings, suggestions } = context; - - // Path validation - if (!config.path) { - errors.push({ - type: 'missing_required', - property: 'path', - message: 'Webhook path is required', - fix: 'Set a unique path like "my-webhook" (no leading slash)' - }); - } else { - const path = config.path; - - // Check for leading slash - if (path.startsWith('/')) { - warnings.push({ - type: 'inefficient', - property: 'path', - message: 'Webhook path should not start with /', - suggestion: 'Remove the leading slash: use "my-webhook" instead of "/my-webhook"' - }); - } - - // Check for spaces - if (path.includes(' ')) { - errors.push({ - type: 'invalid_value', - property: 'path', - message: 'Webhook path cannot contain spaces', - fix: 'Replace spaces with hyphens or underscores' - }); - } - - // Check for special characters - if (!/^[a-zA-Z0-9\-_\/]+$/.test(path.replace(/^\//, ''))) { - warnings.push({ - type: 'inefficient', - property: 'path', - message: 'Webhook path contains special characters', - suggestion: 'Use only letters, numbers, hyphens, and underscores' - }); - } - } - - // Response mode validation - if (config.responseMode === 'responseNode') { - suggestions.push('Add a "Respond to Webhook" node to send custom responses'); - - if (!config.responseData) { - warnings.push({ - type: 'missing_common', - property: 'responseData', - message: 'Response data not configured for responseNode mode', - suggestion: 'Add a "Respond to Webhook" node or change responseMode' - }); - } - } - - // HTTP method validation - if (config.httpMethod && Array.isArray(config.httpMethod)) { - if (config.httpMethod.length === 0) { - errors.push({ - type: 'invalid_value', - property: 'httpMethod', - message: 'At least one HTTP method must be selected', - fix: 'Select GET, POST, or other methods your webhook should accept' - }); - } - } - - // Authentication warnings - if (!config.authentication || config.authentication === 'none') { - warnings.push({ - type: 'security', - message: 'Webhook has no authentication', - suggestion: 'Consider adding authentication to prevent unauthorized access' - }); - } - } /** * Validate Postgres node configuration @@ -677,6 +679,42 @@ export class NodeSpecificValidators { if (config.connectionTimeout === undefined) { suggestions.push('Consider setting connectionTimeout to handle slow connections'); } + + // Error handling for database operations + if (!config.onError && !config.retryOnFail && !config.continueOnFail) { + if (operation === 'execute' && config.query?.toLowerCase().includes('select')) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'Database reads can fail due to connection issues', + suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true' + }); + autofix.onError = 'continueRegularOutput'; + autofix.retryOnFail = true; + autofix.maxTries = 3; + } else if (['insert', 'update', 'delete'].includes(operation)) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'Database writes should handle errors carefully', + suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures' + }); + autofix.onError = 'stopWorkflow'; + autofix.retryOnFail = true; + autofix.maxTries = 2; + autofix.waitBetweenTries = 2000; + } + } + + // Check for deprecated continueOnFail + if (config.continueOnFail !== undefined) { + warnings.push({ + type: 'deprecated', + property: 'continueOnFail', + message: 'continueOnFail is deprecated. Use onError instead', + suggestion: 'Replace with onError: "continueRegularOutput" or "stopWorkflow"' + }); + } } /** @@ -751,6 +789,25 @@ export class NodeSpecificValidators { if (config.timezone === undefined) { suggestions.push('Consider setting timezone to ensure consistent date/time handling'); } + + // Error handling for MySQL operations (similar to Postgres) + if (!config.onError && !config.retryOnFail && !config.continueOnFail) { + if (operation === 'execute' && config.query?.toLowerCase().includes('select')) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'Database queries can fail due to connection issues', + suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true' + }); + } else if (['insert', 'update', 'delete'].includes(operation)) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'Database modifications should handle errors carefully', + suggestion: 'Add onError: "stopWorkflow" with retryOnFail for transient failures' + }); + } + } } /** @@ -834,4 +891,169 @@ export class NodeSpecificValidators { } } } + + /** + * Validate HTTP Request node configuration with error handling awareness + */ + static validateHttpRequest(context: NodeValidationContext): void { + const { config, errors, warnings, suggestions, autofix } = context; + const { method = 'GET', url, sendBody, authentication } = config; + + // Basic URL validation + if (!url) { + errors.push({ + type: 'missing_required', + property: 'url', + message: 'URL is required for HTTP requests', + fix: 'Provide the full URL including protocol (https://...)' + }); + } else if (!url.startsWith('http://') && !url.startsWith('https://') && !url.includes('{{')) { + warnings.push({ + type: 'invalid_value', + property: 'url', + message: 'URL should start with http:// or https://', + suggestion: 'Use https:// for secure connections' + }); + } + + // Method-specific validation + if (['POST', 'PUT', 'PATCH'].includes(method) && !sendBody) { + warnings.push({ + type: 'missing_common', + property: 'sendBody', + message: `${method} requests typically include a body`, + suggestion: 'Set sendBody: true and configure the body content' + }); + } + + // Error handling recommendations + if (!config.retryOnFail && !config.onError && !config.continueOnFail) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'HTTP requests can fail due to network issues or server errors', + suggestion: 'Add onError: "continueRegularOutput" and retryOnFail: true for resilience' + }); + + // Auto-fix suggestion for error handling + autofix.onError = 'continueRegularOutput'; + autofix.retryOnFail = true; + autofix.maxTries = 3; + autofix.waitBetweenTries = 1000; + } + + // Check for deprecated continueOnFail + if (config.continueOnFail !== undefined) { + warnings.push({ + type: 'deprecated', + property: 'continueOnFail', + message: 'continueOnFail is deprecated. Use onError instead', + suggestion: 'Replace with onError: "continueRegularOutput"' + }); + autofix.onError = config.continueOnFail ? 'continueRegularOutput' : 'stopWorkflow'; + delete autofix.continueOnFail; + } + + // Check retry configuration + if (config.retryOnFail) { + // Validate retry settings + if (!['GET', 'HEAD', 'OPTIONS'].includes(method) && (!config.maxTries || config.maxTries > 3)) { + warnings.push({ + type: 'best_practice', + property: 'maxTries', + message: `${method} requests might not be idempotent. Use fewer retries.`, + suggestion: 'Set maxTries: 2 for non-idempotent operations' + }); + } + + // Suggest alwaysOutputData for debugging + if (!config.alwaysOutputData) { + suggestions.push('Enable alwaysOutputData to capture error responses for debugging'); + autofix.alwaysOutputData = true; + } + } + + // Authentication warnings + if (url && url.includes('api') && !authentication) { + warnings.push({ + type: 'security', + property: 'authentication', + message: 'API endpoints typically require authentication', + suggestion: 'Configure authentication method (Bearer token, API key, etc.)' + }); + } + + // Timeout recommendations + if (!config.timeout) { + suggestions.push('Consider setting a timeout to prevent hanging requests'); + } + } + + /** + * Validate Webhook node configuration with error handling + */ + static validateWebhook(context: NodeValidationContext): void { + const { config, errors, warnings, suggestions, autofix } = context; + const { path, httpMethod = 'POST', responseMode } = config; + + // Path validation + if (!path) { + errors.push({ + type: 'missing_required', + property: 'path', + message: 'Webhook path is required', + fix: 'Provide a unique path like "my-webhook" or "github-events"' + }); + } else if (path.startsWith('/')) { + warnings.push({ + type: 'invalid_value', + property: 'path', + message: 'Webhook path should not start with /', + suggestion: 'Use "webhook-name" instead of "/webhook-name"' + }); + } + + // Error handling for webhooks + if (!config.onError && !config.continueOnFail) { + warnings.push({ + type: 'best_practice', + property: 'onError', + message: 'Webhooks should always send a response, even on error', + suggestion: 'Set onError: "continueRegularOutput" to ensure webhook responses' + }); + autofix.onError = 'continueRegularOutput'; + } + + // Check for deprecated continueOnFail in webhooks + if (config.continueOnFail !== undefined) { + warnings.push({ + type: 'deprecated', + property: 'continueOnFail', + message: 'continueOnFail is deprecated. Use onError instead', + suggestion: 'Replace with onError: "continueRegularOutput"' + }); + autofix.onError = 'continueRegularOutput'; + delete autofix.continueOnFail; + } + + // Response mode validation + if (responseMode === 'responseNode' && !config.onError && !config.continueOnFail) { + errors.push({ + type: 'invalid_configuration', + property: 'responseMode', + message: 'responseNode mode requires onError: "continueRegularOutput"', + fix: 'Set onError to ensure response is always sent' + }); + } + + // Always output data for debugging + if (!config.alwaysOutputData) { + suggestions.push('Enable alwaysOutputData to debug webhook payloads'); + autofix.alwaysOutputData = true; + } + + // Security suggestions + suggestions.push('Consider adding webhook validation (HMAC signature verification)'); + suggestions.push('Implement rate limiting for public webhooks'); + } } \ No newline at end of file diff --git a/src/services/task-templates.ts b/src/services/task-templates.ts index cb708e2..77eed0b 100644 --- a/src/services/task-templates.ts +++ b/src/services/task-templates.ts @@ -33,7 +33,13 @@ export class TaskTemplates { configuration: { method: 'GET', url: '', - authentication: 'none' + authentication: 'none', + // Default error handling for API calls + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 1000, + alwaysOutputData: true }, userMustProvide: [ { @@ -52,6 +58,11 @@ export class TaskTemplates { property: 'sendHeaders', description: 'Add custom headers if needed', when: 'API requires specific headers' + }, + { + property: 'alwaysOutputData', + description: 'Set to true to capture error responses', + when: 'Need to debug API errors' } ] }, @@ -66,7 +77,13 @@ export class TaskTemplates { sendBody: true, contentType: 'json', specifyBody: 'json', - jsonBody: '' + jsonBody: '', + // POST requests might modify data, so be careful with retries + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 2, + waitBetweenTries: 1000, + alwaysOutputData: true }, userMustProvide: [ { @@ -84,11 +101,17 @@ export class TaskTemplates { { property: 'authentication', description: 'Add authentication if required' + }, + { + property: 'onError', + description: 'Set to "continueRegularOutput" for non-critical operations', + when: 'Failure should not stop the workflow' } ], notes: [ 'Make sure jsonBody contains valid JSON', - 'Content-Type header is automatically set to application/json' + 'Content-Type header is automatically set to application/json', + 'Be careful with retries on non-idempotent operations' ] }, @@ -102,6 +125,12 @@ export class TaskTemplates { authentication: 'genericCredentialType', genericAuthType: 'headerAuth', sendHeaders: true, + // Authentication calls should handle auth failures gracefully + onError: 'continueErrorOutput', + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000, + alwaysOutputData: true, headerParameters: { parameters: [ { @@ -144,7 +173,10 @@ export class TaskTemplates { httpMethod: 'POST', path: 'webhook', responseMode: 'lastNode', - responseData: 'allEntries' + responseData: 'allEntries', + // Webhooks should always respond, even on error + onError: 'continueRegularOutput', + alwaysOutputData: true }, userMustProvide: [ { @@ -178,7 +210,10 @@ export class TaskTemplates { path: 'webhook', responseMode: 'responseNode', responseData: 'firstEntryJson', - responseCode: 200 + responseCode: 200, + // Ensure webhook always sends response + onError: 'continueRegularOutput', + alwaysOutputData: true }, userMustProvide: [ { @@ -199,7 +234,12 @@ export class TaskTemplates { nodeType: 'nodes-base.postgres', configuration: { operation: 'executeQuery', - query: '' + query: '', + // Database reads can continue on error + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 1000 }, userMustProvide: [ { @@ -229,7 +269,12 @@ export class TaskTemplates { operation: 'insert', table: '', columns: '', - returnFields: '*' + returnFields: '*', + // Database writes should stop on error by default + onError: 'stopWorkflow', + retryOnFail: true, + maxTries: 2, + waitBetweenTries: 1000 }, userMustProvide: [ { @@ -265,7 +310,13 @@ export class TaskTemplates { content: '' } ] - } + }, + // AI calls should handle rate limits and API errors + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 5000, + alwaysOutputData: true }, userMustProvide: [ { @@ -393,7 +444,12 @@ return results;` resource: 'message', operation: 'post', channel: '', - text: '' + text: '', + // Messaging can continue on error + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 2, + waitBetweenTries: 2000 }, userMustProvide: [ { @@ -427,7 +483,13 @@ return results;` fromEmail: '', toEmail: '', subject: '', - text: '' + text: '', + // Email sending should retry on transient failures + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 3000, + alwaysOutputData: true }, userMustProvide: [ { @@ -562,6 +624,255 @@ return results;` 'Test each tool individually before combining', 'Set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true for community nodes' ] + }, + + // Error Handling Templates + 'api_call_with_retry': { + task: 'api_call_with_retry', + description: 'Resilient API call with automatic retry on failure', + nodeType: 'nodes-base.httpRequest', + configuration: { + method: 'GET', + url: '', + // Retry configuration for transient failures + retryOnFail: true, + maxTries: 5, + waitBetweenTries: 2000, + // Always capture response for debugging + alwaysOutputData: true, + // Add request tracking + sendHeaders: true, + headerParameters: { + parameters: [ + { + name: 'X-Request-ID', + value: '={{ $workflow.id }}-{{ $itemIndex }}' + } + ] + } + }, + userMustProvide: [ + { + property: 'url', + description: 'The API endpoint to call', + example: 'https://api.example.com/resource/{{ $json.id }}' + } + ], + optionalEnhancements: [ + { + property: 'authentication', + description: 'Add API authentication' + }, + { + property: 'onError', + description: 'Change to "stopWorkflow" for critical API calls', + when: 'This is a critical API call that must succeed' + } + ], + notes: [ + 'Retries help with rate limits and transient network issues', + 'waitBetweenTries prevents hammering the API', + 'alwaysOutputData captures error responses for debugging', + 'Consider exponential backoff for production use' + ] + }, + + 'fault_tolerant_processing': { + task: 'fault_tolerant_processing', + description: 'Data processing that continues despite individual item failures', + nodeType: 'nodes-base.code', + configuration: { + language: 'javaScript', + jsCode: `// Process items with error handling +const results = []; + +for (const item of items) { + try { + // Your processing logic here + const processed = { + ...item.json, + processed: true, + timestamp: new Date().toISOString() + }; + + results.push({ json: processed }); + } catch (error) { + // Log error but continue processing + console.error('Processing failed for item:', item.json.id, error); + + // Add error item to results + results.push({ + json: { + ...item.json, + error: error.message, + processed: false + } + }); + } +} + +return results;`, + // Continue workflow even if code fails entirely + onError: 'continueRegularOutput', + alwaysOutputData: true + }, + userMustProvide: [ + { + property: 'Processing logic', + description: 'Replace the comment with your data transformation logic' + } + ], + optionalEnhancements: [ + { + property: 'Error notification', + description: 'Add IF node after to handle error items separately' + } + ], + notes: [ + 'Individual item failures won\'t stop processing of other items', + 'Error items are marked and can be handled separately', + 'continueOnFail ensures workflow continues even on total failure' + ] + }, + + 'webhook_with_error_handling': { + task: 'webhook_with_error_handling', + description: 'Webhook that gracefully handles processing errors', + nodeType: 'nodes-base.webhook', + configuration: { + httpMethod: 'POST', + path: 'resilient-webhook', + responseMode: 'responseNode', + responseData: 'firstEntryJson', + // Always continue to ensure response is sent + onError: 'continueRegularOutput', + alwaysOutputData: true + }, + userMustProvide: [ + { + property: 'path', + description: 'Unique webhook path', + example: 'order-processor' + }, + { + property: 'Respond to Webhook node', + description: 'Add node to send appropriate success/error responses' + } + ], + optionalEnhancements: [ + { + property: 'Validation', + description: 'Add IF node to validate webhook payload' + }, + { + property: 'Error logging', + description: 'Add error handler node for failed requests' + } + ], + notes: [ + 'onError: continueRegularOutput ensures webhook always sends a response', + 'Use Respond to Webhook node to send appropriate status codes', + 'Log errors but don\'t expose internal errors to webhook callers', + 'Consider rate limiting for public webhooks' + ] + }, + + // Modern Error Handling Patterns + 'modern_error_handling_patterns': { + task: 'modern_error_handling_patterns', + description: 'Examples of modern error handling using onError property', + nodeType: 'nodes-base.httpRequest', + configuration: { + method: 'GET', + url: '', + // Modern error handling approach + onError: 'continueRegularOutput', // Options: continueRegularOutput, continueErrorOutput, stopWorkflow + retryOnFail: true, + maxTries: 3, + waitBetweenTries: 2000, + alwaysOutputData: true + }, + userMustProvide: [ + { + property: 'url', + description: 'The API endpoint' + }, + { + property: 'onError', + description: 'Choose error handling strategy', + example: 'continueRegularOutput' + } + ], + notes: [ + 'onError replaces the deprecated continueOnFail property', + 'continueRegularOutput: Continue with normal output on error', + 'continueErrorOutput: Route errors to error output for special handling', + 'stopWorkflow: Stop the entire workflow on error', + 'Combine with retryOnFail for resilient workflows' + ] + }, + + 'database_transaction_safety': { + task: 'database_transaction_safety', + description: 'Database operations with proper error handling', + nodeType: 'nodes-base.postgres', + configuration: { + operation: 'executeQuery', + query: 'BEGIN; INSERT INTO orders ...; COMMIT;', + // For transactions, don\'t retry automatically + onError: 'continueErrorOutput', + retryOnFail: false, + alwaysOutputData: true + }, + userMustProvide: [ + { + property: 'query', + description: 'Your SQL query or transaction' + } + ], + notes: [ + 'Transactions should not be retried automatically', + 'Use continueErrorOutput to handle errors separately', + 'Consider implementing compensating transactions', + 'Always log transaction failures for audit' + ] + }, + + 'ai_rate_limit_handling': { + task: 'ai_rate_limit_handling', + description: 'AI API calls with rate limit handling', + nodeType: 'nodes-base.openAi', + configuration: { + resource: 'chat', + operation: 'message', + modelId: 'gpt-4', + messages: { + values: [ + { + role: 'user', + content: '' + } + ] + }, + // Handle rate limits with exponential backoff + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 5, + waitBetweenTries: 5000, + alwaysOutputData: true + }, + userMustProvide: [ + { + property: 'messages.values[0].content', + description: 'The prompt for the AI' + } + ], + notes: [ + 'AI APIs often have rate limits', + 'Longer wait times help avoid hitting limits', + 'Consider implementing exponential backoff in Code node', + 'Monitor usage to stay within quotas' + ] } }; @@ -588,6 +899,13 @@ return results;` return this.templates[task]; } + /** + * Get a specific task template (alias for getTaskTemplate) + */ + static getTemplate(task: string): TaskTemplate | undefined { + return this.getTaskTemplate(task); + } + /** * Search for tasks by keyword */ @@ -607,13 +925,14 @@ return results;` */ static getTaskCategories(): Record { return { - 'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth'], - 'Webhooks': ['receive_webhook', 'webhook_with_response'], - 'Database': ['query_postgres', 'insert_postgres_data'], - 'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent'], - 'Data Processing': ['transform_data', 'filter_data'], + 'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth', 'api_call_with_retry'], + 'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling'], + 'Database': ['query_postgres', 'insert_postgres_data', 'database_transaction_safety'], + 'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent', 'ai_rate_limit_handling'], + 'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing'], 'Communication': ['send_slack_message', 'send_email'], - 'AI Tool Usage': ['use_google_sheets_as_tool', 'use_slack_as_tool', 'multi_tool_ai_agent'] + 'AI Tool Usage': ['use_google_sheets_as_tool', 'use_slack_as_tool', 'multi_tool_ai_agent'], + 'Error Handling': ['modern_error_handling_patterns', 'api_call_with_retry', 'fault_tolerant_processing', 'webhook_with_error_handling', 'database_transaction_safety', 'ai_rate_limit_handling'] }; } } \ No newline at end of file diff --git a/src/services/workflow-diff-engine.ts b/src/services/workflow-diff-engine.ts index fcaa92a..b6ef852 100644 --- a/src/services/workflow-diff-engine.ts +++ b/src/services/workflow-diff-engine.ts @@ -404,6 +404,7 @@ export class WorkflowDiffEngine { notes: operation.node.notes, notesInFlow: operation.node.notesInFlow, continueOnFail: operation.node.continueOnFail, + onError: operation.node.onError, retryOnFail: operation.node.retryOnFail, maxTries: operation.node.maxTries, waitBetweenTries: operation.node.waitBetweenTries, diff --git a/src/services/workflow-validator.ts b/src/services/workflow-validator.ts index c9f13d0..c8ebe05 100644 --- a/src/services/workflow-validator.ts +++ b/src/services/workflow-validator.ts @@ -19,7 +19,15 @@ interface WorkflowNode { credentials?: any; disabled?: boolean; notes?: string; + notesInFlow?: boolean; typeVersion?: number; + continueOnFail?: boolean; + onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow'; + retryOnFail?: boolean; + maxTries?: number; + waitBetweenTries?: number; + alwaysOutputData?: boolean; + executeOnce?: boolean; } interface WorkflowConnection { @@ -775,6 +783,9 @@ export class WorkflowValidator { }); } + // Check node-level error handling properties + this.checkNodeErrorHandling(workflow, result); + // Check for very long linear workflows const linearChainLength = this.getLongestLinearChain(workflow); if (linearChainLength > 10) { @@ -1004,4 +1015,333 @@ export class WorkflowValidator { ); } } + + /** + * Check node-level error handling configuration + */ + private checkNodeErrorHandling( + workflow: WorkflowJson, + result: WorkflowValidationResult + ): void { + // Define node types that typically interact with external services + const errorProneNodeTypes = [ + 'httpRequest', + 'webhook', + 'emailSend', + 'slack', + 'discord', + 'telegram', + 'postgres', + 'mysql', + 'mongodb', + 'redis', + 'github', + 'gitlab', + 'jira', + 'salesforce', + 'hubspot', + 'airtable', + 'googleSheets', + 'googleDrive', + 'dropbox', + 's3', + 'ftp', + 'ssh', + 'mqtt', + 'kafka', + 'rabbitmq', + 'graphql', + 'openai', + 'anthropic' + ]; + + for (const node of workflow.nodes) { + if (node.disabled) continue; + + const normalizedType = node.type.toLowerCase(); + const isErrorProne = errorProneNodeTypes.some(type => normalizedType.includes(type)); + + // CRITICAL: Check for node-level properties in wrong location (inside parameters) + const nodeLevelProps = [ + // Error handling properties + 'onError', 'continueOnFail', 'retryOnFail', 'maxTries', 'waitBetweenTries', 'alwaysOutputData', + // Other node-level properties + 'executeOnce', 'disabled', 'notes', 'notesInFlow', 'credentials' + ]; + const misplacedProps: string[] = []; + + if (node.parameters) { + for (const prop of nodeLevelProps) { + if (node.parameters[prop] !== undefined) { + misplacedProps.push(prop); + } + } + } + + if (misplacedProps.length > 0) { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: `Node-level properties ${misplacedProps.join(', ')} are in the wrong location. They must be at the node level, not inside parameters.`, + details: { + fix: `Move these properties from node.parameters to the node level. Example:\n` + + `{\n` + + ` "name": "${node.name}",\n` + + ` "type": "${node.type}",\n` + + ` "parameters": { /* operation-specific params */ },\n` + + ` "onError": "continueErrorOutput", // āœ… Correct location\n` + + ` "retryOnFail": true, // āœ… Correct location\n` + + ` "executeOnce": true, // āœ… Correct location\n` + + ` "disabled": false, // āœ… Correct location\n` + + ` "credentials": { /* ... */ } // āœ… Correct location\n` + + `}` + } + }); + } + + // Validate error handling properties + + // Check for onError property (the modern approach) + if (node.onError !== undefined) { + const validOnErrorValues = ['continueRegularOutput', 'continueErrorOutput', 'stopWorkflow']; + if (!validOnErrorValues.includes(node.onError)) { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: `Invalid onError value: "${node.onError}". Must be one of: ${validOnErrorValues.join(', ')}` + }); + } + } + + // Check for deprecated continueOnFail + if (node.continueOnFail !== undefined) { + if (typeof node.continueOnFail !== 'boolean') { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'continueOnFail must be a boolean value' + }); + } else if (node.continueOnFail === true) { + // Warn about using deprecated property + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: 'Using deprecated "continueOnFail: true". Use "onError: \'continueRegularOutput\'" instead for better control and UI compatibility.' + }); + } + } + + // Check for conflicting error handling properties + if (node.continueOnFail !== undefined && node.onError !== undefined) { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'Cannot use both "continueOnFail" and "onError" properties. Use only "onError" for modern workflows.' + }); + } + + if (node.retryOnFail !== undefined) { + if (typeof node.retryOnFail !== 'boolean') { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'retryOnFail must be a boolean value' + }); + } + + // If retry is enabled, check retry configuration + if (node.retryOnFail === true) { + if (node.maxTries !== undefined) { + if (typeof node.maxTries !== 'number' || node.maxTries < 1) { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'maxTries must be a positive number when retryOnFail is enabled' + }); + } else if (node.maxTries > 10) { + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `maxTries is set to ${node.maxTries}. Consider if this many retries is necessary.` + }); + } + } else { + // maxTries defaults to 3 if not specified + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: 'retryOnFail is enabled but maxTries is not specified. Default is 3 attempts.' + }); + } + + if (node.waitBetweenTries !== undefined) { + if (typeof node.waitBetweenTries !== 'number' || node.waitBetweenTries < 0) { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'waitBetweenTries must be a non-negative number (milliseconds)' + }); + } else if (node.waitBetweenTries > 300000) { // 5 minutes + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `waitBetweenTries is set to ${node.waitBetweenTries}ms (${(node.waitBetweenTries/1000).toFixed(1)}s). This seems excessive.` + }); + } + } + } + } + + if (node.alwaysOutputData !== undefined && typeof node.alwaysOutputData !== 'boolean') { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'alwaysOutputData must be a boolean value' + }); + } + + // Warnings for error-prone nodes without error handling + const hasErrorHandling = node.onError || node.continueOnFail || node.retryOnFail; + + if (isErrorProne && !hasErrorHandling) { + const nodeTypeSimple = normalizedType.split('.').pop() || normalizedType; + + // Special handling for specific node types + if (normalizedType.includes('httprequest')) { + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: 'HTTP Request node without error handling. Consider adding "onError: \'continueRegularOutput\'" for non-critical requests or "retryOnFail: true" for transient failures.' + }); + } else if (normalizedType.includes('webhook')) { + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: 'Webhook node without error handling. Consider adding "onError: \'continueRegularOutput\'" to prevent workflow failures from blocking webhook responses.' + }); + } else if (errorProneNodeTypes.some(db => normalizedType.includes(db) && ['postgres', 'mysql', 'mongodb'].includes(db))) { + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `Database operation without error handling. Consider adding "retryOnFail: true" for connection issues or "onError: \'continueRegularOutput\'" for non-critical queries.` + }); + } else { + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: `${nodeTypeSimple} node interacts with external services but has no error handling configured. Consider using "onError" property.` + }); + } + } + + // Check for problematic combinations + if (node.continueOnFail && node.retryOnFail) { + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: 'Both continueOnFail and retryOnFail are enabled. The node will retry first, then continue on failure.' + }); + } + + // Validate additional node-level properties + + // Check executeOnce + if (node.executeOnce !== undefined && typeof node.executeOnce !== 'boolean') { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'executeOnce must be a boolean value' + }); + } + + // Check disabled + if (node.disabled !== undefined && typeof node.disabled !== 'boolean') { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'disabled must be a boolean value' + }); + } + + // Check notesInFlow + if (node.notesInFlow !== undefined && typeof node.notesInFlow !== 'boolean') { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'notesInFlow must be a boolean value' + }); + } + + // Check notes + if (node.notes !== undefined && typeof node.notes !== 'string') { + result.errors.push({ + type: 'error', + nodeId: node.id, + nodeName: node.name, + message: 'notes must be a string value' + }); + } + + // Provide guidance for executeOnce + if (node.executeOnce === true) { + result.warnings.push({ + type: 'warning', + nodeId: node.id, + nodeName: node.name, + message: 'executeOnce is enabled. This node will execute only once regardless of input items.' + }); + } + + // Suggest alwaysOutputData for debugging + if ((node.continueOnFail || node.retryOnFail) && !node.alwaysOutputData) { + if (normalizedType.includes('httprequest') || normalizedType.includes('webhook')) { + result.suggestions.push( + `Consider enabling alwaysOutputData on "${node.name}" to capture error responses for debugging` + ); + } + } + } + + // Add general suggestions based on findings + const nodesWithoutErrorHandling = workflow.nodes.filter(n => + !n.disabled && !n.onError && !n.continueOnFail && !n.retryOnFail + ).length; + + if (nodesWithoutErrorHandling > 5 && workflow.nodes.length > 5) { + result.suggestions.push( + 'Most nodes lack error handling. Use "onError" property for modern error handling: "continueRegularOutput" (continue on error), "continueErrorOutput" (use error output), or "stopWorkflow" (stop execution).' + ); + } + + // Check for nodes using deprecated continueOnFail + const nodesWithDeprecatedErrorHandling = workflow.nodes.filter(n => + !n.disabled && n.continueOnFail === true + ).length; + + if (nodesWithDeprecatedErrorHandling > 0) { + result.suggestions.push( + 'Replace "continueOnFail: true" with "onError: \'continueRegularOutput\'" for better UI compatibility and control.' + ); + } + } } \ No newline at end of file diff --git a/src/types/n8n-api.ts b/src/types/n8n-api.ts index b2c0b47..d4a08d1 100644 --- a/src/types/n8n-api.ts +++ b/src/types/n8n-api.ts @@ -9,11 +9,12 @@ export interface WorkflowNode { typeVersion: number; position: [number, number]; parameters: Record; - credentials?: Record; + credentials?: Record; disabled?: boolean; notes?: string; notesInFlow?: boolean; continueOnFail?: boolean; + onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow'; retryOnFail?: boolean; maxTries?: number; waitBetweenTries?: number;