mastra-ai/mastra · error
${path} must contain only plain objects.
Error message
${path} must contain only plain objects. What it means
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.
Source
Thrown at packages/core/src/workflows/builder/index.ts:139
] as const;
export type WorkflowBuilderSupportedStepType = (typeof WORKFLOW_BUILDER_SUPPORTED_STEP_TYPES)[number];
export { WORKFLOW_BUILDER_AUTHORING_CONSTRAINTS, WORKFLOW_BUILDER_AUTHORING_PLAYBOOK } from './authoring-playbook';
function normalizeJsonValue(value: unknown, path: string, seen: Set<object>): WorkflowBuilderJsonValue {
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
if (typeof value === 'number') {
if (!Number.isFinite(value)) throw new TypeError(`${path} must contain only finite numbers.`);
return value;
}
if (typeof value !== 'object') throw new TypeError(`${path} must be JSON-safe.`);
if (seen.has(value)) throw new TypeError(`${path} must not contain cycles.`);
seen.add(value);
try {
if (Array.isArray(value)) return value.map((item, index) => normalizeJsonValue(item, `${path}.${index}`, seen));
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
throw new TypeError(`${path} must contain only plain objects.`);
}
const normalized: WorkflowBuilderJsonObject = {};
for (const [key, item] of Object.entries(value)) {
if (item !== undefined) normalized[key] = normalizeJsonValue(item, `${path}.${key}`, seen);
}
return normalized;
} finally {
seen.delete(value);
}
}
// OpenAI strict-schema compatibility makes every optional property required and
// nullable, so strict-provider models are forced to emit `null` for fields they
// would otherwise omit. Strip null at exactly the optional structural slots the
// canonical schema declares — never blanket-strip, because a mapping constant
// source `{ "value": null }` is a legitimate null.
const OPTIONAL_ENTRY_KEYS = ['description', 'outputSchema', 'options', 'opts'] as const;
const OPTIONAL_STEP_OPTION_KEYS = ['retries', 'metadata'] as const;View on GitHub (pinned to 75dd419e61)
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).
Example fix
// before
createWorkflow({ id: 'w', metadata: { createdAt: new Date() }, ... })
// after
createWorkflow({ id: 'w', metadata: { createdAt: new Date().toISOString() }, ... }) Defensive patterns
Strategy: validation
Validate before calling
function isPlainObject(v) {
return typeof v === 'object' && v !== null &&
(Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
}
function assertJsonSafe(value, path = 'value', seen = new Set()) {
if (value === null || ['string','number','boolean'].includes(typeof value)) return;
if (typeof value !== 'object') throw new TypeError(`${path} must be JSON-safe`);
if (seen.has(value)) throw new TypeError(`${path} contains a cycle`);
seen.add(value);
if (Array.isArray(value)) return value.forEach((v, i) => assertJsonSafe(v, `${path}[${i}]`, seen));
if (!isPlainObject(value)) throw new TypeError(`${path} must be a plain object`);
for (const [k, v] of Object.entries(value)) assertJsonSafe(v, `${path}.${k}`, seen);
} Type guard
const isPlainObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- @mastra/livekit: `workflowInput` is required when `workflow`
- Dynamic workflow bundle contains more than one definition wi
- Dynamic workflow bundle has a circular nested-workflow depen
- SCHEDULES_INVALID_WORKFLOW_PATCH
- Workflow definition graph must be an array.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/cef32995d990b4c3.
Report an issue: GitHub.