Bug Description
A value in workflow data whose meaning lives on its prototype loses it crossing into the expression runtime, so an expression that relied on toString gets [object Object] and nothing is raised.
The case that surfaced it is a MongoDB _id: {{ $json._id.toString() }} returned the hex before the isolate expression runtime and returns [object Object] after. @chung1912 reported it from a live 2.36.8 instance in #36473 (comment). It is a different root cause from the key-shadowing bug #36473 tracks, and fixing that one will not fix this, so it needs its own issue.
Only Object.keys crosses the boundary. In packages/@n8n/expression-runtime/src/bridge/isolated-vm-bridge.ts a non-Date object is marshaled as:
// Dates have no enumerable own keys; pass through instead of
// marshaling as an empty object.
if (value instanceof Date) {
return value;
}
if (value !== null && typeof value === 'object') {
return { __isObject: true, __keys: Object.keys(value) };
}
For a driver-returned ObjectId, Object.keys() is ["buffer"], so the prototype — and the toString that holds the hex — is dropped and Object.prototype.toString answers instead. Date was given a pass-through for exactly this reason, and src/__tests__/integration.test.ts already has a "Date marshaling from workflow data" block from an earlier round of the same problem. This is that problem for the classes the runtime cannot know by name.
Both marshaling sites are affected: getValueAtPath and getArrayElement.
Not limited to ObjectId — Decimal128 loses toString the same way, so {{ $json.price.toString() }} gives [object Object] instead of 12.345.
For the "is this even supported data" question: GenericValue in packages/workflow/src/Interfaces.ts is string | object | number | boolean | undefined | null, so a class instance in item.json is within the declared type.
To Reproduce
Round-tripped through a real MongoDB 7 and the real driver (mongodb 6.11.0, the version nodes-base resolves), so this is a driver-returned ObjectId rather than a hand-made stand-in:
const col = client.db('e2e').collection('docs');
await col.insertOne({ name: 'invoice', amount: 42 });
const doc = await col.findOne({ name: 'invoice' }); // what find() hands downstream
// doc._id is an ObjectId; Object.keys(doc._id) === ["buffer"]
evaluator.evaluate('{{ $json._id.toString() }}', { $json: doc }, caller);
On master at 021cdf26:
driver returned _id -> ObjectId 6a9568ffce3ca127e583b1bb
{{ $json._id.toString() }} -> "[object Object]" ← expected the hex
{{ "id=" + $json._id }} -> "id=[object Object]"
$json.row._id.toString() -> "[object Object]" (nested)
$json.ids[0].toString() -> "[object Object]" (inside an array)
{{ $json.price.toString() }} -> "[object Object]" (Decimal128, expected "12.345")
{{ $json.name }} -> "invoice" (sibling fields fine)
{{ $json.d.toISOString() }} -> "2026-06-30T20:34:04.498Z" (Date, special-cased, fine)
Expected behavior
{{ $json._id.toString() }} gives the hex, as it did before the isolate runtime, and "id=" + $json._id concatenates it.
Only values whose toString is genuinely overridden should change. A plain object must keep returning [object Object], and an array must keep returning its comma-joined form.
Debug Info
The live-instance report is @chung1912's on 2.36.8, linked above. The reproduction here is against master at 021cdf26 (repo version 2.37.0) driving the real IsolatedVmBridge with a document fetched from a real MongoDB, so it is not from an n8n UI session and the usual Debug Info panel does not apply.
Operating System
macOS 26.6.2 (MongoDB 7 in Docker)
n8n Version
2.37.0 (master at 021cdf26); reported live on 2.36.8, and @chung1912 notes it worked before 2.34.x
Node.js Version
24.19.0
Database
n/a for n8n itself; MongoDB 7 as the data source
Two opposite defects, easy to conflate:
A fix for one does not fix the other — verified, not assumed: with the fix for this issue applied and the key-shadowing fix absent, a data key named toString still resolves to the prototype method.
#37422 fixes this one and keeps the two independent.
Bug Description
A value in workflow data whose meaning lives on its prototype loses it crossing into the expression runtime, so an expression that relied on
toStringgets[object Object]and nothing is raised.The case that surfaced it is a MongoDB
_id:{{ $json._id.toString() }}returned the hex before the isolate expression runtime and returns[object Object]after. @chung1912 reported it from a live 2.36.8 instance in #36473 (comment). It is a different root cause from the key-shadowing bug #36473 tracks, and fixing that one will not fix this, so it needs its own issue.Only
Object.keyscrosses the boundary. Inpackages/@n8n/expression-runtime/src/bridge/isolated-vm-bridge.tsa non-Dateobject is marshaled as:For a driver-returned
ObjectId,Object.keys()is["buffer"], so the prototype — and thetoStringthat holds the hex — is dropped andObject.prototype.toStringanswers instead.Datewas given a pass-through for exactly this reason, andsrc/__tests__/integration.test.tsalready has a "Date marshaling from workflow data" block from an earlier round of the same problem. This is that problem for the classes the runtime cannot know by name.Both marshaling sites are affected:
getValueAtPathandgetArrayElement.Not limited to
ObjectId—Decimal128losestoStringthe same way, so{{ $json.price.toString() }}gives[object Object]instead of12.345.For the "is this even supported data" question:
GenericValueinpackages/workflow/src/Interfaces.tsisstring | object | number | boolean | undefined | null, so a class instance initem.jsonis within the declared type.To Reproduce
Round-tripped through a real MongoDB 7 and the real driver (
mongodb6.11.0, the versionnodes-baseresolves), so this is a driver-returnedObjectIdrather than a hand-made stand-in:On
masterat021cdf26:Expected behavior
{{ $json._id.toString() }}gives the hex, as it did before the isolate runtime, and"id=" + $json._idconcatenates it.Only values whose
toStringis genuinely overridden should change. A plain object must keep returning[object Object], and an array must keep returning its comma-joined form.Debug Info
The live-instance report is @chung1912's on 2.36.8, linked above. The reproduction here is against
masterat021cdf26(repo version 2.37.0) driving the realIsolatedVmBridgewith a document fetched from a real MongoDB, so it is not from an n8n UI session and the usual Debug Info panel does not apply.Operating System
macOS 26.6.2 (MongoDB 7 in Docker)
n8n Version
2.37.0 (
masterat021cdf26); reported live on 2.36.8, and @chung1912 notes it worked before 2.34.xNode.js Version
24.19.0
Database
n/a for n8n itself; MongoDB 7 as the data source
Relation to #36473
Two opposite defects, easy to conflate:
toStringcannot be evaluated correctly in expressions / IF node #36473 (and Item fields named toString/valueOf/hasOwnProperty resolve to the prototype method on the vm engine #37358): a data key literally namedtoStringresolves to the prototype method instead of your data.toStringis what you want, and it is gone.A fix for one does not fix the other — verified, not assumed: with the fix for this issue applied and the key-shadowing fix absent, a data key named
toStringstill resolves to the prototype method.#37422 fixes this one and keeps the two independent.