trigger-detector.tsβ’7.89 kB
/**
* Trigger detector - analyzes workflows to detect trigger type
*/
import { Workflow, WorkflowNode } from '../types/n8n-api';
import { normalizeNodeType } from '../utils/node-type-utils';
import { TriggerType, DetectedTrigger, TriggerDetectionResult } from './types';
/**
* Node type patterns for each trigger type
*/
const WEBHOOK_PATTERNS = [
'webhook',
'webhooktrigger',
];
const FORM_PATTERNS = [
'formtrigger',
'form',
];
const CHAT_PATTERNS = [
'chattrigger',
];
/**
* Detect the trigger type from a workflow
*
* Priority order:
* 1. Webhook trigger (most common for API access)
* 2. Chat trigger (AI-specific)
* 3. Form trigger
*
* Note: n8n's public API does not support direct workflow execution.
* Only workflows with webhook/form/chat triggers can be triggered externally.
*/
export function detectTriggerFromWorkflow(workflow: Workflow): TriggerDetectionResult {
if (!workflow.nodes || workflow.nodes.length === 0) {
return {
detected: false,
reason: 'Workflow has no nodes',
};
}
// Find all trigger nodes
const triggerNodes = workflow.nodes.filter(node => !node.disabled && isTriggerNodeType(node.type));
if (triggerNodes.length === 0) {
return {
detected: false,
reason: 'No trigger nodes found in workflow',
};
}
// Check for specific trigger types in priority order
for (const node of triggerNodes) {
const webhookTrigger = detectWebhookTrigger(node);
if (webhookTrigger) {
return {
detected: true,
trigger: webhookTrigger,
};
}
}
for (const node of triggerNodes) {
const chatTrigger = detectChatTrigger(node);
if (chatTrigger) {
return {
detected: true,
trigger: chatTrigger,
};
}
}
for (const node of triggerNodes) {
const formTrigger = detectFormTrigger(node);
if (formTrigger) {
return {
detected: true,
trigger: formTrigger,
};
}
}
// No externally-triggerable trigger found
return {
detected: false,
reason: `Workflow has trigger nodes but none support external triggering (found: ${triggerNodes.map(n => n.type).join(', ')}). Only webhook, form, and chat triggers can be triggered via the API.`,
};
}
/**
* Check if a node type is a trigger
*/
function isTriggerNodeType(nodeType: string): boolean {
const normalized = normalizeNodeType(nodeType).toLowerCase();
return (
normalized.includes('trigger') ||
normalized.includes('webhook') ||
normalized === 'nodes-base.start'
);
}
/**
* Detect webhook trigger and extract configuration
*/
function detectWebhookTrigger(node: WorkflowNode): DetectedTrigger | null {
const normalized = normalizeNodeType(node.type).toLowerCase();
const nodeName = normalized.split('.').pop() || '';
const isWebhook = WEBHOOK_PATTERNS.some(pattern =>
nodeName === pattern || nodeName.includes(pattern)
);
if (!isWebhook) {
return null;
}
// Extract webhook path from parameters
const params = node.parameters || {};
const webhookPath = extractWebhookPath(params, node.id);
const httpMethod = extractHttpMethod(params);
return {
type: 'webhook',
node,
webhookPath,
httpMethod,
};
}
/**
* Detect form trigger and extract configuration
*/
function detectFormTrigger(node: WorkflowNode): DetectedTrigger | null {
const normalized = normalizeNodeType(node.type).toLowerCase();
const nodeName = normalized.split('.').pop() || '';
const isForm = FORM_PATTERNS.some(pattern =>
nodeName === pattern || nodeName.includes(pattern)
);
if (!isForm) {
return null;
}
// Extract form fields from parameters
const params = node.parameters || {};
const formFields = extractFormFields(params);
return {
type: 'form',
node,
formFields,
};
}
/**
* Detect chat trigger and extract configuration
*/
function detectChatTrigger(node: WorkflowNode): DetectedTrigger | null {
const normalized = normalizeNodeType(node.type).toLowerCase();
const nodeName = normalized.split('.').pop() || '';
const isChat = CHAT_PATTERNS.some(pattern =>
nodeName === pattern || nodeName.includes(pattern)
);
if (!isChat) {
return null;
}
// Extract chat configuration
const params = node.parameters || {};
const responseMode = (params.options as any)?.responseMode || 'lastNode';
const webhookPath = extractWebhookPath(params, node.id);
return {
type: 'chat',
node,
webhookPath,
chatConfig: {
responseMode,
},
};
}
/**
* Extract webhook path from node parameters
*/
function extractWebhookPath(params: Record<string, unknown>, nodeId: string): string {
// Check for explicit path parameter
if (typeof params.path === 'string' && params.path) {
return params.path;
}
// Check for httpMethod specific path
if (typeof params.httpMethod === 'string') {
const methodPath = params[`path_${params.httpMethod.toLowerCase()}`];
if (typeof methodPath === 'string' && methodPath) {
return methodPath;
}
}
// Default: use node ID as path (n8n default behavior)
return nodeId;
}
/**
* Extract HTTP method from webhook parameters
*/
function extractHttpMethod(params: Record<string, unknown>): string {
if (typeof params.httpMethod === 'string') {
return params.httpMethod.toUpperCase();
}
return 'POST'; // Default to POST
}
/**
* Extract form field names from form trigger parameters
*/
function extractFormFields(params: Record<string, unknown>): string[] {
const fields: string[] = [];
// Check for formFields parameter (common pattern)
if (Array.isArray(params.formFields)) {
for (const field of params.formFields) {
if (field && typeof field.fieldLabel === 'string') {
fields.push(field.fieldLabel);
} else if (field && typeof field.fieldName === 'string') {
fields.push(field.fieldName);
}
}
}
// Check for fields in options
const options = params.options as Record<string, unknown> | undefined;
if (options && Array.isArray(options.formFields)) {
for (const field of options.formFields) {
if (field && typeof field.fieldLabel === 'string') {
fields.push(field.fieldLabel);
}
}
}
return fields;
}
/**
* Build the trigger URL based on detected trigger and n8n base URL
*
* @param baseUrl - n8n instance base URL (e.g., https://n8n.example.com)
* @param trigger - Detected trigger information
* @param mode - 'production' uses /webhook/, 'test' uses /webhook-test/
*/
export function buildTriggerUrl(
baseUrl: string,
trigger: DetectedTrigger,
mode: 'production' | 'test' = 'production'
): string {
const cleanBaseUrl = baseUrl.replace(/\/+$/, ''); // Remove trailing slashes
switch (trigger.type) {
case 'webhook':
case 'chat': {
const prefix = mode === 'test' ? 'webhook-test' : 'webhook';
const path = trigger.webhookPath || trigger.node.id;
return `${cleanBaseUrl}/${prefix}/${path}`;
}
case 'form': {
// Form triggers use /form/<workflowId> endpoint
const prefix = mode === 'test' ? 'form-test' : 'form';
return `${cleanBaseUrl}/${prefix}/${trigger.node.id}`;
}
default:
throw new Error(`Cannot build URL for trigger type: ${trigger.type}`);
}
}
/**
* Get a human-readable description of the detected trigger
*/
export function describeTrigger(trigger: DetectedTrigger): string {
switch (trigger.type) {
case 'webhook':
return `Webhook trigger (${trigger.httpMethod || 'POST'} /${trigger.webhookPath || trigger.node.id})`;
case 'form':
const fieldCount = trigger.formFields?.length || 0;
return `Form trigger (${fieldCount} fields)`;
case 'chat':
return `Chat trigger (${trigger.chatConfig?.responseMode || 'lastNode'} mode)`;
default:
return 'Unknown trigger';
}
}