Repository: n8n-io/n8n
Pull request #14518: refactor(core): Remove legacy expression evaluator
Author: login: ivov
Labels: name: core
description: Enhancement outside /nodes-base and /editor-ui
name: n8n team
description: Authored by the n8n team
Created: 2025-04-10T09:50:24Z | Merged: 2025-05-13T15:29:49Z

## Summary

This PR removes the legacy expression evaluator `tmpl`. `tournament` has been the default expression evaluator for 19 months since #6964 and we have had no expression evaluation diff reports in the [past 90 days](https://n8nio.sentry.io/issues/?project=4503924908883968&query=is%3Aunresolved%20expression%20difference&referrer=issue-list&sort=freq&statsPeriod=90d).

## Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/CAT-58/drop-tmpl

## Review / Merge checklist

- [x] PR title and summary are descriptive. ([conventions](../blob/master/.github/pull_request_title_conventions.md)) <!--
   **Remember, the title automatically goes into the changelog.
   Use `(no-changelog)` otherwise.**
-->
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [ ] Tests included. <!--
   A bug is not considered fixed, unless a test is added to prevent it from happening again.
   A feature is not complete without tests.
-->
- [ ] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported)



Diff:
diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts
index 1a4f1cfc17469..ab436703da9aa 100644
--- a/packages/@n8n/api-types/src/frontend-settings.ts
+++ b/packages/@n8n/api-types/src/frontend-settings.ts
@@ -1,4 +1,4 @@
-import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
+import type { LogLevel, WorkflowSettings } from 'n8n-workflow';
 
 import { type InsightsDateRange } from './schemas/insights.schema';
 
@@ -157,9 +157,6 @@ export interface FrontendSettings {
 	variables: {
 		limit: number;
 	};
-	expressions: {
-		evaluator: ExpressionEvaluatorType;
-	};
 	mfa: {
 		enabled: boolean;
 	};
diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts
index 33de468bcfbe3..d3716457c1232 100644
--- a/packages/cli/src/commands/base-command.ts
+++ b/packages/cli/src/commands/base-command.ts
@@ -25,7 +25,6 @@ import { DeprecationService } from '@/deprecation/deprecation.service';
 import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee';
 import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
 import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
-import { initExpressionEvaluator } from '@/expression-evaluator';
 import { ExternalHooks } from '@/external-hooks';
 import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee';
 import { License } from '@/license';
@@ -116,7 +115,6 @@ export abstract class BaseCommand extends Command {
 			serverName: deploymentName,
 			releaseDate: N8N_RELEASE_DATE,
 		});
-		initExpressionEvaluator();
 
 		process.once('SIGTERM', this.onTerminationSignal('SIGTERM'));
 		process.once('SIGINT', this.onTerminationSignal('SIGINT'));
diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts
index 5905741cbbba4..6d7fde9259bae 100644
--- a/packages/cli/src/config/schema.ts
+++ b/packages/cli/src/config/schema.ts
@@ -279,21 +279,6 @@ export const schema = {
 		},
 	},
 
