{"record":{"id":"dee1a5f3dfc38afe","repo":"mastra-ai/mastra","slug":"rule-metadata-must-use-plain-objects","errorCode":null,"errorMessage":"Rule metadata must use plain objects.","messagePattern":"Rule metadata must use plain objects\\.","errorType":"validation","errorClass":"FactoryRuleValidationError","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/validation.ts","lineNumber":114,"sourceCode":"): FactoryRuleJsonValue {\n  if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n  if (typeof value === 'number') {\n    if (!Number.isFinite(value)) throw new FactoryRuleValidationError('Rule metadata must contain finite numbers.');\n    return value;\n  }\n  if (depth >= MAX_JSON_DEPTH || (typeof value !== 'object' && !Array.isArray(value))) {\n    throw new FactoryRuleValidationError('Rule metadata is not bounded JSON.');\n  }\n  if (seen.has(value as object)) throw new FactoryRuleValidationError('Rule metadata must not contain cycles.');\n  seen.add(value as object);\n  try {\n    if (Array.isArray(value)) {\n      if (value.length > MAX_JSON_COLLECTION_SIZE) {\n        throw new FactoryRuleValidationError('Rule metadata contains too many entries.');\n      }\n      return value.map(entry => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));\n    }\n    if (!isPlainObject(value)) throw new FactoryRuleValidationError('Rule metadata must use plain objects.');\n    const entries = Object.entries(value);\n    if (entries.length > MAX_JSON_COLLECTION_SIZE) {\n      throw new FactoryRuleValidationError('Rule metadata contains too many fields.');\n    }\n    const sanitized: Record<string, FactoryRuleJsonValue> = {};\n    for (const [key, entry] of entries) {\n      const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);\n      sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)\n        ? '[REDACTED]'\n        : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);\n    }\n    return sanitized;\n  } finally {\n    seen.delete(value as object);\n  }\n}\n\nfunction sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/validation.ts#L96-L132","documentation":"Rule metadata objects must be plain objects — their prototype must be Object.prototype or null. Class instances, Map/Set, Date, and other exotic objects are rejected inside metadata because they do not round-trip through JSON and their behavior cannot be persisted. Arrays are handled separately and are allowed as arrays.","triggerScenarios":"Passing new Date() as a metadata value, a class instance like new MyResult(), a Map or Set as a metadata value, or Object.create(someProto) objects in an upsertLinkedWorkItem decision's metadata.","commonSituations":"Copy-pasting domain objects with methods into metadata, timestamps as Date objects instead of ISO strings, using Map for convenience and passing it wholesale.","solutions":["Convert Date values to ISO strings: date.toISOString().","Convert Map/Set to plain objects/arrays via Object.fromEntries(map) or [...set].","Spread class instances into plain objects: { ...instance } only if fields are enumerable, otherwise map fields explicitly.","Ensure metadata contains only plain objects, arrays, strings, numbers, booleans, and null."],"exampleFix":"// before\nmetadata: { createdAt: new Date(), tags: new Set(['a','b']) }\n// after\nmetadata: { createdAt: new Date().toISOString(), tags: ['a','b'] }","handlingStrategy":"type-guard","validationCode":"const isPlain = (v: unknown): v is Record<string, unknown> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v) &&\n  (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);\nfor (const [k, v] of Object.entries(metadata)) if (!isPlain(v) && !Array.isArray(v) && typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean' && v !== null) throw new TypeError(`metadata.${k} is not JSON-safe`);","typeGuard":"const isPlainObject = (v: unknown): v is Record<string, unknown> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v) &&\n  (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);","tryCatchPattern":"try {\n  emit({ type: 'upsertLinkedWorkItem', metadata });\n} catch (e) {\n  if (e instanceof FactoryRuleValidationError && e.message.includes('plain objects')) {\n    emit({ type: 'upsertLinkedWorkItem', metadata: toPlainJson(metadata) });\n  } else throw e;\n}","preventionTips":["Serialize Dates as ISO strings, Maps via Object.fromEntries, Sets via Array.from.","Never put class instances, functions, symbols, or bigints in metadata.","Round-trip metadata through JSON.parse(JSON.stringify(...)) before emitting.","Keep a single toPlainJson() helper used by all rules that attach metadata."],"tags":["validation","json","plain-object"],"backgroundTag":"schema-validation-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}