fix: auto-inject webhookId on webhook nodes during create/update (#643) (#657)

n8n 2.10+ requires webhookId (UUID) on webhook-type nodes for proper
webhook URL registration. Without it, webhooks silently fail with 404.
The n8n UI always generates webhookId but programmatic creation via
n8n-mcp did not.

Add ensureWebhookIds() helper that injects crypto.randomUUID() on
webhook, webhookTrigger, formTrigger, and chatTrigger nodes when
webhookId is missing. Called from both cleanWorkflowForCreate() and
cleanWorkflowForUpdate(). Existing webhookId values are preserved.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2026-03-22 23:20:34 +01:00
committed by GitHub
parent 93816fce30
commit 1f0738e637
8 changed files with 139 additions and 5 deletions

View File

@@ -19,6 +19,14 @@ import { WorkflowBuilder } from '../../utils/builders/workflow.builder';
import { z } from 'zod';
import { WorkflowNode, WorkflowConnection, Workflow } from '../../../src/types/n8n-api';
function webhookNode(id: string, name: string, type: string, typeVersion = 2): WorkflowNode {
return { id, name, type, typeVersion, position: [250, 300] as [number, number], parameters: {} };
}
function workflowWithNodes(nodes: WorkflowNode[]): Partial<Workflow> {
return { name: 'Test', nodes, connections: {} };
}
describe('n8n-validation', () => {
describe('Zod Schemas', () => {
describe('workflowNodeSchema', () => {
@@ -301,6 +309,44 @@ describe('n8n-validation', () => {
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.settings).toEqual(customSettings);
});
it('should inject webhookId on webhook nodes missing it', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'),
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should preserve existing webhookId on webhook nodes', () => {
const workflow = workflowWithNodes([
{ ...webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'), webhookId: 'existing-id' },
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toBe('existing-id');
});
it('should inject webhookId on formTrigger and chatTrigger nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Form', 'n8n-nodes-base.formTrigger'),
webhookNode('2', 'Chat', '@n8n/n8n-nodes-langchain.chatTrigger'),
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
expect(cleaned.nodes![1].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should not inject webhookId on non-webhook nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Set', 'n8n-nodes-base.set', 3.4),
]);
const cleaned = cleanWorkflowForCreate(workflow as Workflow);
expect(cleaned.nodes![0].webhookId).toBeUndefined();
});
});
describe('cleanWorkflowForUpdate', () => {
@@ -533,6 +579,44 @@ describe('n8n-validation', () => {
});
expect(cleaned.settings).not.toHaveProperty('someOtherProperty');
});
it('should inject webhookId on webhook nodes missing it', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'),
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should preserve existing webhookId on webhook nodes', () => {
const workflow = workflowWithNodes([
{ ...webhookNode('1', 'Webhook', 'n8n-nodes-base.webhook'), webhookId: 'existing-id' },
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toBe('existing-id');
});
it('should inject webhookId on formTrigger and chatTrigger nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Form', 'n8n-nodes-base.formTrigger'),
webhookNode('2', 'Chat', '@n8n/n8n-nodes-langchain.chatTrigger'),
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
expect(cleaned.nodes![1].webhookId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-/);
});
it('should not inject webhookId on non-webhook nodes', () => {
const workflow = workflowWithNodes([
webhookNode('1', 'Set', 'n8n-nodes-base.set', 3.4),
]) as any;
const cleaned = cleanWorkflowForUpdate(workflow);
expect(cleaned.nodes![0].webhookId).toBeUndefined();
});
});
});