mastra-ai/mastra · error · FactoryRuleValidationError

Rule metadata must use plain objects.

Error message

Rule metadata must use plain objects.

What it means

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.

Source

Thrown at mastracode/factory/src/rules/validation.ts:114

): FactoryRuleJsonValue {
  if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;
  if (typeof value === 'number') {
    if (!Number.isFinite(value)) throw new FactoryRuleValidationError('Rule metadata must contain finite numbers.');
    return value;
  }
  if (depth >= MAX_JSON_DEPTH || (typeof value !== 'object' && !Array.isArray(value))) {
    throw new FactoryRuleValidationError('Rule metadata is not bounded JSON.');
  }
  if (seen.has(value as object)) throw new FactoryRuleValidationError('Rule metadata must not contain cycles.');
  seen.add(value as object);
  try {
    if (Array.isArray(value)) {
      if (value.length > MAX_JSON_COLLECTION_SIZE) {
        throw new FactoryRuleValidationError('Rule metadata contains too many entries.');
      }
      return value.map(entry => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
    }
    if (!isPlainObject(value)) throw new FactoryRuleValidationError('Rule metadata must use plain objects.');
    const entries = Object.entries(value);
    if (entries.length > MAX_JSON_COLLECTION_SIZE) {
      throw new FactoryRuleValidationError('Rule metadata contains too many fields.');
    }
    const sanitized: Record<string, FactoryRuleJsonValue> = {};
    for (const [key, entry] of entries) {
      const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);
      sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)
        ? '[REDACTED]'
        : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
    }
    return sanitized;
  } finally {
    seen.delete(value as object);
  }
}

function sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Convert Date values to ISO strings: date.toISOString().
  2. Convert Map/Set to plain objects/arrays via Object.fromEntries(map) or [...set].
  3. Spread class instances into plain objects: { ...instance } only if fields are enumerable, otherwise map fields explicitly.
  4. Ensure metadata contains only plain objects, arrays, strings, numbers, booleans, and null.

Example fix

// before
metadata: { createdAt: new Date(), tags: new Set(['a','b']) }
// after
metadata: { createdAt: new Date().toISOString(), tags: ['a','b'] }
Defensive patterns

Strategy: type-guard

Validate before calling

const isPlain = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null && !Array.isArray(v) &&
  (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
for (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`);

Type guard

const isPlainObject = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null && !Array.isArray(v) &&
  (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);

Try / catch

try {
  emit({ type: 'upsertLinkedWorkItem', metadata });
} catch (e) {
  if (e instanceof FactoryRuleValidationError && e.message.includes('plain objects')) {
    emit({ type: 'upsertLinkedWorkItem', metadata: toPlainJson(metadata) });
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: Copy-pasting domain objects with methods into metadata, timestamps as Date objects instead of ISO strings, using Map for convenience and passing it wholesale.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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