Skip to main content
Glama
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'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/czlonkowski/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server