Skip to main content
Glama
n8n-client.ts31.5 kB
import axios, { AxiosInstance } from 'axios'; import { logger, getLogLevel, redact } from './logger.js'; import { N8nWorkflow, N8nConfig, N8nApiResponse, N8nWorkflowsListResponse, N8nTag, N8nTagsListResponse, N8nVariable, N8nVariablesListResponse, N8nExecution, N8nExecutionsListResponse, N8nExecutionDeleteResponse, N8nWebhookUrls, N8nExecutionResponse, N8nCredential, N8nCredentialsListResponse, TransferRequest, TransferResponse, N8nCredentialSchema, N8nSourceControlPullResponse, N8nNode, CreateNodeRequest, CreateNodeResponse, UpdateNodeRequest, UpdateNodeResponse, ConnectNodesRequest, ConnectNodesResponse, DeleteNodeRequest, DeleteNodeResponse, SetNodePositionRequest, SetNodePositionResponse, PatchOperation, ApplyOpsResponse, N8nNodeType, N8nNodeExample, ValidationResult, EndpointAttempt, FallbackOperationResult } from './types.js'; import { WorkflowOperationsProcessor } from './operations.js'; import { getNodeTypes, getNodeType, getNodeExamples } from './node-registry.js'; import { validateFullNodeConfig } from './node-validator.js'; export class N8nClient { private api: AxiosInstance; private baseUrl: string; private operationsProcessor: WorkflowOperationsProcessor; constructor(config: N8nConfig) { this.baseUrl = config.baseUrl.replace(/\/$/, ''); this.api = axios.create({ baseURL: `${this.baseUrl}/api/v1`, headers: { 'Content-Type': 'application/json' }, }); this.operationsProcessor = new WorkflowOperationsProcessor(); // Setup authentication if (config.apiKey) { this.api.defaults.headers.common['X-N8N-API-KEY'] = config.apiKey; } else if (config.username && config.password) { const auth = Buffer.from(`${config.username}:${config.password}`).toString('base64'); this.api.defaults.headers.common['Authorization'] = `Basic ${auth}`; } // Axios interceptors for debug tracing (guard against mocked axios without interceptors) try { const interceptors: any = (this.api as any).interceptors; if (getLogLevel() === 'debug' && interceptors?.request?.use && interceptors?.response?.use) { interceptors.request.use((req: any) => { req.__start = Date.now(); logger.debug('HTTP request', { method: req.method, url: req.baseURL ? `${req.baseURL}${req.url}` : req.url, headers: redact(req.headers as any), params: redact(req.params as any), }); return req; }); interceptors.response.use( (res: any) => { const start = res.config?.__start || Date.now(); const durationMs = Date.now() - start; logger.debug('HTTP response', { status: res.status, url: res.config?.baseURL ? `${res.config.baseURL}${res.config.url}` : res.config?.url, durationMs, }); return res; }, (err: any) => { try { const cfg = err?.config || {}; const start = (cfg as any).__start || Date.now(); const durationMs = Date.now() - start; logger.error('HTTP error', { url: cfg.baseURL ? `${cfg.baseURL}${cfg.url}` : cfg.url, method: cfg.method, status: err?.response?.status, data: redact(err?.response?.data), durationMs, message: err?.message, }); } catch {} return Promise.reject(err); }, ); } } catch { // ignore interceptor setup errors in non-debug or mocked environments } } /** * Make a request to a REST endpoint (outside /api/v1) * Used for fallback to /rest/* endpoints */ private async requestRest<T = any>(method: string, path: string, data?: any): Promise<{ ok: boolean; status: number; data?: T; error?: any }> { try { const url = `${this.baseUrl}${path}`; const headers: Record<string, string> = { 'Content-Type': 'application/json' }; // Copy auth headers from the main API instance if (this.api.defaults.headers.common['X-N8N-API-KEY']) { headers['X-N8N-API-KEY'] = this.api.defaults.headers.common['X-N8N-API-KEY'] as string; } if (this.api.defaults.headers.common['Authorization']) { headers['Authorization'] = this.api.defaults.headers.common['Authorization'] as string; } const response = await axios.request<T>({ method, url, headers, data, }); return { ok: true, status: response.status, data: response.data }; } catch (error: any) { return { ok: false, status: error.response?.status || 0, error: error.response?.data || error.message, }; } } async listWorkflows(limit?: number, cursor?: string): Promise<N8nWorkflowsListResponse> { const params = new URLSearchParams(); if (limit) params.append('limit', limit.toString()); if (cursor) params.append('cursor', cursor); const url = params.toString() ? `/workflows?${params.toString()}` : '/workflows'; const response = await this.api.get<N8nWorkflowsListResponse>(url); return response.data; } async getWorkflow(id: string | number): Promise<N8nWorkflow> { const response = await this.api.get<N8nApiResponse<N8nWorkflow> | N8nWorkflow>(`/workflows/${id}`); const payload: any = response.data as any; return (payload && typeof payload === 'object' && 'data' in payload) ? payload.data : payload; } async getWorkflowWithETag(id: string | number): Promise<{ workflow: N8nWorkflow; etag: string | null }> { const response = await this.api.get<N8nApiResponse<N8nWorkflow> | N8nWorkflow>(`/workflows/${id}`); const headers: any = response.headers || {}; const etag = headers.etag || headers.ETag || headers['etag'] || headers['ETag'] || null; const payload: any = response.data as any; const workflow: N8nWorkflow = (payload && typeof payload === 'object' && 'data' in payload) ? payload.data : payload; return { workflow, etag }; } async createWorkflow(workflow: Omit<N8nWorkflow, 'id'>): Promise<N8nWorkflow> { // Resolve credential aliases before creating the workflow await this.resolveCredentialsInWorkflow(workflow); // Ensure required fields expected by n8n API if ((workflow as any).settings == null) { (workflow as any).settings = {}; } // Some n8n deployments may not accept non-numeric tag values on create. // If tags are provided as strings (names), omit them here; users can set numeric tag IDs via setWorkflowTags. if (Array.isArray((workflow as any).tags) && (workflow as any).tags.some((t: any) => typeof t !== 'number')) { delete (workflow as any).tags; } // 'active' is managed via dedicated activate/deactivate endpoints; omit on create if ('active' in (workflow as any)) { delete (workflow as any).active; } const response = await this.api.post<N8nApiResponse<N8nWorkflow> | N8nWorkflow>('/workflows', workflow); const payload: any = response.data as any; return (payload && typeof payload === 'object' && 'data' in payload) ? payload.data : payload; } async updateWorkflow(id: string | number, workflow: Partial<N8nWorkflow>, ifMatch?: string): Promise<N8nWorkflow> { // Resolve credential aliases before updating the workflow await this.resolveCredentialsInWorkflow(workflow); const headers: Record<string, string> = {}; // Allow 'active' to be updated via standard update to support tests and API compatibility if (ifMatch) headers['If-Match'] = ifMatch; try { const response = await this.api.put<N8nApiResponse<N8nWorkflow> | N8nWorkflow>(`/workflows/${id}`, workflow, { headers }); const payload: any = response.data as any; return (payload && typeof payload === 'object' && 'data' in payload) ? payload.data : payload; } catch (error: any) { if (error.response?.status === 412) { throw new Error( 'Precondition failed: The workflow has been modified by another user. Please fetch the latest version and try again.', ); } throw error; } } async deleteWorkflow(id: string | number): Promise<void> { await this.api.delete(`/workflows/${id}`); } async activateWorkflow(id: string | number): Promise<N8nWorkflow> { const response = await this.api.post<N8nApiResponse<N8nWorkflow> | N8nWorkflow>(`/workflows/${id}/activate`); const payload: any = response.data as any; return (payload && typeof payload === 'object' && 'data' in payload) ? payload.data : payload; } async deactivateWorkflow(id: string | number): Promise<N8nWorkflow> { const response = await this.api.post<N8nApiResponse<N8nWorkflow> | N8nWorkflow>(`/workflows/${id}/deactivate`); const payload: any = response.data as any; return (payload && typeof payload === 'object' && 'data' in payload) ? payload.data : payload; } async listCredentials(): Promise<N8nCredential[]> { const response = await this.api.get<N8nCredentialsListResponse>('/credentials'); return response.data.data; } async resolveCredentialAlias(alias: string): Promise<string> { const credentials = await this.listCredentials(); const matches = credentials.filter(cred => cred.name === alias); if (matches.length === 0) { throw new Error(`No credential found with alias: ${alias}`); } if (matches.length > 1) { throw new Error(`Multiple credentials found with alias: ${alias}. Found ${matches.length} matches.`); } return matches[0].id!.toString(); } async resolveCredentialsInWorkflow(workflow: Omit<N8nWorkflow, 'id'> | Partial<N8nWorkflow>): Promise<void> { if (!workflow.nodes) return; for (const node of workflow.nodes) { if (node.credentials) { for (const [credType, credValue] of Object.entries(node.credentials)) { // If the value doesn't look like an ID (not all digits), try to resolve as alias if (credValue && !/^\d+$/.test(credValue)) { try { node.credentials[credType] = await this.resolveCredentialAlias(credValue); } catch (error) { throw new Error(`Failed to resolve credential alias '${credValue}' for node '${node.name}': ${error instanceof Error ? error.message : String(error)}`); } } } } } } // Node type metadata methods /** * Get all available node types from curated catalog */ async getNodeTypes(): Promise<N8nNodeType[]> { // Try to get from n8n API first (if available in future) // For now, use curated catalog return getNodeTypes(); } /** * Get a specific node type by name */ async getNodeTypeByName(typeName: string): Promise<N8nNodeType | null> { // Try to get from n8n API first (if available in future) // For now, use curated catalog const nodeType = getNodeType(typeName); return nodeType || null; } /** * Get examples for a specific node type */ async getNodeTypeExamples(typeName: string): Promise<N8nNodeExample[]> { return getNodeExamples(typeName); } /** * Validate a node configuration */ async validateNodeConfiguration( nodeType: string, parameters: Record<string, any>, credentials?: Record<string, string> ): Promise<ValidationResult> { return validateFullNodeConfig(nodeType, parameters, credentials); } /** * Apply a batch of operations to a workflow atomically */ async applyOperations(workflowId: string | number, operations: PatchOperation[]): Promise<ApplyOpsResponse> { try { // Get the current workflow const currentWorkflow = await this.getWorkflow(workflowId); // Apply operations using the processor const result = await this.operationsProcessor.applyOperations(currentWorkflow, operations); if (!result.success) { return result; } // If operations were successful, update the workflow try { const updatedWorkflow = await this.updateWorkflow(workflowId, result.workflow!); return { success: true, workflow: updatedWorkflow }; } catch (error) { // Handle version drift or other update errors const errorMessage = error instanceof Error ? error.message : 'Failed to update workflow'; // Check if it's a version drift error (this depends on n8n's error response format) if (errorMessage.includes('version') || errorMessage.includes('conflict') || errorMessage.includes('409')) { return { success: false, errors: [{ operationIndex: -1, operation: operations[0], // fallback error: 'Version drift detected: workflow was modified by another process', details: errorMessage }] }; } return { success: false, errors: [{ operationIndex: -1, operation: operations[0], // fallback error: `Failed to save workflow: ${errorMessage}`, details: error }] }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { success: false, errors: [{ operationIndex: -1, operation: operations[0] || { type: 'unknown' } as any, error: `Failed to retrieve workflow: ${errorMessage}`, details: error }] }; } } // Graph mutation helpers private generateNodeId(): string { return `node_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; } private getDefaultPosition(existingNodes: N8nNode[]): [number, number] { if (existingNodes.length === 0) return [250, 300]; const rightmostX = Math.max(...existingNodes.map((node) => node.position[0])); return [rightmostX + 200, 300]; } private async performWorkflowUpdate( workflowId: string | number, operation: (workflow: N8nWorkflow) => void, maxRetries: number = 3, ): Promise<void> { let retries = 0; while (retries < maxRetries) { try { const { workflow, etag } = await this.getWorkflowWithETag(workflowId); operation(workflow); await this.updateWorkflow(workflowId, workflow, etag || undefined); return; } catch (error: any) { const message = error?.message || ''; if (message.includes('Precondition failed') && retries < maxRetries - 1) { retries++; await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retries) * 100)); continue; } throw error; } } } async createNode(request: CreateNodeRequest): Promise<CreateNodeResponse> { const nodeId = this.generateNodeId(); await this.performWorkflowUpdate(request.workflowId, (workflow) => { const position = request.position || this.getDefaultPosition(workflow.nodes); const newNode: N8nNode = { id: nodeId, name: request.name || request.type.split('.').pop() || 'New Node', type: request.type, typeVersion: 1, position, parameters: request.params || {}, credentials: request.credentials || {}, }; workflow.nodes.push(newNode); }); return { nodeId }; } async updateNode(request: UpdateNodeRequest): Promise<UpdateNodeResponse> { await this.performWorkflowUpdate(request.workflowId, (workflow) => { const nodeIndex = workflow.nodes.findIndex((node) => node.id === request.nodeId); if (nodeIndex === -1) { throw new Error(`Node with id ${request.nodeId} not found in workflow ${request.workflowId}`); } const node = workflow.nodes[nodeIndex]; if (request.params !== undefined) node.parameters = { ...node.parameters, ...request.params }; if (request.credentials !== undefined) node.credentials = { ...node.credentials, ...request.credentials }; if (request.name !== undefined) node.name = request.name; if (request.typeVersion !== undefined) node.typeVersion = request.typeVersion; }); return { nodeId: request.nodeId }; } async connectNodes(request: ConnectNodesRequest): Promise<ConnectNodesResponse> { await this.performWorkflowUpdate(request.workflowId, (workflow) => { const fromNode = workflow.nodes.find((node) => node.id === request.from.nodeId); const toNode = workflow.nodes.find((node) => node.id === request.to.nodeId); if (!fromNode) throw new Error(`Source node ${request.from.nodeId} not found in workflow ${request.workflowId}`); if (!toNode) throw new Error(`Target node ${request.to.nodeId} not found in workflow ${request.workflowId}`); const connections: any = workflow.connections as any; if (!connections[fromNode.name]) connections[fromNode.name] = {}; const fromMain = connections[fromNode.name].main || []; connections[fromNode.name].main = fromMain; const outputIndex = request.from.outputIndex ?? 0; if (!fromMain[outputIndex]) fromMain[outputIndex] = []; const connection = { node: toNode.name, type: 'main', index: request.to.inputIndex ?? 0 }; const exists = fromMain[outputIndex].some((conn: any) => conn.node === connection.node && conn.index === connection.index); if (!exists) fromMain[outputIndex].push(connection); }); return { ok: true }; } async deleteNode(request: DeleteNodeRequest): Promise<DeleteNodeResponse> { await this.performWorkflowUpdate(request.workflowId, (workflow) => { const nodeIndex = workflow.nodes.findIndex((node) => node.id === request.nodeId); if (nodeIndex === -1) { throw new Error(`Node with id ${request.nodeId} not found in workflow ${request.workflowId}`); } const nodeName = workflow.nodes[nodeIndex].name; workflow.nodes.splice(nodeIndex, 1); const connections: any = workflow.connections as any; delete connections[nodeName]; Object.keys(connections).forEach((sourceNodeName) => { Object.keys(connections[sourceNodeName]).forEach((outputType) => { connections[sourceNodeName][outputType] = connections[sourceNodeName][outputType].map((outputArray: any[]) => outputArray.filter((conn: any) => conn.node !== nodeName), ); }); }); }); return { ok: true }; } async setNodePosition(request: SetNodePositionRequest): Promise<SetNodePositionResponse> { await this.performWorkflowUpdate(request.workflowId, (workflow) => { const nodeIndex = workflow.nodes.findIndex((node) => node.id === request.nodeId); if (nodeIndex === -1) { throw new Error(`Node with id ${request.nodeId} not found in workflow ${request.workflowId}`); } workflow.nodes[nodeIndex].position = [request.x, request.y]; }); return { ok: true }; } async sourceControlPull(): Promise<N8nSourceControlPullResponse> { const response = await this.api.post<N8nApiResponse<N8nSourceControlPullResponse>>('/source-control/pull'); return response.data.data; } async getCredentialSchema(credentialTypeName: string): Promise<N8nCredentialSchema> { const response = await this.api.get<N8nApiResponse<N8nCredentialSchema>>(`/credential-types/${credentialTypeName}`); return response.data.data; } async transferWorkflow(id: string | number, transferData: TransferRequest): Promise<TransferResponse> { const response = await this.api.put<N8nApiResponse<TransferResponse>>(`/workflows/${id}/transfer`, transferData); return response.data.data; } async transferCredential(id: string | number, transferData: TransferRequest): Promise<TransferResponse> { const response = await this.api.put<N8nApiResponse<TransferResponse>>(`/credentials/${id}/transfer`, transferData); return response.data.data; } async listWorkflowTags(workflowId: string | number): Promise<N8nTag[]> { const response = await this.api.get<N8nApiResponse<N8nTag[]>>(`/workflows/${workflowId}/tags`); return response.data.data; } async setWorkflowTags(workflowId: string | number, tagIds: (string | number)[]): Promise<N8nTag[]> { const attempts: EndpointAttempt[] = []; // Get tag names for fallback strategies let tagNames: string[] = []; try { const allTags = await this.listTags(); tagNames = tagIds .map(id => allTags.data.find(t => t.id == id)?.name) .filter((name): name is string => !!name); } catch (e) { // If we can't get tag names, we'll skip name-based strategies } // Strategy 1: PATCH /rest/workflows/{id} with tag names // Only run if ALL tagIds resolved to names to avoid dropping tags if (tagNames.length > 0 && tagNames.length === tagIds.length) { const patchRestNames = await this.requestRest('PATCH', `/rest/workflows/${workflowId}`, { tags: tagNames }); attempts.push({ endpoint: `/rest/workflows/${workflowId}`, method: 'PATCH (names)', status: patchRestNames.status, message: String(patchRestNames.error?.message ?? patchRestNames.error ?? '') }); if (patchRestNames.ok) { // Fetch and return the updated tags try { return await this.listWorkflowTags(workflowId); } catch (e) { // If list fails, return empty array but log success logger.debug('Tags set successfully but unable to list them', { workflowId }); return []; } } } // Strategy 2: PATCH /rest/workflows/{id} with tag IDs as objects const tagIdObjects = tagIds.map(id => ({ id })); const patchRestIds = await this.requestRest('PATCH', `/rest/workflows/${workflowId}`, { tags: tagIdObjects }); attempts.push({ endpoint: `/rest/workflows/${workflowId}`, method: 'PATCH (id objects)', status: patchRestIds.status, message: String(patchRestIds.error?.message ?? patchRestIds.error ?? '') }); if (patchRestIds.ok) { try { return await this.listWorkflowTags(workflowId); } catch (e) { logger.debug('Tags set successfully but unable to list them', { workflowId }); return []; } } // Strategy 3: PUT /api/v1/workflows/{id}/tags with tagIds (current default) try { const response = await this.api.put<N8nApiResponse<N8nTag[]>>(`/workflows/${workflowId}/tags`, { tagIds }); attempts.push({ endpoint: `/api/v1/workflows/${workflowId}/tags`, method: 'PUT', status: response.status, }); return response.data.data; } catch (error: any) { attempts.push({ endpoint: `/api/v1/workflows/${workflowId}/tags`, method: 'PUT', status: error.response?.status || 0, message: error.response?.data?.message || error.message }); // All strategies failed, throw with details const errorMsg = `Unable to set workflow tags. Attempted endpoints: ${attempts.map(a => `${a.method} ${a.endpoint} (${a.status})`).join(', ')}`; const err = new Error(errorMsg); (err as any).attempts = attempts; throw err; } } async listTags(limit?: number, cursor?: string): Promise<N8nTagsListResponse> { const params = new URLSearchParams(); if (limit) params.append('limit', limit.toString()); if (cursor) params.append('cursor', cursor); const queryString = params.toString(); const url = queryString ? `/tags?${queryString}` : '/tags'; try { const response = await this.api.get<N8nTagsListResponse>(url); return response.data; } catch (error: any) { // Fallback to /rest/tags if /api/v1/tags fails if (error.response?.status === 404 || error.response?.status === 401) { const restPath = queryString ? `/rest/tags?${queryString}` : '/rest/tags'; const restResponse = await this.requestRest<N8nTagsListResponse>('GET', restPath); if (restResponse.ok && restResponse.data) { // Normalize response - /rest might return array directly or { data: [] } const data = restResponse.data; if (Array.isArray(data)) { return { data, nextCursor: undefined }; } return data; } } throw error; } } async getTag(id: string | number): Promise<N8nTag> { const response = await this.api.get<N8nApiResponse<N8nTag>>(`/tags/${id}`); return response.data.data; } async createTag(tag: Omit<N8nTag, 'id' | 'createdAt' | 'updatedAt'>): Promise<N8nTag> { const response = await this.api.post<N8nApiResponse<N8nTag>>('/tags', tag); return response.data.data; } async updateTag(id: string | number, tag: Partial<Omit<N8nTag, 'id' | 'createdAt' | 'updatedAt'>>): Promise<N8nTag> { const attempts: EndpointAttempt[] = []; // Strategy 1: Try PATCH on /rest/tags/{id} const patchRest = await this.requestRest('PATCH', `/rest/tags/${id}`, tag); attempts.push({ endpoint: `/rest/tags/${id}`, method: 'PATCH', status: patchRest.status, message: String(patchRest.error?.message ?? patchRest.error ?? '') }); if (patchRest.ok && patchRest.data) { // Unwrap response if needed const result = (patchRest.data as any)?.data || patchRest.data; return result as N8nTag; } // Strategy 2: Try PUT on /api/v1/tags/{id} // PUT typically requires a full payload, so if the incoming tag is partial, fetch and merge try { let fullPayload = tag; // Check if we need to fetch the existing tag for a merge // If name is missing, we need to fetch the existing tag if (!tag.name) { try { const existingTag = await this.getTag(id); // Merge existing tag with provided updates fullPayload = { name: existingTag.name, ...tag }; } catch (fetchError) { // If we can't fetch, try the PUT anyway with what we have logger.debug('Could not fetch existing tag for merge', { id, error: fetchError }); } } const response = await this.api.put<N8nApiResponse<N8nTag>>(`/tags/${id}`, fullPayload); attempts.push({ endpoint: `/api/v1/tags/${id}`, method: 'PUT', status: response.status, message: undefined }); return response.data.data; } catch (error: any) { attempts.push({ endpoint: `/api/v1/tags/${id}`, method: 'PUT', status: error.response?.status || 0, message: String(error.response?.data?.message ?? error.message ?? error ?? '') }); // If updating color specifically and all endpoints failed, provide helpful message if (tag.color && !tag.name) { const errorMsg = `Unable to update tag color. Attempted endpoints: ${attempts.map(a => `${a.method} ${a.endpoint} (${a.status})`).join(', ')}. Tag color may need to be set via the n8n UI for this instance.`; const err = new Error(errorMsg); (err as any).attempts = attempts; throw err; } throw error; } } async deleteTag(id: string | number): Promise<void> { await this.api.delete(`/tags/${id}`); } async listVariables(limit?: number, cursor?: string): Promise<N8nVariablesListResponse> { const params = new URLSearchParams(); if (limit) params.append('limit', limit.toString()); if (cursor) params.append('cursor', cursor); const url = params.toString() ? `/variables?${params.toString()}` : '/variables'; const response = await this.api.get<N8nVariablesListResponse>(url); return response.data; } async createVariable(variable: Omit<N8nVariable, 'id'>): Promise<N8nVariable> { const response = await this.api.post<N8nApiResponse<N8nVariable>>('/variables', variable); return response.data.data; } async updateVariable(id: string, variable: Partial<N8nVariable>): Promise<N8nVariable> { const response = await this.api.put<N8nApiResponse<N8nVariable>>(`/variables/${id}`, variable); return response.data.data; } async deleteVariable(id: string): Promise<{ ok: boolean }> { await this.api.delete(`/variables/${id}`); return { ok: true }; } async listExecutions(options?: { limit?: number; cursor?: string; workflowId?: string }): Promise<N8nExecutionsListResponse> { const params = new URLSearchParams(); if (options?.limit) params.append('limit', options.limit.toString()); if (options?.cursor) params.append('cursor', options.cursor); if (options?.workflowId) params.append('workflowId', options.workflowId); const url = `/executions${params.toString() ? `?${params.toString()}` : ''}`; const response = await this.api.get<N8nExecutionsListResponse>(url); return response.data; } async getExecution(id: string): Promise<N8nExecution> { const response = await this.api.get<N8nApiResponse<N8nExecution>>(`/executions/${id}`); return response.data.data; } async deleteExecution(id: string): Promise<N8nExecutionDeleteResponse> { await this.api.delete(`/executions/${id}`); return { success: true }; } async getWebhookUrls(workflowId: string | number, nodeId: string): Promise<N8nWebhookUrls> { const workflow = await this.getWorkflow(workflowId); const webhookNode = workflow.nodes.find((node) => node.id === nodeId); if (!webhookNode) throw new Error(`Node with ID '${nodeId}' not found in workflow ${workflowId}`); if (webhookNode.type !== 'n8n-nodes-base.webhook') { throw new Error(`Node '${nodeId}' is not a webhook node (type: ${webhookNode.type})`); } const path = webhookNode.parameters?.path || ''; if (!path) throw new Error(`Webhook node '${nodeId}' does not have a path configured`); const testUrl = `${this.baseUrl}/webhook-test/${path}`; const productionUrl = `${this.baseUrl}/webhook/${path}`; return { testUrl, productionUrl }; } async runOnce(workflowId: string | number, input?: any): Promise<N8nExecutionResponse> { try { const workflow = await this.getWorkflow(workflowId); const hasTriggerNodes = workflow.nodes.some( (node) => node.type === 'n8n-nodes-base.webhook' || node.type === 'n8n-nodes-base.cron' || node.type.includes('trigger'), ); if (hasTriggerNodes) { const executionData = { workflowData: workflow, runData: input || {} }; const response = await this.api.post<N8nApiResponse<any>>('/executions', executionData); return { executionId: response.data.data.id || response.data.data.executionId, status: response.data.data.status || 'running' }; } else { const response = await this.api.post<N8nApiResponse<any>>(`/workflows/${workflowId}/execute`, { data: input || {} }); return { executionId: response.data.data.id || response.data.data.executionId, status: response.data.data.status || 'running' }; } } catch (error: any) { if (error instanceof Error && error.message.includes('404')) { throw new Error(`Workflow ${workflowId} not found or cannot be executed manually`); } throw error; } } }

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/get2knowio/n8n-mcp'

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