mastra-ai/mastra · error · FactoryRuleValidationError
Rule metadata must not contain cycles.
Error message
Rule metadata must not contain cycles.
What it means
Cyclic object graphs cannot be JSON-serialized, so normalizeFactoryRuleJsonValue tracks visited containers (with a seen set that allows shared non-cyclic references via backtracking in the finally block) and throws if the same object appears twice along one path. This prevents infinite recursion and invalid metadata.
Source
Thrown at mastracode/factory/src/rules/validation.ts:105
throw new FactoryRuleValidationError(`${label} is invalid.`);
}
return value as T;
}
export function normalizeFactoryRuleJsonValue(
value: unknown,
depth = 0,
seen = new Set<object>(),
): 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]'View on GitHub (pinned to 75dd419e61)
Solutions
- Break cycles: remove back-references (e.g. delete node.parent) before building metadata.
- Construct metadata as fresh plain-object literals rather than spreading existing graph structures.
- Serialize with a cycle-safe replacer, or copy only primitive leaves into metadata.
- Run a quick cycle check before calling the API (e.g. try JSON.stringify(value) and catch the TypeError).
Example fix
// before
metadata: { item, item: { parent: item } }
// after
const { parent, ...safe } = item;
metadata: { item: safe } Defensive patterns
Strategy: validation
Validate before calling
const hasCycle = (root: unknown): boolean => {
const seen = new Set<object>(); const visit = (v: unknown): boolean => {
if (!v || typeof v !== 'object') return false;
if (seen.has(v)) return true;
seen.add(v);
return Object.values(v).some(visit);
};
return visit(root);
};
if (hasCycle(metadata)) throw new TypeError('metadata contains a cycle'); Type guard
const isAcyclic = (v: unknown, seen = new Set<object>()): boolean => {
if (!v || typeof v !== 'object') return true;
if (seen.has(v)) return false;
seen.add(v);
return Object.values(v).every(x => isAcyclic(x, seen));
}; Try / catch
try {
JSON.stringify(metadata); // throws TypeError on cycles too
emit({ type: 'upsertLinkedWorkItem', metadata });
} catch (e) {
if (e instanceof TypeError && /circular|cyclic/i.test(e.message)) {
emit({ type: 'upsertLinkedWorkItem', metadata: JSON.parse(JSON.stringify(metadata, cycleReplacer())) });
} else throw e;
} Prevention
- Never spread objects that carry parent/back-pointers into metadata.
- Build metadata as fresh object literals from primitives.
- Dry-run with JSON.stringify(metadata) in tests — it throws on cycles.
- Strip known back-reference fields (parent, container, owner) before attaching objects.
When it happens
Trigger: Metadata containing an object that references itself (obj.self = obj) or a mutual cycle (a.child = b; b.parent = a), commonly from graph-like structures, linked lists with back-pointers, or error objects with circular cause chains spread into metadata.
Common situations: Spreading objects with parent/back references (DOM-like trees, AST nodes with parent pointers), reusing a logger or context object that closes over its container, merging configs where a value ends up referencing an ancestor.
Related errors
- Rule metadata must contain finite numbers.
- Rule metadata is not bounded JSON.
- Rule metadata contains too many entries.
- Rule metadata must use plain objects.
- Rule metadata contains too many fields.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/c84e187e94ecb406.
Report an issue: GitHub.