{"record":{"id":"cef32995d990b4c3","repo":"mastra-ai/mastra","slug":"path-must-contain-only-plain-objects","errorCode":null,"errorMessage":"${path} must contain only plain objects.","messagePattern":"(.+?) must contain only plain objects\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/workflows/builder/index.ts","lineNumber":139,"sourceCode":"] as const;\n\nexport type WorkflowBuilderSupportedStepType = (typeof WORKFLOW_BUILDER_SUPPORTED_STEP_TYPES)[number];\n\nexport { WORKFLOW_BUILDER_AUTHORING_CONSTRAINTS, WORKFLOW_BUILDER_AUTHORING_PLAYBOOK } from './authoring-playbook';\n\nfunction normalizeJsonValue(value: unknown, path: string, seen: Set<object>): WorkflowBuilderJsonValue {\n  if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;\n  if (typeof value === 'number') {\n    if (!Number.isFinite(value)) throw new TypeError(`${path} must contain only finite numbers.`);\n    return value;\n  }\n  if (typeof value !== 'object') throw new TypeError(`${path} must be JSON-safe.`);\n  if (seen.has(value)) throw new TypeError(`${path} must not contain cycles.`);\n  seen.add(value);\n  try {\n    if (Array.isArray(value)) return value.map((item, index) => normalizeJsonValue(item, `${path}.${index}`, seen));\n    if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {\n      throw new TypeError(`${path} must contain only plain objects.`);\n    }\n    const normalized: WorkflowBuilderJsonObject = {};\n    for (const [key, item] of Object.entries(value)) {\n      if (item !== undefined) normalized[key] = normalizeJsonValue(item, `${path}.${key}`, seen);\n    }\n    return normalized;\n  } finally {\n    seen.delete(value);\n  }\n}\n\n// OpenAI strict-schema compatibility makes every optional property required and\n// nullable, so strict-provider models are forced to emit `null` for fields they\n// would otherwise omit. Strip null at exactly the optional structural slots the\n// canonical schema declares — never blanket-strip, because a mapping constant\n// source `{ \"value\": null }` is a legitimate null.\nconst OPTIONAL_ENTRY_KEYS = ['description', 'outputSchema', 'options', 'opts'] as const;\nconst OPTIONAL_STEP_OPTION_KEYS = ['retries', 'metadata'] as const;","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/workflows/builder/index.ts#L121-L157","documentation":"normalizeJsonValue recursively validates that a workflow builder definition is JSON-safe. It throws this error when it encounters an object whose prototype is not Object.prototype (or null) — i.e. class instances, Dates, Maps, etc. nested in the definition. The library requires plain serializable data so definitions can be stored and rehydrated.","triggerScenarios":"Passing a workflow definition object containing non-plain values — e.g. `new Date()` in metadata, a Map/Set, a class instance (like a Zod default object with methods), or a value created with Object.create(someProto) — into createWorkflow/normalizeWorkflowBuilderDefinition (directly or via graph entries, description, metadata, stateSchema, requestContextSchema).","commonSituations":"Putting runtime objects (Date, RegExp, class instances from other libraries) into workflow metadata or step configs; copying config objects that were constructed by a framework with a custom prototype; deserializing data with a revival library that returns class instances.","solutions":["Replace non-plain values with JSON-safe equivalents (ISO string for Date, Array for Set/Map, plain object literal).","Convert class instances with {...instance} spread or a toPlainObject/toJSON call before building the definition.","If the offending value should not be serialized at all, move it out of the definition (e.g. register the agent/tool on Mastra instead of embedding the instance)."],"exampleFix":"// before\ncreateWorkflow({ id: 'w', metadata: { createdAt: new Date() }, ... })\n// after\ncreateWorkflow({ id: 'w', metadata: { createdAt: new Date().toISOString() }, ... })","handlingStrategy":"validation","validationCode":"function isPlainObject(v) {\n  return typeof v === 'object' && v !== null &&\n    (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);\n}\nfunction assertJsonSafe(value, path = 'value', seen = new Set()) {\n  if (value === null || ['string','number','boolean'].includes(typeof value)) return;\n  if (typeof value !== 'object') throw new TypeError(`${path} must be JSON-safe`);\n  if (seen.has(value)) throw new TypeError(`${path} contains a cycle`);\n  seen.add(value);\n  if (Array.isArray(value)) return value.forEach((v, i) => assertJsonSafe(v, `${path}[${i}]`, seen));\n  if (!isPlainObject(value)) throw new TypeError(`${path} must be a plain object`);\n  for (const [k, v] of Object.entries(value)) assertJsonSafe(v, `${path}.${k}`, seen);\n}","typeGuard":"const isPlainObject = (v: unknown): v is Record<string, unknown> =>\n  typeof v === 'object' && v !== null &&\n  (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);","tryCatchPattern":null,"preventionTips":["Use only JSON primitives, arrays, and object literals in definitions/metadata.","Convert Dates to ISO strings before embedding them.","JSON.parse(JSON.stringify(value)) as a sanitizer before building definitions.","Keep runtime instances (agents, tools, models) out of serialized definitions."],"tags":["validation","json-serialization","workflow"],"backgroundTag":"non-plain-object-in-json","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}