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

  1. Break cycles: remove back-references (e.g. delete node.parent) before building metadata.
  2. Construct metadata as fresh plain-object literals rather than spreading existing graph structures.
  3. Serialize with a cycle-safe replacer, or copy only primitive leaves into metadata.
  4. 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

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


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