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:
czlonkowski
2025-06-26 14:30:45 +02:00
parent c45fcbfb6b
commit 3c4e32e55f
8 changed files with 790 additions and 18 deletions

View File

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