Expression engine now resolves variables lazily on demand
Credential nodes using dynamic expressions now authenticate correctly when the VM engine is enabled. The fix replaces a hardcoded list of variables with a Proxy that resolves any key on demand — no more manual updates when new variables are added, and 17-39% faster context setup.
When the VM expression engine was enabled, credentials using expression-based authentication silently failed with 401 errors. Expressions like ={{$credentials.serviceRole}} resolved to undefined because the VM isolate only knew about a hardcoded set of variables — $credentials, $request, $response, and dozens of others were simply invisible.
The expression engine now uses a Proxy-based approach that resolves any variable on demand from the host, instead of maintaining a manually-updated list. The first time a piece of code checks for a key (via the in operator) or reads a property, the Proxy intercepts the lookup, fetches the value from the host, and caches it for the rest of that evaluation. Already-known values like $now, $today, and the DateTime class return immediately with no round-trip.
This eliminates the maintenance burden of keeping key lists in sync with the data proxy. It also removes seven eager fetches and four host function probes that happened during context setup regardless of whether those variables were used. The result: 17-39% faster expression evaluation across benchmarks.
The change lives in the expression runtime package, part of ongoing work to make the VM expression engine production-ready.
View Original GitHub Description
Summary
When N8N_EXPRESSION_ENGINE=vm is enabled, credential types using IAuthenticateGeneric with expression-based authentication (e.g. Supabase, Airtable, Notion) fail with 401 errors. The root cause: buildContext() in the VM isolate only registered a hardcoded set of known properties. Keys like $credentials — passed as additionalKeys during credential resolution — were invisible inside the isolate, so expressions like ={{$credentials.serviceRole}} silently resolved to undefined.
Many other keys were also missing: $request, $response, $pageCount, $self, $evaluateExpression, $jmesPath, $getPairedItem, $rawParameter, $agentInfo, etc.
Fix: Replace the plain context object in buildContext() with a Proxy whose has/get traps lazily resolve unknown keys from the host via the existing getValueAtPath callback. Each key is resolved at most once per evaluation and cached on the proxy target.
This approach:
- Eliminates all hardcoded key lists — no more sync issues when new keys are added to the data proxy
- Removes 7 eager
fetchPrimitive()round-trips and 4exposeHostFunction()probes from context setup - Keys already on the target (builtins,
DateTime,$now,$item,$) are returned immediately with no round-trip - Results in a 17-39% improvement on expression engine benchmarks vs baseline (because setup is cheaper)
- Only file changed in the runtime:
context.ts. No bridge changes needed.
Related Linear tickets, Github issues, and Community forum posts
<!-- Internal: VM expression engine production readiness -->Review / Merge checklist
- I have seen this code, I have run this code, and I take responsibility for this code.
- PR title and summary are descriptive. (conventions)
- Docs updated or follow-up ticket created.
- Tests included.
🤖 Generated with Claude Code