Skip to main content
Glama
n8n-version.ts7.73 kB
/** * n8n Version Detection and Version-Aware Settings Filtering * * This module provides version detection for n8n instances and filters * workflow settings based on what the target n8n version supports. * * VERSION HISTORY for workflowSettings in n8n Public API: * - All versions: 7 core properties (saveExecutionProgress, saveManualExecutions, * saveDataErrorExecution, saveDataSuccessExecution, executionTimeout, * errorWorkflow, timezone) * - 1.37.0+: Added executionOrder * - 1.119.0+: Added callerPolicy, callerIds, timeSavedPerExecution, availableInMCP * * References: * - https://github.com/n8n-io/n8n/pull/21297 (PR adding 4 new properties in 1.119.0) * - https://community.n8n.io/t/n8n-api-update-workflow-does-not-accept-executionorder-setting/44512 */ import axios from 'axios'; import { logger } from '../utils/logger'; import { N8nVersionInfo, N8nSettingsResponse } from '../types/n8n-api'; // Cache version info per base URL with TTL to handle server upgrades interface CachedVersion { info: N8nVersionInfo; fetchedAt: number; } // Cache TTL: 5 minutes - allows for server upgrades without requiring restart const VERSION_CACHE_TTL_MS = 5 * 60 * 1000; const versionCache = new Map<string, CachedVersion>(); // Settings properties supported by each n8n version range // These are CUMULATIVE - each version adds to the previous const SETTINGS_BY_VERSION = { // Core properties supported by all versions core: [ 'saveExecutionProgress', 'saveManualExecutions', 'saveDataErrorExecution', 'saveDataSuccessExecution', 'executionTimeout', 'errorWorkflow', 'timezone', ], // Added in n8n 1.37.0 v1_37_0: [ 'executionOrder', ], // Added in n8n 1.119.0 (PR #21297) v1_119_0: [ 'callerPolicy', 'callerIds', 'timeSavedPerExecution', 'availableInMCP', ], }; /** * Parse version string into structured version info */ export function parseVersion(versionString: string): N8nVersionInfo | null { // Handle formats like "1.119.0", "1.37.0-beta.1", "0.200.0", "v1.2.3" // Support optional 'v' prefix for robustness const match = versionString.match(/^v?(\d+)\.(\d+)\.(\d+)/); if (!match) { return null; } return { version: versionString, major: parseInt(match[1], 10), minor: parseInt(match[2], 10), patch: parseInt(match[3], 10), }; } /** * Compare two versions: returns -1 if a < b, 0 if equal, 1 if a > b */ export function compareVersions(a: N8nVersionInfo, b: N8nVersionInfo): number { if (a.major !== b.major) return a.major - b.major; if (a.minor !== b.minor) return a.minor - b.minor; return a.patch - b.patch; } /** * Check if version meets minimum requirement */ export function versionAtLeast(version: N8nVersionInfo, major: number, minor: number, patch = 0): boolean { const target = { version: '', major, minor, patch }; return compareVersions(version, target) >= 0; } /** * Get supported settings properties for a given n8n version */ export function getSupportedSettingsProperties(version: N8nVersionInfo): Set<string> { const supported = new Set<string>(SETTINGS_BY_VERSION.core); // Add executionOrder if >= 1.37.0 if (versionAtLeast(version, 1, 37, 0)) { SETTINGS_BY_VERSION.v1_37_0.forEach(prop => supported.add(prop)); } // Add new properties if >= 1.119.0 if (versionAtLeast(version, 1, 119, 0)) { SETTINGS_BY_VERSION.v1_119_0.forEach(prop => supported.add(prop)); } return supported; } /** * Fetch n8n version from /rest/settings endpoint * * This endpoint is available on all n8n instances and doesn't require authentication. * Note: There's a security concern about this being unauthenticated (see n8n community), * but it's the only reliable way to get version info. */ export async function fetchN8nVersion(baseUrl: string): Promise<N8nVersionInfo | null> { // Check cache first (with TTL) const cached = versionCache.get(baseUrl); if (cached && Date.now() - cached.fetchedAt < VERSION_CACHE_TTL_MS) { logger.debug(`Using cached n8n version for ${baseUrl}: ${cached.info.version}`); return cached.info; } try { // Remove /api/v1 suffix if present to get base URL const cleanBaseUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '').replace(/\/$/, ''); const settingsUrl = `${cleanBaseUrl}/rest/settings`; logger.debug(`Fetching n8n version from ${settingsUrl}`); const response = await axios.get<N8nSettingsResponse>(settingsUrl, { timeout: 5000, validateStatus: (status: number) => status < 500, }); if (response.status === 200 && response.data) { // n8n wraps the settings in a "data" property const settings = response.data.data; if (!settings) { logger.warn('No data in settings response'); return null; } // n8n can return version in different fields - validate type const versionString = typeof settings.n8nVersion === 'string' ? settings.n8nVersion : typeof settings.versionCli === 'string' ? settings.versionCli : null; if (versionString) { const versionInfo = parseVersion(versionString); if (versionInfo) { // Cache the result with timestamp versionCache.set(baseUrl, { info: versionInfo, fetchedAt: Date.now() }); logger.debug(`Detected n8n version: ${versionInfo.version}`); return versionInfo; } } } logger.warn(`Could not determine n8n version from ${settingsUrl}`); return null; } catch (error) { logger.warn(`Failed to fetch n8n version: ${error instanceof Error ? error.message : 'Unknown error'}`); return null; } } /** * Clear version cache (useful for testing or when server changes) */ export function clearVersionCache(): void { versionCache.clear(); } /** * Get cached version for a base URL (or null if not cached or expired) */ export function getCachedVersion(baseUrl: string): N8nVersionInfo | null { const cached = versionCache.get(baseUrl); if (cached && Date.now() - cached.fetchedAt < VERSION_CACHE_TTL_MS) { return cached.info; } return null; } /** * Set cached version (useful for testing or when version is known) */ export function setCachedVersion(baseUrl: string, version: N8nVersionInfo): void { versionCache.set(baseUrl, { info: version, fetchedAt: Date.now() }); } /** * Clean workflow settings for API update based on n8n version * * This function filters workflow settings to only include properties * that the target n8n version supports, preventing "additional properties" errors. * * @param settings - The workflow settings to clean * @param version - The target n8n version (if null, returns settings unchanged) * @returns Cleaned settings object */ export function cleanSettingsForVersion( settings: Record<string, unknown> | undefined, version: N8nVersionInfo | null ): Record<string, unknown> { if (!settings || typeof settings !== 'object') { return {}; } // If version unknown, return settings unchanged (let the API decide) if (!version) { return settings; } const supportedProperties = getSupportedSettingsProperties(version); const cleaned: Record<string, unknown> = {}; for (const [key, value] of Object.entries(settings)) { if (supportedProperties.has(key)) { cleaned[key] = value; } else { logger.debug(`Filtered out unsupported settings property: ${key} (n8n ${version.version})`); } } return cleaned; } // Export version thresholds for testing export const VERSION_THRESHOLDS = { EXECUTION_ORDER: { major: 1, minor: 37, patch: 0 }, CALLER_POLICY: { major: 1, minor: 119, patch: 0 }, };

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