
Security News
GPT-6 Astra Attempts Supply Chain Attacks Against Open Source Maintainers in Testing
GPT-6 Astra hits 100% on ExploitBench and finds zero-days autonomously, while independent tests reveal scope violations and monitoring gaps.
@n8n/expression-runtime
Advanced tools
Secure, isolated expression evaluation runtime for n8n workflows.
Shipped — the vm engine is n8n's default expression engine.
IsolatedVmBridge: V8 isolate management via isolated-vmExpressionEvaluator: tournament integration, expression code caching, isolate poolingN8N_EXPRESSION_ENGINE=legacy opts outpackages/cliComing later:
This package provides a secure runtime for evaluating expressions in isolated contexts.
Currently supports:
isolated-vm for V8 isolate-based isolation with lazy data loadingFuture support (Phase 2+):
ObservabilityProviderThisSanitizer, PrototypeSanitizer, DollarSignValidator) validate expressions before executionThe runtime uses a three-layer architecture:
See ARCHITECTURE.md for detailed design documentation.
pnpm add @n8n/expression-runtime
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
// Create evaluator with a bridge factory (bridges are pooled)
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ memoryLimit: 128, timeout: 5000 }),
maxCodeCacheSize: 1024,
});
// Initialize
await evaluator.initialize();
// Acquire an isolate for a caller, evaluate, release
const caller = {};
await evaluator.acquire(caller);
const result = evaluator.evaluate(
'{{ $json.user.email }}',
{
$json: {
user: { email: 'test@example.com' }
}
},
caller,
);
console.log(result); // "test@example.com"
await evaluator.release(caller);
// Clean up
await evaluator.dispose();
Pass AST security hooks from expression-sandboxing.ts to enable full security validation. This is the pattern used by the workflow package:
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
import {
ThisSanitizer,
PrototypeSanitizer,
DollarSignValidator,
} from 'n8n-workflow/expression-sandboxing';
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
maxCodeCacheSize: 1024,
hooks: {
before: [ThisSanitizer],
after: [PrototypeSanitizer, DollarSignValidator],
},
});
await evaluator.initialize();
When hooks is omitted the evaluator still runs tournament transformation (template parsing, this binding) but without AST security validation — suitable for development and testing.
Pass an ObservabilityProvider implementation to emit metrics, traces, and logs for evaluations:
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
maxCodeCacheSize: 1024,
observability,
});
This package defines the ObservabilityProvider interface; the production implementation lives in packages/cli/src/expression-observability/expression-observability.provider.ts and is wired up during backend startup. It is controlled via the N8N_EXPRESSION_ENGINE_OBSERVABILITY_* and N8N_EXPRESSION_ENGINE_TRACES_* environment variables (see below).
Main class for expression evaluation.
class ExpressionEvaluator {
constructor(config: EvaluatorConfig);
initialize(): Promise<void>;
acquire(owner: object): Promise<boolean>;
evaluate(expression: string, data: WorkflowData, caller: object, options?: EvaluateOptions): unknown;
release(owner: object): Promise<void>;
dispose(): Promise<void>;
isDisposed(): boolean;
}
Abstract interface for bridge implementations.
interface RuntimeBridge {
initialize(): Promise<void>;
execute(code: string, data: Record<string, unknown>): unknown;
dispose(): Promise<void>;
isDisposed(): boolean;
}
E() error handler for tournament-generated try-catch codeinterface EvaluatorConfig {
createBridge: () => RuntimeBridge; // required - factory, bridges are pooled
maxCodeCacheSize: number; // required - LRU size for tournament-transformed code
observability?: ObservabilityProvider; // optional - metrics/traces/logs provider
hooks?: TournamentHooks; // optional - AST security hooks for tournament
poolSize?: number; // optional - pre-warmed bridges, default 1
idleTimeoutMs?: number; // optional - scale pool to 0 after idle period
logger?: Logger; // optional - falls back to no-op
}
In n8n, the evaluator is configured via ExpressionEngineConfig (@n8n/config):
# Engine selection ('vm' is the default; 'legacy' opts out of isolation)
N8N_EXPRESSION_ENGINE=vm
# Isolate pool and code cache
N8N_EXPRESSION_ENGINE_POOL_SIZE=1
N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE=1024
N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT= # seconds; unset = pool never scales to 0
# Bridge limits
N8N_EXPRESSION_ENGINE_TIMEOUT=5000 # ms
N8N_EXPRESSION_ENGINE_MEMORY_LIMIT=128 # MB
# Observability
N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED=true
N8N_EXPRESSION_ENGINE_TRACES_ENABLED=true
N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS=50
N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE=0.0
See packages/@n8n/config/src/configs/expression-engine.config.ts for the authoritative list and defaults.
# Install dependencies
pnpm install
# Build package
pnpm build
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Type check
pnpm typecheck
# Lint
pnpm lint
The package uses vitest for fast, isolated testing:
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
describe('ExpressionEvaluator', () => {
it('evaluates simple expression', async () => {
const evaluator = new ExpressionEvaluator({
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
maxCodeCacheSize: 1024,
});
await evaluator.initialize();
const caller = {};
await evaluator.acquire(caller);
const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } }, caller);
expect(result).toBe(42);
await evaluator.release(caller);
await evaluator.dispose();
});
});
Run tests:
pnpm test # Run all tests
pnpm test integration # Run integration tests only
The runtime uses several optimizations (implemented in PRs 2–4):
Performance characteristics:
The runtime enforces strict security at multiple layers (implemented in PRs 2–4):
ThisSanitizer rewrites $json → this.$json; PrototypeSanitizer wraps computed property access in this.__sanitize(key) to block prototype chain attacks; DollarSignValidator enforces correct $-variable usage__sanitize() inside the isolate blocks access to __proto__, constructor, prototype, and other dangerous properties at runtimeFuture security features (Phase 2+):
See the main n8n repository for contribution guidelines.
See LICENSE.md in the n8n repository root.
FAQs
Secure, isolated expression evaluation runtime for n8n
The npm package @n8n/expression-runtime receives a total of 113,249 weekly downloads. As such, @n8n/expression-runtime popularity was classified as popular.
We found that @n8n/expression-runtime demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 5 open source maintainers collaborating on the project.

Security News
GPT-6 Astra hits 100% on ExploitBench and finds zero-days autonomously, while independent tests reveal scope violations and monitoring gaps.

Product
Socket can now send alerts and supply chain attack notifications to Microsoft Teams, with filters that route the right updates to each channel.

Security News
pnpm 12 rewrites the package manager in Rust, cutting install times by up to 90% while preserving pnpm 11 workflows and lockfiles.