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

  1. Replace non-plain values with JSON-safe equivalents (ISO string for Date, Array for Set/Map, plain object literal).
  2. Convert class instances with {...instance} spread or a toPlainObject/toJSON call before building the definition.
  3. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/cef32995d990b4c3. Report an issue: GitHub.