@n8n/expression-runtime 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,18 +4,16 @@ Secure, isolated expression evaluation runtime for n8n workflows.
4
4
 
5
5
  ## Status
6
6
 
7
- **In progress landing as a series of incremental PRs.**
8
-
9
- Implemented so far:
10
- - ✅ TypeScript interfaces and architecture design (PR 1)
11
- - ✅ Core architecture documentation (PR 1)
12
- - ✅ Runtime bundle: extension functions, deep lazy proxy system (PR 2)
13
- - ✅ `IsolatedVmBridge`: V8 isolate management via `isolated-vm` (PR 3)
14
- - ✅ `ExpressionEvaluator`: tournament integration, expression code caching (PR 4)
15
- - ✅ Integration tests (PR 4)
16
-
17
- Coming in later PRs:
18
- - 🚧 Workflow integration behind `N8N_EXPRESSION_ENGINE=vm` flag (PR 5)
7
+ **Shippedthe `vm` engine is n8n's default expression engine.**
8
+
9
+ - TypeScript interfaces and architecture design
10
+ - ✅ Runtime bundle: extension functions, deep lazy proxy system
11
+ - ✅ `IsolatedVmBridge`: V8 isolate management via `isolated-vm`
12
+ - ✅ `ExpressionEvaluator`: tournament integration, expression code caching, isolate pooling
13
+ - ✅ Workflow integration default engine; `N8N_EXPRESSION_ENGINE=legacy` opts out
14
+ - ✅ Observability (metrics, traces, logs) wired up in `packages/cli`
15
+
16
+ Coming later:
19
17
  - 🚧 Web Worker support (Phase 2+)
20
18
  - 🚧 Performance optimizations (Phase 3)
21
19
 
@@ -34,7 +32,7 @@ Future support (Phase 2+):
34
32
 
35
33
  - 🔒 **Secure**: Expressions run in isolated V8 contexts with memory limits (128MB) and timeouts (5s)
36
34
  - 🚀 **Performant**: Lazy data loading via proxies, script compilation caching, and expression code caching
37
- - 📊 **Observable**: Built-in metrics, traces, and logs support (interfaces defined; providers coming later)
35
+ - 📊 **Observable**: Built-in metrics, traces, and logs support via `ObservabilityProvider`
38
36
  - 🌐 **Universal**: Works in Node.js backend (browsers and task runners in Phase 2+)
39
37
  - 🛡️ **AST Security**: Tournament AST hooks (`ThisSanitizer`, `PrototypeSanitizer`, `DollarSignValidator`) validate expressions before execution
40
38
 
@@ -61,32 +59,33 @@ pnpm add @n8n/expression-runtime
61
59
  ```typescript
62
60
  import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
63
61
 
64
- // Create bridge
65
- const bridge = new IsolatedVmBridge({
66
- memoryLimit: 128,
67
- timeout: 5000,
68
- });
69
-
70
- // Create evaluator
62
+ // Create evaluator with a bridge factory (bridges are pooled)
71
63
  const evaluator = new ExpressionEvaluator({
72
- bridge,
64
+ createBridge: () => new IsolatedVmBridge({ memoryLimit: 128, timeout: 5000 }),
65
+ maxCodeCacheSize: 1024,
73
66
  });
74
67
 
75
68
  // Initialize
76
69
  await evaluator.initialize();
77
70
 
78
- // Evaluate expression using {{ }} template syntax
71
+ // Acquire an isolate for a caller, evaluate, release
72
+ const caller = {};
73
+ await evaluator.acquire(caller);
74
+
79
75
  const result = evaluator.evaluate(
80
76
  '{{ $json.user.email }}',
81
77
  {
82
78
  $json: {
83
79
  user: { email: 'test@example.com' }
84
80
  }
85
- }
81
+ },
82
+ caller,
86
83
  );
87
84
 
88
85
  console.log(result); // "test@example.com"
89
86
 
87
+ await evaluator.release(caller);
88
+
90
89
  // Clean up
91
90
  await evaluator.dispose();
92
91
  ```
@@ -103,9 +102,9 @@ import {
103
102
  DollarSignValidator,
104
103
  } from 'n8n-workflow/expression-sandboxing';
105
104
 
