packages/@n8n/expression-runtime/README.md
Secure, isolated expression evaluation runtime for n8n workflows.
In progress — landing as a series of incremental PRs.
Implemented so far:
IsolatedVmBridge: V8 isolate management via isolated-vm (PR 3)ExpressionEvaluator: tournament integration, expression code caching (PR 4)Coming in later PRs:
N8N_EXPRESSION_ENGINE=vm flag (PR 5)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+):
ThisSanitizer, 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 bridge
const bridge = new IsolatedVmBridge({
memoryLimit: 128,
timeout: 5000,
});
// Create evaluator
const evaluator = new ExpressionEvaluator({
bridge,
});
// Initialize
await evaluator.initialize();
// Evaluate expression using {{ }} template syntax
const result = evaluator.evaluate(
'{{ $json.user.email }}',
{
$json: {
user: { email: '[email protected]' }
}
}
);
console.log(result); // "[email protected]"
// 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 bridge = new IsolatedVmBridge({ timeout: 5000 });
const evaluator = new ExpressionEvaluator({
bridge,
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.
import { OpenTelemetryProvider } from '@n8n/expression-runtime/observability';
const observability = new OpenTelemetryProvider({
serviceName: 'n8n-expressions',
});
const evaluator = new ExpressionEvaluator({
bridge,
observability,
});
Note: Observability providers are not yet implemented. The ObservabilityProvider interface exists but no implementations are available yet.
Main class for expression evaluation.
class ExpressionEvaluator {
constructor(config: EvaluatorConfig);
initialize(): Promise<void>;
evaluate(expression: string, data: WorkflowData, options?: EvaluateOptions): unknown;
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 {
bridge: RuntimeBridge; // required
observability?: ObservabilityProvider; // optional - interfaces defined, providers not yet implemented
hooks?: TournamentHooks; // optional - AST security hooks for tournament
}
interface BridgeConfig {
memoryLimit?: number; // Default: 128 MB
timeout?: number; // Default: 5000 ms
debug?: boolean; // Default: false
}
# Bridge configuration (not yet implemented)
N8N_EXPRESSION_MEMORY_LIMIT_MB=128
N8N_EXPRESSION_TIMEOUT_MS=5000
N8N_EXPRESSION_DEBUG=false
# Code cache (not yet implemented - caches transformed code, not results)
N8N_EXPRESSION_CODE_CACHE_ENABLED=true
N8N_EXPRESSION_CODE_CACHE_MAX_SIZE=1000
# Observability (not yet implemented)
N8N_EXPRESSION_OBSERVABILITY_ENABLED=true
N8N_EXPRESSION_METRICS_ENABLED=true
N8N_EXPRESSION_TRACES_ENABLED=true
N8N_EXPRESSION_TRACE_SAMPLE_RATE=0.01
Note: Currently, configuration is passed via constructor options. Environment variable support will be added in future phases.
# 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 bridge = new IsolatedVmBridge({ timeout: 5000 });
const evaluator = new ExpressionEvaluator({ bridge });
await evaluator.initialize();
const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } });
expect(result).toBe(42);
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.