mastra-ai/mastra · error

${path} must be JSON-safe.

Error message

${path} must be JSON-safe.

What it means

normalizeJsonValue() rejects anything that isn't null, string, boolean, finite number, plain object, or array. Values like undefined, functions, Symbols, class instances, Dates, or Maps hit the `typeof value !== 'object'` / non-plain-prototype checks and throw this TypeError with the offending path. The builder enforces strict JSON-safety for all input trees.

Source

Thrown at packages/core/src/workflows/builder/index.ts:133

  'parallel',
  'foreach',
  'sleep',
  'sleepUntil',
  'conditional',
  'loop',
] 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

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Convert Dates to .toISOString(), Maps/Sets to arrays/records, and class instances to plain objects (structuredClone or toObject)
  2. Filter out undefined fields before passing the object (e.g. JSON.parse(JSON.stringify()) for trusted data, or an explicit omit-undefined pass)
  3. Replace functions/Symbols/BigInts with serializable representations (string names, Number())
  4. Use the reported `path` to locate the exact offending value

Example fix

// before
builder.withParams({ createdAt: new Date(), onUpdate: () => {} });
// after
builder.withParams({ createdAt: new Date().toISOString() });
Defensive patterns

Strategy: validation

Validate before calling

const isJsonSafe = (v) => v === null || ['string','boolean','number'].includes(typeof v) || (typeof v === 'object' && !Array.isArray(v) === false || v.constructor === Object);
function deepSanitize(o) { return JSON.parse(JSON.stringify(o, (k, v) => v === undefined ? null : v)); }

Type guard

function isPlainObject(v) { return typeof v === 'object' && v !== null && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null); }

Try / catch

try {
  builder.withParams(raw);
} catch (e) {
  if (e instanceof TypeError && e.message.includes('must be JSON-safe')) {
    builder.withParams(deepSanitize(raw));
  } else throw e;
}

Prevention

When it happens

Trigger: Passing undefined, a function, a Symbol, a BigInt, or a non-plain object (Date, Map, Set, class instance) anywhere inside data handed to workflow builder APIs.

Common situations: Embedding a Date directly instead of ISO string; passing class instances from ORM results; forgetting that optional fields left as `undefined` are not JSON-safe; including callback functions in config objects.

Related errors


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