feat: enhance workflow validation to prevent broken workflows (v2.6.2)
- Add node type existence validation that catches invalid types - Fix critical issue: now catches 'nodes-base.webhook' BEFORE database lookup - Add smart suggestions for common mistakes (webhook → n8n-nodes-base.webhook) - Add minimum viable workflow validation (prevents single-node workflows) - Add empty connection detection for multi-node workflows - Add helper functions for workflow structure examples and fix suggestions - Prevent AI agents from creating workflows with question mark nodes This fixes the exact issue where workflows created with 'nodes-base.webhook' would show as broken (question marks) in the n8n UI. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -134,11 +134,38 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
errors.push('Workflow connections are required');
|
||||
}
|
||||
|
||||
// Check for minimum viable workflow
|
||||
if (workflow.nodes && workflow.nodes.length === 1) {
|
||||
const singleNode = workflow.nodes[0];
|
||||
const isWebhookOnly = singleNode.type === 'n8n-nodes-base.webhook' ||
|
||||
singleNode.type === 'n8n-nodes-base.webhookTrigger';
|
||||
|
||||
if (!isWebhookOnly) {
|
||||
errors.push('Single-node workflows are only valid for webhooks. Add at least one more node and connect them. Example: Manual Trigger → Set node');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for empty connections in multi-node workflows
|
||||
if (workflow.nodes && workflow.nodes.length > 1 && workflow.connections) {
|
||||
const connectionCount = Object.keys(workflow.connections).length;
|
||||
|
||||
if (connectionCount === 0) {
|
||||
errors.push('Multi-node workflow has empty connections. Connect nodes like this: connections: { "Node1 Name": { "main": [[{ "node": "Node2 Name", "type": "main", "index": 0 }]] } }');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate nodes
|
||||
if (workflow.nodes) {
|
||||
workflow.nodes.forEach((node, index) => {
|
||||
try {
|
||||
validateWorkflowNode(node);
|
||||
|
||||
// Additional check for common node type mistakes
|
||||
if (node.type.startsWith('nodes-base.')) {
|
||||
errors.push(`Invalid node type "${node.type}" at index ${index}. Use "n8n-nodes-base.${node.type.substring(11)}" instead.`);
|
||||
} else if (!node.type.includes('.')) {
|
||||
errors.push(`Invalid node type "${node.type}" at index ${index}. Node types must include package prefix (e.g., "n8n-nodes-base.webhook").`);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Invalid node at index ${index}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
@@ -154,19 +181,35 @@ export function validateWorkflowStructure(workflow: Partial<Workflow>): string[]
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that all connection references exist
|
||||
// Validate that all connection references exist and use node NAMES (not IDs)
|
||||
if (workflow.nodes && workflow.connections) {
|
||||
const nodeNames = new Set(workflow.nodes.map(node => node.name));
|
||||
const nodeIds = new Set(workflow.nodes.map(node => node.id));
|
||||
const nodeIdToName = new Map(workflow.nodes.map(node => [node.id, node.name]));
|
||||
|
||||
Object.entries(workflow.connections).forEach(([sourceId, connection]) => {
|
||||
if (!nodeIds.has(sourceId)) {
|
||||
errors.push(`Connection references non-existent source node: ${sourceId}`);
|
||||
Object.entries(workflow.connections).forEach(([sourceName, connection]) => {
|
||||
// Check if source exists by name (correct)
|
||||
if (!nodeNames.has(sourceName)) {
|
||||
// Check if they're using an ID instead of name
|
||||
if (nodeIds.has(sourceName)) {
|
||||
const correctName = nodeIdToName.get(sourceName);
|
||||
errors.push(`Connection uses node ID '${sourceName}' but must use node name '${correctName}'. Change connections.${sourceName} to connections['${correctName}']`);
|
||||
} else {
|
||||
errors.push(`Connection references non-existent node: ${sourceName}`);
|
||||
}
|
||||
}
|
||||
|
||||
connection.main.forEach((outputs, outputIndex) => {
|
||||
outputs.forEach((target, targetIndex) => {
|
||||
if (!nodeIds.has(target.node)) {
|
||||
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceId}[${outputIndex}][${targetIndex}])`);
|
||||
// Check if target exists by name (correct)
|
||||
if (!nodeNames.has(target.node)) {
|
||||
// Check if they're using an ID instead of name
|
||||
if (nodeIds.has(target.node)) {
|
||||
const correctName = nodeIdToName.get(target.node);
|
||||
errors.push(`Connection target uses node ID '${target.node}' but must use node name '${correctName}' (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
} else {
|
||||
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceName}[${outputIndex}][${targetIndex}])`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -204,4 +247,74 @@ export function getWebhookUrl(workflow: Workflow): string | null {
|
||||
// Note: We can't construct the full URL without knowing the n8n instance URL
|
||||
// The caller will need to prepend the base URL
|
||||
return path;
|
||||
}
|
||||
|
||||
// Helper function to generate proper workflow structure examples
|
||||
export function getWorkflowStructureExample(): string {
|
||||
return `
|
||||
Minimal Workflow Example:
|
||||
{
|
||||
"name": "My Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "manual-trigger-1",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300],
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"id": "set-1",
|
||||
"name": "Set Data",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [450, 300],
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"assignments": {
|
||||
"assignments": [{
|
||||
"id": "1",
|
||||
"name": "message",
|
||||
"value": "Hello World",
|
||||
"type": "string"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Manual Trigger": {
|
||||
"main": [[{
|
||||
"node": "Set Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IMPORTANT: In connections, use the node NAME (e.g., "Manual Trigger"), NOT the node ID or type!`;
|
||||
}
|
||||
|
||||
// Helper function to fix common workflow issues
|
||||
export function getWorkflowFixSuggestions(errors: string[]): string[] {
|
||||
const suggestions: string[] = [];
|
||||
|
||||
if (errors.some(e => e.includes('empty connections'))) {
|
||||
suggestions.push('Add connections between your nodes. Each node (except endpoints) should connect to another node.');
|
||||
suggestions.push('Connection format: connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }');
|
||||
}
|
||||
|
||||
if (errors.some(e => e.includes('Single-node workflows'))) {
|
||||
suggestions.push('Add at least one more node to process data. Common patterns: Trigger → Process → Output');
|
||||
suggestions.push('Examples: Manual Trigger → Set, Webhook → HTTP Request, Schedule Trigger → Database Query');
|
||||
}
|
||||
|
||||
if (errors.some(e => e.includes('node ID') && e.includes('instead of node name'))) {
|
||||
suggestions.push('Replace node IDs with node names in connections. The name is what appears in the node header.');
|
||||
suggestions.push('Wrong: connections: { "set-1": {...} }, Right: connections: { "Set Data": {...} }');
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
Reference in New Issue
Block a user