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:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
200
src/scripts/test-node-level-properties.ts
Executable file
200
src/scripts/test-node-level-properties.ts
Executable 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);
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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']
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user