fix: comprehensive error handling and node-level properties validation (fixes #26)

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 <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-09 20:39:24 +02:00
parent 66e032c2a0
commit eab3cc858e
12 changed files with 1638 additions and 138 deletions

View File

@@ -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/), 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). 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<string, string>` to `Record<string, unknown>` 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 ## [2.7.10] - 2025-07-07
### Added ### Added

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.7.10", "version": "2.7.11",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
@@ -36,6 +36,7 @@
"test:n8n-manager": "node dist/scripts/test-n8n-manager-integration.js", "test:n8n-manager": "node dist/scripts/test-n8n-manager-integration.js",
"test:n8n-validate-workflow": "node dist/scripts/test-n8n-validate-workflow.js", "test:n8n-validate-workflow": "node dist/scripts/test-n8n-validate-workflow.js",
"test:typeversion-validation": "node dist/scripts/test-typeversion-validation.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:workflow-diff": "node dist/scripts/test-workflow-diff.js",
"test:transactional-diff": "node dist/scripts/test-transactional-diff.js", "test:transactional-diff": "node dist/scripts/test-transactional-diff.js",
"test:tools-documentation": "node dist/scripts/test-tools-documentation.js", "test:tools-documentation": "node dist/scripts/test-tools-documentation.js",

View File

@@ -315,11 +315,12 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
performance: 'API call - depends on n8n instance', performance: 'API call - depends on n8n instance',
tips: [ tips: [
'ALWAYS use node names in connections, never IDs', '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' 'Requires N8N_API_URL and N8N_API_KEY configuration'
] ]
}, },
full: { 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: { parameters: {
name: { type: 'string', description: 'Workflow name', required: true }, name: { type: 'string', description: 'Workflow name', required: true },
nodes: { type: 'array', description: 'Array of node configurations', required: true }, nodes: { type: 'array', description: 'Array of node configurations', required: true },
@@ -329,14 +330,61 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
}, },
returns: 'Created workflow object with id, name, nodes, connections, and metadata', returns: 'Created workflow object with id, name, nodes, connections, and metadata',
examples: [ examples: [
`n8n_create_workflow({ `// Basic workflow with proper error handling
name: "Slack Notification", n8n_create_workflow({
name: "Slack Notification with Error Handling",
nodes: [ 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: { 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<string, ToolDocumentation> = {
'Deploying workflows programmatically', 'Deploying workflows programmatically',
'Automating workflow creation', 'Automating workflow creation',
'Migrating workflows between instances', '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.', performance: 'Depends on n8n instance and network. Typically 100-500ms.',
bestPractices: [ bestPractices: [
'CRITICAL: Use node NAMES in connections, not IDs', 'CRITICAL: Use node NAMES in connections, not IDs',
'CRITICAL: Place error handling at NODE level, not in parameters',
'Validate workflow before creating', 'Validate workflow before creating',
'Use meaningful workflow names', 'Use meaningful workflow names',
'Check n8n_health_check before creating', 'Add error handling to external service nodes',
'Handle API errors gracefully' 'Check n8n_health_check before creating'
], ],
pitfalls: [ pitfalls: [
'Placing error handling properties inside parameters object',
'Using node IDs in connections breaks UI display', 'Using node IDs in connections breaks UI display',
'Workflow not automatically activated', 'Workflow not automatically activated',
'Tags must exist (use tag IDs not names)', 'Tags must exist (use tag IDs not names)',
@@ -370,15 +421,16 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
essentials: { essentials: {
description: 'Update workflows using diff operations - only send changes, not entire workflow', description: 'Update workflows using diff operations - only send changes, not entire workflow',
keyParameters: ['id', 'operations'], 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', performance: '80-90% more efficient than full updates',
tips: [ tips: [
'Maximum 5 operations per request', '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: { 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: { parameters: {
id: { type: 'string', description: 'Workflow ID to update', required: true }, id: { type: 'string', description: 'Workflow ID to update', required: true },
operations: { type: 'array', description: 'Array of diff operations (max 5)', required: true }, operations: { type: 'array', description: 'Array of diff operations (max 5)', required: true },
@@ -386,29 +438,50 @@ export const toolsDocumentation: Record<string, ToolDocumentation> = {
}, },
returns: 'Updated workflow with applied changes and operation results', returns: 'Updated workflow with applied changes and operation results',
examples: [ examples: [
`// Update node parameters `// Update node parameters (properties inside parameters object)
n8n_update_partial_workflow({ n8n_update_partial_workflow({
id: "123", id: "123",
operations: [{ operations: [{
type: "updateNode", type: "updateNode",
nodeId: "Slack", nodeName: "Slack",
updates: {parameters: {channel: "general"}} 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({ n8n_update_partial_workflow({
id: "123", id: "123",
operations: [{ operations: [{
type: "addConnection", type: "addConnection",
from: "HTTP Request", source: "Database Query",
to: "Slack", target: "Error Handler",
fromOutput: "main", sourceOutput: "error", // Error output
toInput: "main" targetInput: "main"
}] }]
})` })`
], ],
useCases: [ useCases: [
'Updating node configurations', 'Updating node configurations',
'Adding error handling to nodes',
'Adding/removing connections', 'Adding/removing connections',
'Enabling/disabling nodes', 'Enabling/disabling nodes',
'Moving nodes in canvas', '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.', performance: 'Very efficient - only sends changes. 80-90% less data than full updates.',
bestPractices: [ 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', 'Batch related operations together',
'Use validateOnly:true to test first', 'Use validateOnly:true to test first',
'Reference nodes by name for clarity', 'Reference nodes by name for clarity'
'Keep under 5 operations per request',
'Check operation results for success'
], ],
pitfalls: [ pitfalls: [
'Placing error handling properties inside parameters (common mistake!)',
'Maximum 5 operations per request', 'Maximum 5 operations per request',
'Some operations have dependencies', 'Some operations have dependencies',
'Node must exist for update operations', 'Node must exist for update operations',
@@ -578,6 +652,55 @@ validate_workflow(workflow)
n8n_create_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 ### Using AI Tools
Any node can be an AI tool! Connect it to an AI Agent's ai_tool port: Any node can be an AI tool! Connect it to an AI Agent's ai_tool port:
\`\`\`javascript \`\`\`javascript

View File

@@ -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);

View File

@@ -16,14 +16,14 @@ export interface ValidationResult {
} }
export interface ValidationError { export interface ValidationError {
type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible'; type: 'missing_required' | 'invalid_type' | 'invalid_value' | 'incompatible' | 'invalid_configuration';
property: string; property: string;
message: string; message: string;
fix?: string; fix?: string;
} }
export interface ValidationWarning { export interface ValidationWarning {
type: 'missing_common' | 'deprecated' | 'inefficient' | 'security'; type: 'missing_common' | 'deprecated' | 'inefficient' | 'security' | 'best_practice' | 'invalid_value';
property?: string; property?: string;
message: string; message: string;
suggestion?: string; suggestion?: string;

View File

@@ -491,9 +491,11 @@ export class EnhancedConfigValidator extends ConfigValidator {
case 'strict': case 'strict':
// Keep everything, add more suggestions // Keep everything, add more suggestions
if (result.warnings.length === 0 && result.errors.length === 0) { 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'); result.suggestions.push('Add authentication if connecting to external services');
} }
// Require error handling for external service nodes
this.enforceErrorHandlingForProfile(result, profile);
break; break;
case 'ai-friendly': case 'ai-friendly':
@@ -503,7 +505,64 @@ export class EnhancedConfigValidator extends ConfigValidator {
result.warnings = result.warnings.filter(w => result.warnings = result.warnings.filter(w =>
w.type !== 'inefficient' || !w.property?.startsWith('_') w.type !== 'inefficient' || !w.property?.startsWith('_')
); );
// Add error handling suggestions for AI-friendly profile
this.addErrorHandlingSuggestions(result);
break; 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'
);
}
}
} }

View File

@@ -47,7 +47,13 @@ export class ExampleGenerator {
sendBody: true, sendBody: true,
contentType: 'json', contentType: 'json',
specifyBody: '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', httpMethod: 'POST',
responseMode: 'lastNode', responseMode: 'lastNode',
responseData: 'allEntries', 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 *;`, RETURNING *;`,
additionalFields: { additionalFields: {
queryParams: '={{ $json.name }},{{ $json.email }},active' 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: { options: {
maxTokens: 150, maxTokens: 150,
temperature: 0.7 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 *;`,
<p>Best regards,<br>The Team</p>`, <p>Best regards,<br>The Team</p>`,
options: { options: {
ccEmail: 'admin@company.com' 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: { options: {
upsert: true, upsert: true,
returnNewDocument: 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', columns: 'customer_id,product_id,quantity,order_date',
options: { options: {
queryBatching: 'independently' queryBatching: 'independently'
} },
// Database writes should handle connection errors
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 2000,
onError: 'stopWorkflow'
} }
}, },
@@ -514,6 +554,145 @@ return processedItems;`
assignees: ['maintainer'], assignees: ['maintainer'],
labels: ['bug', 'needs-triage'] 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
}
} }
}; };

View File

@@ -20,7 +20,7 @@ export class NodeSpecificValidators {
* Validate Slack node configuration with operation awareness * Validate Slack node configuration with operation awareness
*/ */
static validateSlack(context: NodeValidationContext): void { static validateSlack(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions } = context; const { config, errors, warnings, suggestions, autofix } = context;
const { resource, operation } = config; const { resource, operation } = config;
// Message operations // 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 { private static validateSlackSendMessage(context: NodeValidationContext): void {
@@ -376,7 +400,7 @@ export class NodeSpecificValidators {
* Validate OpenAI node configuration * Validate OpenAI node configuration
*/ */
static validateOpenAI(context: NodeValidationContext): void { static validateOpenAI(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions } = context; const { config, errors, warnings, suggestions, autofix } = context;
const { resource, operation } = config; const { resource, operation } = config;
if (resource === 'chat' && operation === 'create') { 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 * Validate MongoDB node configuration
*/ */
static validateMongoDB(context: NodeValidationContext): void { static validateMongoDB(context: NodeValidationContext): void {
const { config, errors, warnings } = context; const { config, errors, warnings, autofix } = context;
const { operation } = config; const { operation } = config;
// Collection is always required // Collection is always required
@@ -501,91 +550,44 @@ export class NodeSpecificValidators {
} }
break; 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 * Validate Postgres node configuration
@@ -677,6 +679,42 @@ export class NodeSpecificValidators {
if (config.connectionTimeout === undefined) { if (config.connectionTimeout === undefined) {
suggestions.push('Consider setting connectionTimeout to handle slow connections'); 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) { if (config.timezone === undefined) {
suggestions.push('Consider setting timezone to ensure consistent date/time handling'); 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');
}
} }

View File

@@ -33,7 +33,13 @@ export class TaskTemplates {
configuration: { configuration: {
method: 'GET', method: 'GET',
url: '', url: '',
authentication: 'none' authentication: 'none',
// Default error handling for API calls
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 1000,
alwaysOutputData: true
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -52,6 +58,11 @@ export class TaskTemplates {
property: 'sendHeaders', property: 'sendHeaders',
description: 'Add custom headers if needed', description: 'Add custom headers if needed',
when: 'API requires specific headers' 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, sendBody: true,
contentType: 'json', contentType: 'json',
specifyBody: '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: [ userMustProvide: [
{ {
@@ -84,11 +101,17 @@ export class TaskTemplates {
{ {
property: 'authentication', property: 'authentication',
description: 'Add authentication if required' description: 'Add authentication if required'
},
{
property: 'onError',
description: 'Set to "continueRegularOutput" for non-critical operations',
when: 'Failure should not stop the workflow'
} }
], ],
notes: [ notes: [
'Make sure jsonBody contains valid JSON', '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', authentication: 'genericCredentialType',
genericAuthType: 'headerAuth', genericAuthType: 'headerAuth',
sendHeaders: true, sendHeaders: true,
// Authentication calls should handle auth failures gracefully
onError: 'continueErrorOutput',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 2000,
alwaysOutputData: true,
headerParameters: { headerParameters: {
parameters: [ parameters: [
{ {
@@ -144,7 +173,10 @@ export class TaskTemplates {
httpMethod: 'POST', httpMethod: 'POST',
path: 'webhook', path: 'webhook',
responseMode: 'lastNode', responseMode: 'lastNode',
responseData: 'allEntries' responseData: 'allEntries',
// Webhooks should always respond, even on error
onError: 'continueRegularOutput',
alwaysOutputData: true
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -178,7 +210,10 @@ export class TaskTemplates {
path: 'webhook', path: 'webhook',
responseMode: 'responseNode', responseMode: 'responseNode',
responseData: 'firstEntryJson', responseData: 'firstEntryJson',
responseCode: 200 responseCode: 200,
// Ensure webhook always sends response
onError: 'continueRegularOutput',
alwaysOutputData: true
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -199,7 +234,12 @@ export class TaskTemplates {
nodeType: 'nodes-base.postgres', nodeType: 'nodes-base.postgres',
configuration: { configuration: {
operation: 'executeQuery', operation: 'executeQuery',
query: '' query: '',
// Database reads can continue on error
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 1000
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -229,7 +269,12 @@ export class TaskTemplates {
operation: 'insert', operation: 'insert',
table: '', table: '',
columns: '', columns: '',
returnFields: '*' returnFields: '*',
// Database writes should stop on error by default
onError: 'stopWorkflow',
retryOnFail: true,
maxTries: 2,
waitBetweenTries: 1000
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -265,7 +310,13 @@ export class TaskTemplates {
content: '' content: ''
} }
] ]
} },
// AI calls should handle rate limits and API errors
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 5000,
alwaysOutputData: true
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -393,7 +444,12 @@ return results;`
resource: 'message', resource: 'message',
operation: 'post', operation: 'post',
channel: '', channel: '',
text: '' text: '',
// Messaging can continue on error
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 2,
waitBetweenTries: 2000
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -427,7 +483,13 @@ return results;`
fromEmail: '', fromEmail: '',
toEmail: '', toEmail: '',
subject: '', subject: '',
text: '' text: '',
// Email sending should retry on transient failures
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 3000,
alwaysOutputData: true
}, },
userMustProvide: [ userMustProvide: [
{ {
@@ -562,6 +624,255 @@ return results;`
'Test each tool individually before combining', 'Test each tool individually before combining',
'Set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true for community nodes' '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]; 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 * Search for tasks by keyword
*/ */
@@ -607,13 +925,14 @@ return results;`
*/ */
static getTaskCategories(): Record<string, string[]> { static getTaskCategories(): Record<string, string[]> {
return { return {
'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth'], 'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth', 'api_call_with_retry'],
'Webhooks': ['receive_webhook', 'webhook_with_response'], 'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling'],
'Database': ['query_postgres', 'insert_postgres_data'], 'Database': ['query_postgres', 'insert_postgres_data', 'database_transaction_safety'],
'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent'], 'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent', 'ai_rate_limit_handling'],
'Data Processing': ['transform_data', 'filter_data'], 'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing'],
'Communication': ['send_slack_message', 'send_email'], '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']
}; };
} }
} }

View File

@@ -404,6 +404,7 @@ export class WorkflowDiffEngine {
notes: operation.node.notes, notes: operation.node.notes,
notesInFlow: operation.node.notesInFlow, notesInFlow: operation.node.notesInFlow,
continueOnFail: operation.node.continueOnFail, continueOnFail: operation.node.continueOnFail,
onError: operation.node.onError,
retryOnFail: operation.node.retryOnFail, retryOnFail: operation.node.retryOnFail,
maxTries: operation.node.maxTries, maxTries: operation.node.maxTries,
waitBetweenTries: operation.node.waitBetweenTries, waitBetweenTries: operation.node.waitBetweenTries,

View File

@@ -19,7 +19,15 @@ interface WorkflowNode {
credentials?: any; credentials?: any;
disabled?: boolean; disabled?: boolean;
notes?: string; notes?: string;
notesInFlow?: boolean;
typeVersion?: number; typeVersion?: number;
continueOnFail?: boolean;
onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow';
retryOnFail?: boolean;
maxTries?: number;
waitBetweenTries?: number;
alwaysOutputData?: boolean;
executeOnce?: boolean;
} }
interface WorkflowConnection { 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 // Check for very long linear workflows
const linearChainLength = this.getLongestLinearChain(workflow); const linearChainLength = this.getLongestLinearChain(workflow);
if (linearChainLength > 10) { 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.'
);
}
}
} }

View File

@@ -9,11 +9,12 @@ export interface WorkflowNode {
typeVersion: number; typeVersion: number;
position: [number, number]; position: [number, number];
parameters: Record<string, unknown>; parameters: Record<string, unknown>;
credentials?: Record<string, string>; credentials?: Record<string, unknown>;
disabled?: boolean; disabled?: boolean;
notes?: string; notes?: string;
notesInFlow?: boolean; notesInFlow?: boolean;
continueOnFail?: boolean; continueOnFail?: boolean;
onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow';
retryOnFail?: boolean; retryOnFail?: boolean;
maxTries?: number; maxTries?: number;
waitBetweenTries?: number; waitBetweenTries?: number;