106
- const bridge = new IsolatedVmBridge({ timeout: 5000 });
107
105
  const evaluator = new ExpressionEvaluator({
108
- bridge,
106
+ createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
107
+ maxCodeCacheSize: 1024,
109
108
  hooks: {
110
109
  before: [ThisSanitizer],
111
110
  after: [PrototypeSanitizer, DollarSignValidator],
@@ -117,22 +116,19 @@ await evaluator.initialize();
117
116
 
118
117
  When `hooks` is omitted the evaluator still runs tournament transformation (template parsing, `this` binding) but without AST security validation — suitable for development and testing.
119
118
 
120
- ### With Observability (Not Yet Implemented)
119
+ ### With Observability
121
120
 
122
- ```typescript
123
- import { OpenTelemetryProvider } from '@n8n/expression-runtime/observability';
124
-
125
- const observability = new OpenTelemetryProvider({
126
- serviceName: 'n8n-expressions',
127
- });
121
+ Pass an `ObservabilityProvider` implementation to emit metrics, traces, and logs for evaluations:
128
122
 
123
+ ```typescript
129
124
  const evaluator = new ExpressionEvaluator({
130
- bridge,
125
+ createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
126
+ maxCodeCacheSize: 1024,
131
127
  observability,
132
128
  });
133
129
  ```
134
130
 
135
- **Note**: Observability providers are not yet implemented. The `ObservabilityProvider` interface exists but no implementations are available yet.
131
+ 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).
136
132
 
137
133
  ## API
138
134
 
@@ -144,7 +140,9 @@ Main class for expression evaluation.
144
140
  class ExpressionEvaluator {
145
141
  constructor(config: EvaluatorConfig);
146
142
  initialize(): Promise<void>;
147
- evaluate(expression: string, data: WorkflowData, options?: EvaluateOptions): unknown;
143
+ acquire(owner: object): Promise<boolean>;
144
+ evaluate(expression: string, data: WorkflowData, caller: object, options?: EvaluateOptions): unknown;
145
+ release(owner: object): Promise<void>;
148
146
  dispose(): Promise<void>;
149
147
  isDisposed(): boolean;
150
148
  }
@@ -179,38 +177,41 @@ interface RuntimeBridge {
179
177
 
180
178
  ```typescript
181
179
  interface EvaluatorConfig {
182
- bridge: RuntimeBridge; // required
183
- observability?: ObservabilityProvider; // optional - interfaces defined, providers not yet implemented
180
+ createBridge: () => RuntimeBridge; // required - factory, bridges are pooled
181
+ maxCodeCacheSize: number; // required - LRU size for tournament-transformed code
182
+ observability?: ObservabilityProvider; // optional - metrics/traces/logs provider
184
183
  hooks?: TournamentHooks; // optional - AST security hooks for tournament
185
- }
186
-
187
- interface BridgeConfig {
188
- memoryLimit?: number; // Default: 128 MB
189
- timeout?: number; // Default: 5000 ms
190
- debug?: boolean; // Default: false
184
+ poolSize?: number; // optional - pre-warmed bridges, default 1
185
+ idleTimeoutMs?: number; // optional - scale pool to 0 after idle period
186
+ logger?: Logger; // optional - falls back to no-op
191
187
  }
192
188
  ```
193
189
 
194
- ## Environment Variables (Not Yet Implemented)
190
+ ## Environment Variables
191
+
192
+ In n8n, the evaluator is configured via `ExpressionEngineConfig` (`@n8n/config`):
195
193
 
196
194
  ```bash
197
- # Bridge configuration (not yet implemented)
198
- N8N_EXPRESSION_MEMORY_LIMIT_MB=128
199
- N8N_EXPRESSION_TIMEOUT_MS=5000
200
- N8N_EXPRESSION_DEBUG=false
201
-
202
- # Code cache (not yet implemented - caches transformed code, not results)
203
- N8N_EXPRESSION_CODE_CACHE_ENABLED=true
204
- N8N_EXPRESSION_CODE_CACHE_MAX_SIZE=1000
205
-
206
- # Observability (not yet implemented)
207
- N8N_EXPRESSION_OBSERVABILITY_ENABLED=true
208
- N8N_EXPRESSION_METRICS_ENABLED=true
209
- N8N_EXPRESSION_TRACES_ENABLED=true
210
- N8N_EXPRESSION_TRACE_SAMPLE_RATE=0.01
195
+ # Engine selection ('vm' is the default; 'legacy' opts out of isolation)
196
+ N8N_EXPRESSION_ENGINE=vm
197
+
198
+ # Isolate pool and code cache
199
+ N8N_EXPRESSION_ENGINE_POOL_SIZE=1
200
+ N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE=1024
201
+ N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT= # seconds; unset = pool never scales to 0
202
+
203
+ # Bridge limits
204
+ N8N_EXPRESSION_ENGINE_TIMEOUT=5000 # ms
205
+ N8N_EXPRESSION_ENGINE_MEMORY_LIMIT=128 # MB
206
+
207
+ # Observability
208
+ N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED=true
209
+ N8N_EXPRESSION_ENGINE_TRACES_ENABLED=true
210
+ N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS=50
211
+ N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE=0.0
211
212
  ```
212
213
 
213
- **Note**: Currently, configuration is passed via constructor options. Environment variable support will be added in future phases.
214
+ See `packages/@n8n/config/src/configs/expression-engine.config.ts` for the authoritative list and defaults.
214
215
 
215
216
  ## Development
216
217
 
@@ -243,13 +244,18 @@ import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
243
244
 
244
245
  describe('ExpressionEvaluator', () => {
245
246
  it('evaluates simple expression', async () => {
246
- const bridge = new IsolatedVmBridge({ timeout: 5000 });
247
- const evaluator = new ExpressionEvaluator({ bridge });
247
+ const evaluator = new ExpressionEvaluator({
248
+ createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
249
+ maxCodeCacheSize: 1024,
250
+ });
248
251
 
249
252
  await evaluator.initialize();
250
253
 
251
- const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } });
254
+ const caller = {};
255
+ await evaluator.acquire(caller);
256
+ const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } }, caller);
252
257
  expect(result).toBe(42);
258
+ await evaluator.release(caller);
253
259
 
254
260
  await evaluator.dispose();
255
261
  });