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
- Convert Date values to ISO strings: date.toISOString().
- Convert Map/Set to plain objects/arrays via Object.fromEntries(map) or [...set].
- Spread class instances into plain objects: { ...instance } only if fields are enumerable, otherwise map fields explicitly.
- 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
- Serialize Dates as ISO strings, Maps via Object.fromEntries, Sets via Array.from.
- Never put class instances, functions, symbols, or bigints in metadata.
- Round-trip metadata through JSON.parse(JSON.stringify(...)) before emitting.
- Keep a single toPlainJson() helper used by all rules that attach metadata.
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
- Rule metadata must contain finite numbers.
- Rule metadata is not bounded JSON.
- Rule metadata must not contain cycles.
- Rule metadata contains too many entries.
- Rule metadata contains too many fields.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/dee1a5f3dfc38afe.
Report an issue: GitHub.