{"record":{"id":"3c994b64470cd755","repo":"Automattic/mongoose","slug":"invalid-update-pipeline-operator-op","errorCode":null,"errorMessage":"Invalid update pipeline operator: \"${op}\"","messagePattern":"Invalid update pipeline operator: \"(.+?)\"","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/helpers/query/castUpdate.js","lineNumber":202,"sourceCode":"  if (op === '$project') {\n    if (val == null || typeof val !== 'object') {\n      throw new MongooseError('Invalid $project in pipeline, must be an object');\n    }\n    return val;\n  }\n  if (op === '$addFields' || op === '$set') {\n    if (val == null || typeof val !== 'object') {\n      throw new MongooseError('Invalid ' + op + ' in pipeline, must be an object');\n    }\n    return val;\n  } else if (op === '$replaceRoot' || op === '$replaceWith') {\n    if (val == null || typeof val !== 'object') {\n      throw new MongooseError('Invalid ' + op + ' in pipeline, must be an object');\n    }\n    return val;\n  }\n\n  throw new MongooseError('Invalid update pipeline operator: \"' + op + '\"');\n}\n\n/**\n * Walk each path of obj and cast its values\n * according to its schema.\n *\n * @param {Schema} schema\n * @param {object} obj part of a query\n * @param {string} op the atomic operator ($pull, $set, etc)\n * @param {object} [options]\n * @param {boolean|'throw'} [options.strict]\n * @param {Query} context\n * @param {object} filter\n * @param {string} pref path prefix (internal only)\n * @return {Bool} true if this path has keys to update\n * @api private\n */\n","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/helpers/query/castUpdate.js#L184-L220","documentation":"Mongoose whitelists pipeline stages in update pipelines: only $unset, $project, $addFields, $set, $replaceRoot and $replaceWith are allowed. Any other top-level key in any stage of the array throws MongooseError 'Invalid update pipeline operator: \"<op>\"'. This is stricter than the MongoDB server, which also permits stages like $match and $addFields variants in updates — Mongoose rejects them client-side.","triggerScenarios":"Model.updateOne({}, [{ $match: { status: 'active' } }, { $set: { archived: true } }]); [{ $group: {...} }]; any stage key outside the allowlist, including typos like '$Set' or '$unsetField'.","commonSituations":"Pasting an aggregation pipeline into updateOne expecting it to work; trying to filter with $match inside an update pipeline instead of the query filter; assuming all aggregation stages are supported in update pipelines because MongoDB accepts some Mongoose does not.","solutions":["Move filtering into the update's first argument (the filter), keep only allowed stages in the array","Use Model.aggregate() when you need $group/$match/other stages — then merge/write with $merge or $out if needed","Check each stage key against the allowlist ($unset, $project, $addFields, $set, $replaceRoot, $replaceWith) before sending"],"exampleFix":"// before\nUser.updateMany({}, [{ $match: { status: 'active' } }, { $set: { archived: true } }]);\n\n// after: filter goes in the query, not the pipeline\nUser.updateMany({ status: 'active' }, [{ $set: { archived: true } }]);","handlingStrategy":"validation","validationCode":"const ALLOWED = new Set(['$unset','$project','$addFields','$set','$replaceRoot','$replaceWith']);\nfor (const stage of pipeline) {\n  for (const op of Object.keys(stage)) {\n    if (!ALLOWED.has(op)) throw new Error(`${op} not allowed in a Mongoose update pipeline`);\n  }\n}","typeGuard":"const UPDATE_PIPELINE_STAGES = ['$unset','$project','$addFields','$set','$replaceRoot','$replaceWith'] as const;\ntype UpdatePipelineStage = { [K in typeof UPDATE_PIPELINE_STAGES[number]]?: Record<string, unknown> };","tryCatchPattern":"try { await Model.updateOne(f, pipeline); } catch (err) { if (/Invalid update pipeline operator/.test(err.message)) { /* move $match-style logic into the filter, use aggregate() for the rest */ } throw err; }","preventionTips":["Put filter conditions in the query, not as $match stages","Use Model.aggregate() for $group/$match and write results via $merge","Validate stage names against the Mongoose allowlist before sending"],"tags":["mongoose","update","pipeline","unsupported-operator","aggregation"],"backgroundTag":"mongoose-update-pipeline","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}