-	expression: {
-		evaluator: {
-			doc: 'Expression evaluator to use',
-			format: ['tmpl', 'tournament'] as const,
-			default: 'tournament',
-			env: 'N8N_EXPRESSION_EVALUATOR',
-		},
-		reportDifference: {
-			doc: 'Whether to report differences in the evaluator outputs',
-			format: Boolean,
-			default: false,
-			env: 'N8N_EXPRESSION_REPORT_DIFFERENCE',
-		},
-	},
-
 	proxy_hops: {
 		format: Number,
 		default: 0,
diff --git a/packages/cli/src/deprecation/deprecation.service.ts b/packages/cli/src/deprecation/deprecation.service.ts
index 59cb8ee813cb4..c4b2d5bb19e23 100644
--- a/packages/cli/src/deprecation/deprecation.service.ts
+++ b/packages/cli/src/deprecation/deprecation.service.ts
@@ -76,6 +76,14 @@ export class DeprecationService {
 			envVar: 'N8N_PARTIAL_EXECUTION_VERSION_DEFAULT',
 			message: 'This environment variable is internal and should not be set.',
 		},
+		{
+			envVar: 'N8N_EXPRESSION_EVALUATOR',
+			message: `n8n has replaced \`tmpl\` with \`tournament\` as expression evaluator. ${SAFE_TO_REMOVE}`,
+		},
+		{
+			envVar: 'N8N_EXPRESSION_REPORT_DIFFERENCE',
+			message: `n8n has replaced \`tmpl\` with \`tournament\` as expression evaluator. ${SAFE_TO_REMOVE}`,
+		},
 		{
 			envVar: 'EXECUTIONS_PROCESS',
 			message: SAFE_TO_REMOVE,
diff --git a/packages/cli/src/expression-evaluator.ts b/packages/cli/src/expression-evaluator.ts
deleted file mode 100644
index d77838dbc1590..0000000000000
--- a/packages/cli/src/expression-evaluator.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Container } from '@n8n/di';
-import { ErrorReporter } from 'n8n-core';
-import { ExpressionEvaluatorProxy } from 'n8n-workflow';
-
-import config from '@/config';
-
-export const initExpressionEvaluator = () => {
-	ExpressionEvaluatorProxy.setEvaluator(config.getEnv('expression.evaluator'));
-	ExpressionEvaluatorProxy.setDifferEnabled(config.getEnv('expression.reportDifference'));
-	ExpressionEvaluatorProxy.setDiffReporter((expr) => {
-		Container.get(ErrorReporter).warn('Expression difference', {
-			extra: {
-				expression: expr,
-			},
-		});
-	});
-};
diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts
index be64b7dcf39be..6dc2dc21333f0 100644
--- a/packages/cli/src/services/frontend.service.ts
+++ b/packages/cli/src/services/frontend.service.ts
@@ -220,9 +220,6 @@ export class FrontendService {
 			variables: {
 				limit: 0,
 			},
-			expressions: {
-				evaluator: config.getEnv('expression.evaluator'),
-			},
 			banners: {
 				dismissed: [],
 			},
diff --git a/packages/core/nodes-testing/node-test-harness.ts b/packages/core/nodes-testing/node-test-harness.ts
index 5d1ccf2766aef..43100c2e7c9cd 100644
--- a/packages/core/nodes-testing/node-test-harness.ts
+++ b/packages/core/nodes-testing/node-test-harness.ts
@@ -12,12 +12,7 @@ import type {
 	IWorkflowExecuteAdditionalData,
 	WorkflowTestData,
 } from 'n8n-workflow';
-import {
-	createDeferredPromise,
-	ExpressionEvaluatorProxy,
-	UnexpectedError,
-	Workflow,
-} from 'n8n-workflow';
+import { createDeferredPromise, UnexpectedError, Workflow } from 'n8n-workflow';
 import nock from 'nock';
 import { readFileSync, mkdtempSync, existsSync, rmSync } from 'node:fs';
 import { tmpdir } from 'node:os';
@@ -49,7 +44,6 @@ export class NodeTestHarness {
 	private readonly packagePaths: string[];
 
 	constructor({ additionalPackagePaths }: TestHarnessOptions = {}) {
-		ExpressionEvaluatorProxy.setEvaluator('tournament');
 		this.testDir = path.dirname(callsites()[1].getFileName()!);
 		this.packagePaths = additionalPackagePaths ?? [];
 		this.packagePaths.unshift(this.packageDir);
diff --git a/packages/frontend/editor-ui/src/__tests__/defaults.ts b/packages/frontend/editor-ui/src/__tests__/defaults.ts
index f3af21136d881..707790f5eb672 100644
--- a/packages/frontend/editor-ui/src/__tests__/defaults.ts
+++ b/packages/frontend/editor-ui/src/__tests__/defaults.ts
@@ -44,9 +44,6 @@ export const defaultSettings: FrontendSettings = {
 			},
 		},
 	},
-	expressions: {
-		evaluator: 'tournament',
-	},
 	executionMode: 'regular',
 	isMultiMain: false,
 	executionTimeout: 0,
diff --git a/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts b/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts
index a9f9bcb867adb..02140cb76185d 100644
--- a/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts
+++ b/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts
@@ -2,7 +2,7 @@ import { ref } from 'vue';
 import { useHistoryStore } from '@/stores/history.store';
 import { CUSTOM_API_CALL_KEY, PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
 
-import { NodeHelpers, ExpressionEvaluatorProxy, NodeConnectionTypes } from 'n8n-workflow';
+import { NodeHelpers, NodeConnectionTypes } from 'n8n-workflow';
 import type {
 	INodeProperties,
 	INodeCredentialDescription,
@@ -37,7 +37,6 @@ import type {
 
 import { isString } from '@/utils/typeGuards';
 import { isObject } from '@/utils/objectUtils';
-import { useSettingsStore } from '@/stores/settings.store';
 import { useWorkflowsStore } from '@/stores/workflows.store';
 import { useNodeTypesStore } from '@/stores/nodeTypes.store';
 import { useCredentialsStore } from '@/stores/credentials.store';
@@ -702,9 +701,6 @@ export function useNodeHelpers() {
 
 		if (nodeType?.subtitle !== undefined) {
 			try {
-				ExpressionEvaluatorProxy.setEvaluator(
-					useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
-				);
 				return workflow.expression.getSimpleParameterValue(
 					data,
 					nodeType.subtitle,
diff --git a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts
index e4de532599abe..acd49b58cdf3f 100644
--- a/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts
+++ b/packages/frontend/editor-ui/src/composables/useWorkflowHelpers.ts
@@ -24,12 +24,7 @@ import type {
 	NodeParameterValue,
 	Workflow,
 } from 'n8n-workflow';
-import {
-	NodeConnectionTypes,
-	ExpressionEvaluatorProxy,
-	NodeHelpers,
-	WEBHOOK_NODE_TYPE,
-} from 'n8n-workflow';
+import { NodeConnectionTypes, NodeHelpers, WEBHOOK_NODE_TYPE } from 'n8n-workflow';
 
 import type {
 	ICredentialsResponse,
@@ -60,7 +55,6 @@ import { useTemplatesStore } from '@/stores/templates.store';
 import { useUIStore } from '@/stores/ui.store';
 import { useWorkflowsStore } from '@/stores/workflows.store';
 import { getSourceItems } from '@/utils/pairedItemUtils';
-import { useSettingsStore } from '@/stores/settings.store';
 import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
 import { useDocumentTitle } from '@/composables/useDocumentTitle';
 import { useExternalHooks } from '@/composables/useExternalHooks';
@@ -229,9 +223,6 @@ export function resolveParameter<T = IDataObject>(
 		_executeData = executeData(parentNode, contextNode!.name, inputName, runIndexParent);
 	}
 
-	ExpressionEvaluatorProxy.setEvaluator(
-		useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
-	);
 	return workflow.expression.getParameterValue(
 		parameter,
 		runExecutionData,
diff --git a/packages/frontend/editor-ui/src/stores/settings.store.ts b/packages/frontend/editor-ui/src/stores/settings.store.ts
index 8a9ec36cfd660..05c488338afca 100644
--- a/packages/frontend/editor-ui/src/stores/settings.store.ts
+++ b/packages/frontend/editor-ui/src/stores/settings.store.ts
@@ -10,7 +10,6 @@ import type { ILdapConfig } from '@/Interface';
 import { STORES, INSECURE_CONNECTION_WARNING } from '@/constants';
 import { UserManagementAuthenticationMethod } from '@/Interface';
 import type { IDataObject, WorkflowSettings } from 'n8n-workflow';
-import { ExpressionEvaluatorProxy } from 'n8n-workflow';
 import { defineStore } from 'pinia';
 import { useRootStore } from './root.store';
 import { useUIStore } from './ui.store';
@@ -300,8 +299,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
 		try {
 			await getSettings();
 
-			ExpressionEvaluatorProxy.setEvaluator(settings.value.expressions.evaluator);
-
 			initialized.value = true;
 		} catch (e) {
 			showToast({
diff --git a/packages/workflow/package.json b/packages/workflow/package.json
index ffd6dd0ce6be0..7151c221ebb14 100644
--- a/packages/workflow/package.json
+++ b/packages/workflow/package.json
@@ -43,7 +43,6 @@
   },
   "dependencies": {
     "@n8n/tournament": "1.0.6",
-    "@n8n_io/riot-tmpl": "4.0.0",
     "ast-types": "0.15.2",
     "axios": "catalog:",
     "callsites": "catalog:",
diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts
index 1c956f0b5bb81..98f2873e9a22f 100644
--- a/packages/workflow/src/Expression.ts
+++ b/packages/workflow/src/Expression.ts
@@ -1,4 +1,3 @@
-import * as tmpl from '@n8n_io/riot-tmpl';
 import { DateTime, Duration, Interval } from 'luxon';
 
 import { ApplicationError } from './errors/application.error';
@@ -53,7 +52,7 @@ export class Expression {
 	constructor(private readonly workflow: Workflow) {}
 
 	static resolveWithoutWorkflow(expression: string, data: IDataObject = {}) {
-		return tmpl.tmpl(expression, data);
+		return evaluateExpression(expression, data);
 	}
 
 	/**
@@ -327,10 +326,7 @@ export class Expression {
 		return returnValue;
 	}
 
-	private renderExpression(
-		expression: string,
-		data: IWorkflowDataProxyData,
-	): tmpl.ReturnValue | undefined {
+	private renderExpression(expression: string, data: IWorkflowDataProxyData) {
 		try {
 			return evaluateExpression(expression, data);
 		} catch (error) {
diff --git a/packages/workflow/src/ExpressionEvaluatorProxy.ts b/packages/workflow/src/ExpressionEvaluatorProxy.ts
index 37dec8af51b03..6525b343a24ef 100644
--- a/packages/workflow/src/ExpressionEvaluatorProxy.ts
+++ b/packages/workflow/src/ExpressionEvaluatorProxy.ts
@@ -1,155 +1,21 @@
-import type { ReturnValue, TmplDifference } from '@n8n/tournament';
 import { Tournament } from '@n8n/tournament';
-import * as tmpl from '@n8n_io/riot-tmpl';
 
 import { PrototypeSanitizer } from './ExpressionSandboxing';
-import type { ExpressionEvaluatorType } from './Interfaces';
-import * as LoggerProxy from './LoggerProxy';
 
-type Evaluator = (expr: string, data: unknown) => tmpl.ReturnValue;
+type Evaluator = (expr: string, data: unknown) => string | null | (() => unknown);
 type ErrorHandler = (error: Error) => void;
-type DifferenceHandler = (expr: string) => void;
 
-// Set it to use double curly brackets instead of single ones
-tmpl.brackets.set('{{ }}');
-
-let errorHandler: ErrorHandler = () => {};
-let differenceHandler: DifferenceHandler = () => {};
-const differenceChecker = (diff: TmplDifference) => {
-	try {
-		if (diff.same) {
-			return;
-		}
-		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-		if (diff.has?.function || diff.has?.templateString) {
-			return;
-		}
-		if (diff.expression === 'UNPARSEABLE') {
-			differenceHandler(diff.expression);
-		} else {
-			differenceHandler(diff.expression.value);
-		}
-	} catch {
-		LoggerProxy.error('Expression evaluator difference checker failed');
-	}
-};
+const errorHandler: ErrorHandler = () => {};
 const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
 	before: [],
 	after: [PrototypeSanitizer],
 });
-let evaluator: Evaluator = tmpl.tmpl;
-let currentEvaluatorType: ExpressionEvaluatorType = 'tmpl';
-let diffExpressions = false;
+const evaluator: Evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator);
 
 export const setErrorHandler = (handler: ErrorHandler) => {
-	errorHandler = handler;
-	tmpl.tmpl.errorHandler = handler;
 	tournamentEvaluator.errorHandler = handler;
 };
 
-export const setEvaluator = (evalType: ExpressionEvaluatorType) => {
-	currentEvaluatorType = evalType;
-	if (evalType === 'tmpl') {
-		evaluator = tmpl.tmpl;
-	} else if (evalType === 'tournament') {
-		evaluator = tournamentEvaluator.execute.bind(tournamentEvaluator);
-	}
-};
-
-export const setDiffReporter = (reporter: (expr: string) => void) => {
-	differenceHandler = reporter;
-};
-
-export const setDifferEnabled = (enabled: boolean) => {
-	diffExpressions = enabled;
-};
-
-const diffCache: Record<string, TmplDifference | null> = {};
-
-export const checkEvaluatorDifferences = (expr: string): TmplDifference | null => {
-	if (expr in diffCache) {
-		return diffCache[expr];
-	}
-	let diff: TmplDifference | null;
-	try {
-		diff = tournamentEvaluator.tmplDiff(expr);
-	} catch {
-		// We don't include the expression for privacy reasons
-		try {
-			differenceHandler('ERROR');
-		} catch {}
-		diff = null;
-	}
-
-	if (diff?.same === false) {
-		differenceChecker(diff);
-	}
-
-	diffCache[expr] = diff;
-	return diff;
-};
-
-export const getEvaluator = () => {
-	return evaluator;
-};
-
 export const evaluateExpression: Evaluator = (expr, data) => {
-	if (!diffExpressions) {
-		return evaluator(expr, data);
-	}
-	const diff = checkEvaluatorDifferences(expr);
-
-	// We already know that they're different so don't bother
-	// evaluating with both evaluators
-	if (!diff?.same) {
-		return evaluator(expr, data);
-	}
-
-	let tmplValue: tmpl.ReturnValue;
-	let tournValue: ReturnValue;
-	let wasTmplError = false;
-	let tmplError: unknown;
-	let wasTournError = false;
-	let tournError: unknown;
-
-	try {
-		tmplValue = tmpl.tmpl(expr, data);
-	} catch (error) {
-		tmplError = error;
-		wasTmplError = true;
-	}
-
-	try {
-		tournValue = tournamentEvaluator.execute(expr, data);
-	} catch (error) {
-		tournError = error;
-		wasTournError = true;
-	}
-
-	if (
-		wasTmplError !== wasTournError ||
-		JSON.stringify(tmplValue!) !== JSON.stringify(tournValue!)
-	) {
-		try {
-			if (diff.expression) {
-				differenceHandler(diff.expression.value);
-			} else {
-				differenceHandler('VALUEDIFF');
-			}
-		} catch {
-			LoggerProxy.error('Failed to report error difference');
-		}
-	}
-
-	if (currentEvaluatorType === 'tmpl') {
-		if (wasTmplError) {
-			throw tmplError;
-		}
-		return tmplValue!;
-	}
-
-	if (wasTournError) {
-		throw tournError;
-	}
-	return tournValue!;
+	return evaluator(expr, data);
 };
diff --git a/packages/workflow/src/Extensions/ExpressionExtension.ts b/packages/workflow/src/Extensions/ExpressionExtension.ts
index 1f3b8d2fc0b32..81efe99ea7ac3 100644
--- a/packages/workflow/src/Extensions/ExpressionExtension.ts
+++ b/packages/workflow/src/Extensions/ExpressionExtension.ts
@@ -134,7 +134,7 @@ export const extendTransform = (expression: string): { code: string } | undefine
 				const chainNumber = currentChain;
 				currentChain += 1;
 
-				// This is to match our fork of tmpl
+				// This is to match behavior in our original expression evaluator (tmpl)
 				const globalIdentifier = types.builders.identifier(
 					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 					typeof window !== 'object' ? 'global' : 'window',
diff --git a/packages/workflow/src/Extensions/ExpressionParser.ts b/packages/workflow/src/Extensions/ExpressionParser.ts
index 3e94bfb2eb931..004211edc356f 100644
--- a/packages/workflow/src/Extensions/ExpressionParser.ts
+++ b/packages/workflow/src/Extensions/ExpressionParser.ts
@@ -6,8 +6,9 @@ export interface ExpressionText {
 export interface ExpressionCode {
 	type: 'code';
 	text: string;
-	// tmpl has different behaviours if the last expression
-	// doesn't close itself.
+
+	// This is to match behavior in our original expression evaluator (tmpl),
+	// which has different behaviours if the last expression doesn't close itself.
 	hasClosingBrackets: boolean;
 }
 
diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts
index ef9939d6021b3..40f825e552db4 100644
--- a/packages/workflow/src/Interfaces.ts
+++ b/packages/workflow/src/Interfaces.ts
@@ -2875,8 +2875,6 @@ export interface ICheckProcessedContextData {
 	};
 }
 
-export type ExpressionEvaluatorType = 'tmpl' | 'tournament';
-
 export type N8nAIProviderType = 'openai' | 'unknown';
 
 export interface SecretsHelpersBase {
diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts
index 4e13469fa9c1c..0ed7a2c93efdb 100644
--- a/packages/workflow/src/index.ts
+++ b/packages/workflow/src/index.ts
@@ -1,5 +1,4 @@
 import * as LoggerProxy from './LoggerProxy';
-export * as ExpressionEvaluatorProxy from './ExpressionEvaluatorProxy';
 import * as NodeHelpers from './NodeHelpers';
 import * as ObservableObject from './ObservableObject';
 import * as TelemetryHelpers from './TelemetryHelpers';
diff --git a/packages/workflow/test/Expression.test.ts b/packages/workflow/test/Expression.test.ts
index 22378a11a445a..a365a733b91d4 100644
--- a/packages/workflow/test/Expression.test.ts
+++ b/packages/workflow/test/Expression.test.ts
@@ -5,7 +5,6 @@
 import { DateTime, Duration, Interval } from 'luxon';
 
 import { ExpressionError } from '@/errors/expression.error';
-import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy';
 import { extendSyntax } from '@/Extensions/ExpressionExtension';
 import type { INodeExecutionData } from '@/Interfaces';
 import { Workflow } from '@/Workflow';
@@ -15,202 +14,206 @@ import { baseFixtures } from './ExpressionFixtures/base';
 import type { ExpressionTestEvaluation, ExpressionTestTransform } from './ExpressionFixtures/base';
 import * as Helpers from './Helpers';
 
-setDifferEnabled(true);
-
-for (const evaluator of ['tmpl', 'tournament'] as const) {
-	setEvaluator(evaluator);
-	describe(`Expression (with ${evaluator})`, () => {
-		describe('getParameterValue()', () => {
-			const nodeTypes = Helpers.NodeTypes();
-			const workflow = new Workflow({
-				id: '1',
-				nodes: [
-					{
-						name: 'node',
-						typeVersion: 1,
-						type: 'test.set',
-						id: 'uuid-1234',
-						position: [0, 0],
-						parameters: {},
-					},
-				],
-				connections: {},
-				active: false,
-				nodeTypes,
-			});
-			const expression = workflow.expression;
-
-			const evaluate = (value: string) =>
-				expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', {});
-
-			it('should not be able to use global built-ins from denylist', () => {
-				expect(evaluate('={{document}}')).toEqual({});
-				expect(evaluate('={{window}}')).toEqual({});
+describe('Expression', () => {
+	describe('getParameterValue()', () => {
+		const nodeTypes = Helpers.NodeTypes();
+		const workflow = new Workflow({
+			id: '1',
+			nodes: [
+				{
+					name: 'node',
+					typeVersion: 1,
+					type: 'test.set',
+					id: 'uuid-1234',
+					position: [0, 0],
+					parameters: {},
+				},
+			],
+			connections: {},
+			active: false,
+			nodeTypes,
+		});
+		const expression = workflow.expression;
 
-				expect(evaluate('={{Window}}')).toEqual({});
-				expect(evaluate('={{globalThis}}')).toEqual({});
-				expect(evaluate('={{self}}')).toEqual({});
+		const evaluate = (value: string) =>
+			expression.getParameterValue(value, null, 0, 0, 'node', [], 'manual', {});
 
-				expect(evaluate('={{alert}}')).toEqual({});
-				expect(evaluate('={{prompt}}')).toEqual({});
-				expect(evaluate('={{confirm}}')).toEqual({});
+		it('should not be able to use global built-ins from denylist', () => {
+			expect(evaluate('={{document}}')).toEqual({});
+			expect(evaluate('={{window}}')).toEqual({});
 
-				expect(evaluate('={{eval}}')).toEqual({});
-				expect(evaluate('={{uneval}}')).toEqual({});
-				expect(evaluate('={{setTimeout}}')).toEqual({});
-				expect(evaluate('={{setInterval}}')).toEqual({});
-				expect(evaluate('={{Function}}')).toEqual({});
+			expect(evaluate('={{Window}}')).toEqual({});
+			expect(evaluate('={{globalThis}}')).toEqual({});
+			expect(evaluate('={{self}}')).toEqual({});
 
-				expect(evaluate('={{fetch}}')).toEqual({});
-				expect(evaluate('={{XMLHttpRequest}}')).toEqual({});
+			expect(evaluate('={{alert}}')).toEqual({});
+			expect(evaluate('={{prompt}}')).toEqual({});
+			expect(evaluate('={{confirm}}')).toEqual({});
 
-				expect(evaluate('={{Promise}}')).toEqual({});
-				expect(evaluate('={{Generator}}')).toEqual({});
-				expect(evaluate('={{GeneratorFunction}}')).toEqual({});
-				expect(evaluate('={{AsyncFunction}}')).toEqual({});
-				expect(evaluate('={{AsyncGenerator}}')).toEqual({});
-				expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({});
+			expect(evaluate('={{eval}}')).toEqual({});
+			expect(evaluate('={{uneval}}')).toEqual({});
+			expect(evaluate('={{setTimeout}}')).toEqual({});
+			expect(evaluate('={{setInterval}}')).toEqual({});
+			expect(evaluate('={{Function}}')).toEqual({});
 
-				expect(evaluate('={{WebAssembly}}')).toEqual({});
+			expect(evaluate('={{fetch}}')).toEqual({});
+			expect(evaluate('={{XMLHttpRequest}}')).toEqual({});
 
-				expect(evaluate('={{Reflect}}')).toEqual({});
-				expect(evaluate('={{Proxy}}')).toEqual({});
+			expect(evaluate('={{Promise}}')).toEqual({});
+			expect(evaluate('={{Generator}}')).toEqual({});
+			expect(evaluate('={{GeneratorFunction}}')).toEqual({});
+			expect(evaluate('={{AsyncFunction}}')).toEqual({});
+			expect(evaluate('={{AsyncGenerator}}')).toEqual({});
+			expect(evaluate('={{AsyncGeneratorFunction}}')).toEqual({});
 
-				expect(() => evaluate('={{constructor}}')).toThrowError(
-					new ExpressionError('Cannot access "constructor" due to security concerns'),
-				);
+			expect(evaluate('={{WebAssembly}}')).toEqual({});
 
-				expect(evaluate('={{escape}}')).toEqual({});
-				expect(evaluate('={{unescape}}')).toEqual({});
-			});
+			expect(evaluate('={{Reflect}}')).toEqual({});
+			expect(evaluate('={{Proxy}}')).toEqual({});
 
-			it('should be able to use global built-ins from allowlist', () => {
-				expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date);
-				expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual(
-					DateTime.now().toLocaleString(),
-				);
+			expect(() => evaluate('={{constructor}}')).toThrowError(
+				new ExpressionError('Cannot access "constructor" due to security concerns'),
+			);
 
-				jest.useFakeTimers({ now: new Date() });
-				expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual(
-					Interval.after(new Date(), 100),
-				);
-				jest.useRealTimers();
-
-				expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100));
-
-				expect(evaluate('={{new Object()}}')).toEqual(new Object());
-
-				expect(evaluate('={{new Array()}}')).toEqual([]);
-				expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array());
-				expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array());
-				expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray());
-				expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array());
-				expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array());
-				expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array());
-				expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array());
-				expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array());
-				expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array());
-				expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array());
-				expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array());
-
-				expect(evaluate('={{new Map()}}')).toEqual(new Map());
-				expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap());
-				expect(evaluate('={{new Set()}}')).toEqual(new Set());
-				expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet());
-
-				expect(evaluate('={{new Error()}}')).toEqual(new Error());
-				expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError());
-				expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError());
-				expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError());
-				expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError());
-				expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError());
-				expect(evaluate('={{new URIError()}}')).toEqual(new URIError());
-
-				expect(evaluate('={{Intl}}')).toEqual(Intl);
-
-				expect(evaluate('={{new String()}}')).toEqual(new String());
-				expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp(''));
-
-				expect(evaluate('={{Math}}')).toEqual(Math);
-				expect(evaluate('={{new Number()}}')).toEqual(new Number());
-				expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1'));
-				expect(evaluate('={{Infinity}}')).toEqual(Infinity);
-				expect(evaluate('={{NaN}}')).toEqual(NaN);
-				expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1));
-				expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1));
-				expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1'));
-				expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10));
-
-				expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({}));
-				expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10));
-				expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10));
-				expect(evaluate('={{Atomics}}')).toEqual(Atomics);
-				expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual(
-					new DataView(new ArrayBuffer(1)),
-				);
+			expect(evaluate('={{escape}}')).toEqual({});
+			expect(evaluate('={{unescape}}')).toEqual({});
+		});
 
-				expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual(
-					encodeURI('https://google.com'),
-				);
-				expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual(
-					encodeURIComponent('https://google.com'),
-				);
-				expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual(
-					decodeURI('https://google.com'),
-				);
-				expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual(
-					decodeURIComponent('https://google.com'),
-				);
+		it('should be able to use global built-ins from allowlist', () => {
+			expect(evaluate('={{new Date()}}')).toBeInstanceOf(Date);
+			expect(evaluate('={{DateTime.now().toLocaleString()}}')).toEqual(
+				DateTime.now().toLocaleString(),
+			);
+
+			jest.useFakeTimers({ now: new Date() });
+			expect(evaluate('={{Interval.after(new Date(), 100)}}')).toEqual(
+				Interval.after(new Date(), 100),
+			);
+			jest.useRealTimers();
+
+			expect(evaluate('={{Duration.fromMillis(100)}}')).toEqual(Duration.fromMillis(100));
+
+			expect(evaluate('={{new Object()}}')).toEqual(new Object());
+
+			expect(evaluate('={{new Array()}}')).toEqual([]);
+			expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array());
+			expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array());
+			expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray());
+			expect(evaluate('={{new Int16Array()}}')).toEqual(new Int16Array());
+			expect(evaluate('={{new Uint16Array()}}')).toEqual(new Uint16Array());
+			expect(evaluate('={{new Int32Array()}}')).toEqual(new Int32Array());
+			expect(evaluate('={{new Uint32Array()}}')).toEqual(new Uint32Array());
+			expect(evaluate('={{new Float32Array()}}')).toEqual(new Float32Array());
+			expect(evaluate('={{new Float64Array()}}')).toEqual(new Float64Array());
+			expect(evaluate('={{new BigInt64Array()}}')).toEqual(new BigInt64Array());
+			expect(evaluate('={{new BigUint64Array()}}')).toEqual(new BigUint64Array());
+
+			expect(evaluate('={{new Map()}}')).toEqual(new Map());
+			expect(evaluate('={{new WeakMap()}}')).toEqual(new WeakMap());
+			expect(evaluate('={{new Set()}}')).toEqual(new Set());
+			expect(evaluate('={{new WeakSet()}}')).toEqual(new WeakSet());
+
+			expect(evaluate('={{new Error()}}')).toEqual(new Error());
+			expect(evaluate('={{new TypeError()}}')).toEqual(new TypeError());
+			expect(evaluate('={{new SyntaxError()}}')).toEqual(new SyntaxError());
+			expect(evaluate('={{new EvalError()}}')).toEqual(new EvalError());
+			expect(evaluate('={{new RangeError()}}')).toEqual(new RangeError());
+			expect(evaluate('={{new ReferenceError()}}')).toEqual(new ReferenceError());
+			expect(evaluate('={{new URIError()}}')).toEqual(new URIError());
+
+			expect(evaluate('={{Intl}}')).toEqual(Intl);
+
+			expect(evaluate('={{new String()}}')).toEqual(new String());
+			expect(evaluate("={{new RegExp('')}}")).toEqual(new RegExp(''));
+
+			expect(evaluate('={{Math}}')).toEqual(Math);
+			expect(evaluate('={{new Number()}}')).toEqual(new Number());
+			expect(evaluate("={{BigInt('1')}}")).toEqual(BigInt('1'));
+			expect(evaluate('={{Infinity}}')).toEqual(Infinity);
+			expect(evaluate('={{NaN}}')).toEqual(NaN);
+			expect(evaluate('={{isFinite(1)}}')).toEqual(isFinite(1));
+			expect(evaluate('={{isNaN(1)}}')).toEqual(isNaN(1));
+			expect(evaluate("={{parseFloat('1')}}")).toEqual(parseFloat('1'));
+			expect(evaluate("={{parseInt('1', 10)}}")).toEqual(parseInt('1', 10));
+
+			expect(evaluate('={{JSON.stringify({})}}')).toEqual(JSON.stringify({}));
+			expect(evaluate('={{new ArrayBuffer(10)}}')).toEqual(new ArrayBuffer(10));
+			expect(evaluate('={{new SharedArrayBuffer(10)}}')).toEqual(new SharedArrayBuffer(10));
+			expect(evaluate('={{Atomics}}')).toEqual(Atomics);
+			expect(evaluate('={{new DataView(new ArrayBuffer(1))}}')).toEqual(
+				new DataView(new ArrayBuffer(1)),
+			);
+
+			expect(evaluate("={{encodeURI('https://google.com')}}")).toEqual(
+				encodeURI('https://google.com'),
+			);
+			expect(evaluate("={{encodeURIComponent('https://google.com')}}")).toEqual(
+				encodeURIComponent('https://google.com'),
+			);
+			expect(evaluate("={{decodeURI('https://google.com')}}")).toEqual(
+				decodeURI('https://google.com'),
+			);
+			expect(evaluate("={{decodeURIComponent('https://google.com')}}")).toEqual(
+				decodeURIComponent('https://google.com'),
+			);
+
+			expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1));
+			expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString());
+		});
 
-				expect(evaluate('={{Boolean(1)}}')).toEqual(Boolean(1));
-				expect(evaluate('={{Symbol(1).toString()}}')).toEqual(Symbol(1).toString());
-			});
+		it('should not able to do arbitrary code execution', () => {
+			const testFn = jest.fn();
+			Object.assign(global, { testFn });
+			expect(() => evaluate("={{ Date['constructor']('testFn()')()}}")).toThrowError(
+				new ExpressionError('Cannot access "constructor" due to security concerns'),
+			);
+			expect(testFn).not.toHaveBeenCalled();
 		});
+	});
 
-		describe('Test all expression value fixtures', () => {
-			const expression = workflow.expression;
+	describe('Test all expression value fixtures', () => {
+		const expression = workflow.expression;
 
-			const evaluate = (value: string, data: INodeExecutionData[]) => {
-				const itemIndex = data.length === 0 ? -1 : 0;
-				return expression.getParameterValue(value, null, 0, itemIndex, 'node', data, 'manual', {});
-			};
+		const evaluate = (value: string, data: INodeExecutionData[]) => {
+			const itemIndex = data.length === 0 ? -1 : 0;
+			return expression.getParameterValue(value, null, 0, itemIndex, 'node', data, 'manual', {});
+		};
 
-			for (const t of baseFixtures) {
-				if (!t.tests.some((test) => test.type === 'evaluation')) {
-					continue;
-				}
-				test(t.expression, () => {
-					const evaluationTests = t.tests.filter(
-						(test): test is ExpressionTestEvaluation => test.type === 'evaluation',
-					);
-
-					for (const test of evaluationTests) {
-						const input = test.input.map((d) => ({ json: d })) as any;
-
-						if ('error' in test) {
-							expect(() => evaluate(t.expression, input)).toThrowError(test.error);
-						} else {
-							expect(evaluate(t.expression, input)).toStrictEqual(test.output);
-						}
-					}
-				});
+		for (const t of baseFixtures) {
+			if (!t.tests.some((test) => test.type === 'evaluation')) {
+				continue;
 			}
-		});
+			test(t.expression, () => {
+				const evaluationTests = t.tests.filter(
+					(test): test is ExpressionTestEvaluation => test.type === 'evaluation',
+				);
 
-		describe('Test all expression transform fixtures', () => {
-			for (const t of baseFixtures) {
-				if (!t.tests.some((test) => test.type === 'transform')) {
-					continue;
-				}
-				test(t.expression, () => {
-					for (const test of t.tests.filter(
-						(test): test is ExpressionTestTransform => test.type === 'transform',
-					)) {
-						const expr = t.expression;
-						expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr);
+				for (const test of evaluationTests) {
+					const input = test.input.map((d) => ({ json: d })) as any;
+
+					if ('error' in test) {
+						expect(() => evaluate(t.expression, input)).toThrowError(test.error);
+					} else {
+						expect(evaluate(t.expression, input)).toStrictEqual(test.output);
 					}
-				});
+				}
+			});
+		}
+	});
+
+	describe('Test all expression transform fixtures', () => {
+		for (const t of baseFixtures) {
+			if (!t.tests.some((test) => test.type === 'transform')) {
+				continue;
 			}
-		});
+			test(t.expression, () => {
+				for (const test of t.tests.filter(
+					(test): test is ExpressionTestTransform => test.type === 'transform',
+				)) {
+					const expr = t.expression;
+					expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr);
+				}
+			});
+		}
 	});
-}
+});
diff --git a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts
index fc107484cbb48..79c0d84da867b 100644
--- a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts
+++ b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts
@@ -30,7 +30,7 @@ describe('Expression Extension Transforms', () => {
 	});
 });
 
-describe('tmpl Expression Parser', () => {
+describe('Expression Parser', () => {
 	describe('Compatible splitting', () => {
 		test('Lone expression', () => {
 			expect(splitExpression('{{ "" }}')).toEqual([
diff --git a/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts
index 120e5aa9d7511..ef785c9db8d17 100644
--- a/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts
+++ b/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts
@@ -1,3 +1,4 @@
+import { ApplicationError } from '@/errors';
 import { objectExtensions } from '@/Extensions/ObjectExtensions';
 
 import { evaluate } from './Helpers';
@@ -126,7 +127,9 @@ describe('Data Transformation Functions', () => {
 			test('should not allow prototype pollution', () => {
 				['{__proto__: {polluted: true}}', '{constructor: {prototype: {polluted: true}}}'].forEach(
 					(testExpression) => {
-						expect(evaluate(`={{ (${testExpression}).compact() }}`)).toEqual(null);
+						expect(() => evaluate(`={{ (${testExpression}).compact() }}`)).toThrow(
+							ApplicationError,
+						);
 						expect(({} as any).polluted).toBeUndefined();
 					},
 				);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ac80f811a9e60..7bdfd7808f540 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -717,7 +717,7 @@ importers:
         version: 4.3.0
       '@getzep/zep-cloud':
         specifier: 1.0.12
-        version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586))
+        version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905))
       '@getzep/zep-js':
         specifier: 0.9.0
         version: 0.9.0
@@ -744,7 +744,7 @@ importers:
         version: 0.3.2(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
       '@langchain/community':
         specifier: 'catalog:'
-        version: 0.3.24(67fb36bad0bcdd2b0df3579415b33a93)
+        version: 0.3.24(0b620065402de60ffbc4ade3af2d8197)
       '@langchain/core':
         specifier: 'catalog:'
         version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
@@ -846,7 +846,7 @@ importers:
         version: 23.0.1
       langchain:
         specifier: 0.3.11
-        version: 0.3.11(b4eb53fe8b825d6e8edd96cc3d942586)
+        version: 0.3.11(7f2a4b9c5436679ca8b0df05212b4905)
       lodash:
         specifier: 'catalog:'
         version: 4.17.21
@@ -2486,9 +2486,6 @@ importers:
       '@n8n/tournament':
         specifier: 1.0.6
         version: 1.0.6
-      '@n8n_io/riot-tmpl':
-        specifier: 4.0.0
-        version: 4.0.0
       ast-types:
         specifier: 0.15.2
         version: 0.15.2
@@ -4953,9 +4950,6 @@ packages:
     resolution: {integrity: sha512-HrxdwlZw3MfYrfgvjxIGTyi3gJUCOFHtMcL5OpqpHhwfMGwuBauRgUOt63Twx4qs7f2bnz5yHV3jvuU3mF45UQ==}
     engines: {node: '>=18.12.1'}
 
-  '@n8n_io/riot-tmpl@4.0.0':
-    resolution: {integrity: sha512-/xw8HQgYQlBCrt3IKpNSSB1CgpP7XArw1QTRjP+KEw+OHT8XGvHxXrW9VGdUu9RwDnzm/LFu+dNLeDmwJMeOwQ==}
-
   '@n8n_io/riot-tmpl@4.0.1':
     resolution: {integrity: sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==}
 
@@ -16462,7 +16456,7 @@ snapshots:
   '@gar/promisify@1.1.3':
     optional: true
 
-  '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586))':
+  '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905))':
     dependencies:
       form-data: 4.0.0
       node-fetch: 2.7.0(encoding@0.1.13)
@@ -16471,7 +16465,7 @@ snapshots:
       zod: 3.24.1
     optionalDependencies:
       '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
-      langchain: 0.3.11(b4eb53fe8b825d6e8edd96cc3d942586)
+      langchain: 0.3.11(7f2a4b9c5436679ca8b0df05212b4905)
     transitivePeerDependencies:
       - encoding
 
@@ -16983,7 +16977,7 @@ snapshots:
       - aws-crt
       - encoding
 
-  '@langchain/community@0.3.24(67fb36bad0bcdd2b0df3579415b33a93)':
+  '@langchain/community@0.3.24(0b620065402de60ffbc4ade3af2d8197)':
     dependencies:
       '@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
       '@ibm-cloud/watsonx-ai': 1.1.2
@@ -16994,7 +16988,7 @@ snapshots:
       flat: 5.0.2
       ibm-cloud-sdk-core: 5.3.2
       js-yaml: 4.1.0
-      langchain: 0.3.11(b4eb53fe8b825d6e8edd96cc3d942586)
+      langchain: 0.3.11(7f2a4b9c5436679ca8b0df05212b4905)
       langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
       openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
       uuid: 10.0.0
@@ -17009,7 +17003,7 @@ snapshots:
       '@aws-sdk/credential-provider-node': 3.808.0
       '@azure/storage-blob': 12.26.0
       '@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
-      '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586))
+      '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905))
       '@getzep/zep-js': 0.9.0
       '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
       '@google-cloud/storage': 7.12.1(encoding@0.1.13)
@@ -17459,10 +17453,6 @@ snapshots:
       node-rsa: 1.1.1
       undici: 7.7.0
 
-  '@n8n_io/riot-tmpl@4.0.0':
-    dependencies:
-      eslint-config-riot: 1.0.0
-
   '@n8n_io/riot-tmpl@4.0.1':
     dependencies:
       eslint-config-riot: 1.0.0
@@ -23200,7 +23190,7 @@ snapshots:
       '@types/debug': 4.1.12
       '@types/node': 18.16.16
       '@types/tough-cookie': 4.0.2
-      axios: 1.8.3(debug@4.4.0)
+      axios: 1.8.3
       camelcase: 6.3.0
       debug: 4.4.0(supports-color@8.1.1)
       dotenv: 16.4.5
@@ -23210,7 +23200,7 @@ snapshots:
       isstream: 0.1.2
       jsonwebtoken: 9.0.2
       mime-types: 2.1.35
-      retry-axios: 2.6.0(axios@1.8.3)
+      retry-axios: 2.6.0(axios@1.8.3(debug@4.4.0))
       tough-cookie: 4.1.3
     transitivePeerDependencies:
       - supports-color
@@ -24204,7 +24194,7 @@ snapshots:
 
   kuler@2.0.0: {}
 
-  langchain@0.3.11(b4eb53fe8b825d6e8edd96cc3d942586):
+  langchain@0.3.11(7f2a4b9c5436679ca8b0df05212b4905):
     dependencies:
       '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
       '@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
@@ -26581,7 +26571,7 @@ snapshots:
       onetime: 5.1.2
       signal-exit: 3.0.7
 
-  retry-axios@2.6.0(axios@1.8.3):
+  retry-axios@2.6.0(axios@1.8.3(debug@4.4.0)):
     dependencies:
       axios: 1.8.3
 


Reviews:
user: login: ivov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T09:52:37Z
user: login: ivov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T11:02:48Z
user: login: ivov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T11:34:58Z
user: login: ivov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T12:45:16Z
user: login: CharlieKolb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:10:49Z
user: login: CharlieKolb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:12:52Z
user: login: CharlieKolb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:56:27Z
user: login: ivov
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T12:53:58Z
user: login: CharlieKolb
body: 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:29:15Z

Review comments:
diff_hunk: @@ -64,7 +63,7 @@ export class Expression {
 	constructor(private readonly workflow: Workflow) {}
 
 	static resolveWithoutWorkflow(expression: string, data: IDataObject = {}) {
-		return tmpl.tmpl(expression, data);
path: packages/workflow/src/Expression.ts
user: login: ivov
body: Likely due to an oversight, this was never switched to `tournament` back in the day.
created_at: 2025-04-10T09:52:37Z
author_association: MEMBER
diff_hunk: @@ -126,7 +127,9 @@ describe('Data Transformation Functions', () => {
 			test('should not allow prototype pollution', () => {
 				['{__proto__: {polluted: true}}', '{constructor: {prototype: {polluted: true}}}'].forEach(
 					(testExpression) => {
-						expect(evaluate(`={{ (${testExpression}).compact() }}`)).toEqual(null);
+						expect(() => evaluate(`={{ (${testExpression}).compact() }}`)).toThrow(
+							ApplicationError,
+						);
path: packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts
user: login: ivov
body: The `tournament` hook `PrototypeSanitizer` throws on prototype pollution, whereas `tmpl` seems to skip the assignment. This slipped through the diff checker because it [suppresses](https://github.com/n8n-io/n8n/blob/313cfec74da58dc2403d828a2a211c6c4004f7df/packages/workflow/src/ExpressionEvaluatorProxy.ts#L80) any errors thrown, and because `ObjectExtensions` wasn't testing both evaluators.

Decided to adjust the assertion because it is unlikely users are relying on a `null` coming from prototype pollution in an expression. There are no bug reports about this that I'm aware of. Please let me know if you disagree.

created_at: 2025-04-10T11:02:47Z
author_association: MEMBER
path: packages/core/src/binary-data/object-store/object-store.service.ee.ts
user: login: ivov
body: Had to add return type annotations to this file due to [this build error](https://github.com/n8n-io/n8n/actions/runs/14377502314/job/40313274552) but I'm unclear on its root cause.
created_at: 2025-04-10T11:34:57Z
author_association: MEMBER
path: packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts
user: login: ivov
body: Some change in this PR caused a single test in all of nodes-base to start failing, so I made this change here to fix it, but I don't understand the root cause of the failure yet.

<details>
<summary>Logs</summary>

```
nt -- entra/test/user

> n8n-nodes-base@1.87.0 test /Users/ivov/Development/n8n/packages/nodes-base
> jest -- entra/test/user

Nock: No match for request {
  "method": "PATCH",
  "url": "https://graph.microsoft.com/v1.0/users/87d349ed-44d7-43e1-9a83-5f2406dee5bd",
  "headers": {
    "accept": "application/json, text/plain, */*",
    "accept-encoding": "gzip, compress, deflate, br",
    "connection": "close",
    "content-length": "968",
    "content-type": "application/json",
    "user-agent": "n8n"
  },
  "body": "{\"accountEnabled\":true,\"ageGroup\":\"Adult\",\"businessPhones\":[\"0123456789\"],\"city\":\"New York\",\"companyName\":\"Contoso\",\"consentProvidedForMinor\":\"Granted\",\"country\":\"US\",\"department\":\"IT\",\"displayName\":\"Group Display Name\",\"employeeId\":\"employee-id-123\",\"employeeType\":\"Contractor\",\"givenName\":\"John\",\"employeeOrgData\":{\"costCenter\":\"Cost Center 1\",\"division\":\"Division 1\"},\"jobTitle\":\"Project manager\",\"surname\":\"Doe\",\"mail\":\"johndoe@contoso.com\",\"mailNickname\":\"MailNickname\",\"mobilePhone\":\"+123456789\",\"officeLocation\":\"New York\",\"onPremisesImmutableId\":\"premiseid123\",\"otherMails\":[\"johndoe2@contoso.com\",\"johndoe3@contoso.com\"],\"passwordProfile\":{\"password\":\"Test!12345\",\"forceChangePasswordNextSignInWithMfa\":true},\"passwordPolicies\":\"DisablePasswordExpiration,DisableStrongPassword\",\"postalCode\":\"0123456\",\"preferredLanguage\":\"en-US\",\"state\":\"New York\",\"streetAddress\":\"Street 123\",\"usageLocation\":\"US\",\"userPrincipalName\":\"johndoe@contoso.com\",\"userType\":\"Guest\"}"
}
Nock: No match for request {
  "method": "PATCH",
  "url": "https://graph.microsoft.com/v1.0/users/87d349ed-44d7-43e1-9a83-5f2406dee5bd",
  "headers": {
    "accept": "application/json, text/plain, */*",
    "accept-encoding": "gzip, compress, deflate, br",
    "connection": "close",
    "content-length": "968",
    "content-type": "application/json",
    "user-agent": "n8n"
  },
  "body": "{\"accountEnabled\":true,\"ageGroup\":\"Adult\",\"businessPhones\":[\"0123456789\"],\"city\":\"New York\",\"companyName\":\"Contoso\",\"consentProvidedForMinor\":\"Granted\",\"country\":\"US\",\"department\":\"IT\",\"displayName\":\"Group Display Name\",\"employeeId\":\"employee-id-123\",\"employeeType\":\"Contractor\",\"givenName\":\"John\",\"employeeOrgData\":{\"costCenter\":\"Cost Center 1\",\"division\":\"Division 1\"},\"jobTitle\":\"Project manager\",\"surname\":\"Doe\",\"mail\":\"johndoe@contoso.com\",\"mailNickname\":\"MailNickname\",\"mobilePhone\":\"+123456789\",\"officeLocation\":\"New York\",\"onPremisesImmutableId\":\"premiseid123\",\"otherMails\":[\"johndoe2@contoso.com\",\"johndoe3@contoso.com\"],\"passwordProfile\":{\"password\":\"Test!12345\",\"forceChangePasswordNextSignInWithMfa\":true},\"passwordPolicies\":\"DisablePasswordExpiration,DisableStrongPassword\",\"postalCode\":\"0123456\",\"preferredLanguage\":\"en-US\",\"state\":\"New York\",\"streetAddress\":\"Street 123\",\"usageLocation\":\"US\",\"userPrincipalName\":\"johndoe@contoso.com\",\"userType\":\"Guest\"}"
}
 FAIL  nodes/Microsoft/Entra/test/UserDescription.test.ts
  Microsoft Entra Node
    User description
      ✓ should add user to group (130 ms)
      ✓ should create user (39 ms)
      ✓ should delete user (18 ms)
      ✓ should get user (11 ms)
      ✓ should get user with options (13 ms)
      ✓ should get all users with simple output (40 ms)
      ✓ should get limit 10 users with raw output (15 ms)
      ✓ should get all users with options and filter (21 ms)
      ✓ should remove user from group (16 ms)
      ✕ should update user (103 ms)

  ● Microsoft Entra Node › User description › should update user

    NodeApiError: The resource you are requesting could not be found

      1312 |            }
      1313 |
    > 1314 |            throw new NodeApiError(this.getNode(), error);
           |                  ^
      1315 |    }
      1316 | }
      1317 |

      at ExecuteSingleContext.httpRequestWithAuthentication (../core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts:1314:9)
      at ExecuteSingleContext.httpRequestWithAuthentication (../core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts:1686:11)
      at RoutingNode.rawRoutingRequest (../core/src/execution-engine/routing-node.ts:568:20)
      at RoutingNode.makeRequest (../core/src/execution-engine/routing-node.ts:762:19)
          at async Promise.allSettled (index 0)
      at RoutingNode.runNode (../core/src/execution-engine/routing-node.ts:273:29)
      at WorkflowExecute.runNode (../core/src/execution-engine/workflow-execute.ts:1251:17)
      at ../core/src/execution-engine/workflow-execute.ts:1508:27
      at ../core/src/execution-engine/workflow-execute.ts:2065:11

Test Suites: 1 failed, 1 total
Tests:       1 failed, 9 passed, 10 total
Snapshots:   0 total
Time:        2.621 s, estimated 3 s
Ran all test suites matching /entra\/test\/user/i.
 ELIFECYCLE  Test failed. See above for more details.
```

</details>
created_at: 2025-04-10T12:45:16Z
author_association: MEMBER
path: packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts
user: login: CharlieKolb
body: ~~This actually fails locally for me (with a different equality error), but passes with the two values restored? 🙃 Do I need to recompile fully before testing nodes-base?~~
created_at: 2025-05-08T08:10:49Z
author_association: CONTRIBUTOR
path: packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts
user: login: CharlieKolb
body: Ah that changed it indeed, didn't realize that, makes sense I guess. But I do get a more normal error if I restore the test to what's on master:
```
    Custom message:
      Equality failed for "should update user" at node "Microsoft Entra ID"

    expect(received).toEqual(expected) // deep equality

    - Expected  - 7
    + Received  + 1

      Array [
    -   Array [
    -     Object {
    -       "json": Object {
    -         "updated": true,
    -       },
    -     },
    -   ],
    +   null,
      ]
```
created_at: 2025-05-08T08:12:52Z
author_association: CONTRIBUTOR
path: packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts
user: login: CharlieKolb
body: What I do find concerning here is that this test fails on master with these two properties removed. I couldn't quite make sense of this failure either. How risky do you think just changing the test is?
created_at: 2025-05-08T08:56:27Z
author_association: CONTRIBUTOR
path: packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts
user: login: ivov
body: https://n8nio.slack.com/archives/C069HS026UF/p1747124489622669
created_at: 2025-05-13T12:53:58Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-10T10:21:06Z
body: ## [Codecov](https://app.codecov.io/gh/n8n-io/n8n/pull/14518?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) Report
Attention: Patch coverage is `85.71429%` with `1 line` in your changes missing coverage. Please review.
| [Files with missing lines](https://app.codecov.io/gh/n8n-io/n8n/pull/14518?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) | Patch % | Lines |
|---|---|---|
| [packages/workflow/src/Expression.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/14518?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvd29ya2Zsb3cvc3JjL0V4cHJlc3Npb24udHM=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14518?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: ivov
created_at: 2025-05-12T14:34:01Z
body: Closing: https://n8nio.slack.com/archives/C069HS026UF/p1747059624855959
author_association: MEMBER
user: login: ivov
created_at: 2025-05-13T12:52:39Z
body: Reopening: https://n8nio.slack.com/archives/C069HS026UF/p1747124489622669
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-13T13:39:39Z
body: :warning: Some Cypress E2E specs are failing, please fix them before merging
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-13T14:41:54Z
body: :white_check_mark: All Cypress E2E specs passed
author_association: CONTRIBUTOR
user: login: janober
created_at: 2025-05-19T13:11:09Z
body: Got released with [`n8n@1.94.0`](https://github.com/n8n-io/n8n/releases/tag/n8n%401.94.0)
author_association: MEMBER