mastra-ai/mastra · error

${path} must not contain cycles.

Error message

${path} must not contain cycles.

What it means

To detect self-referential structures, normalizeJsonValue() tracks every object/array it enters in a `seen` Set. Re-encountering an object already on the current traversal means a reference cycle, which JSON.stringify cannot serialize, so the builder throws this TypeError with the cycle's path.

Source

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

  '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
// nullable, so strict-provider models are forced to emit `null` for fields they

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Break cycles by removing back-pointers before passing the data (store ids/keys instead of object references)
  2. Use a safe deep-clone that throws/handles cycles, or serialize deliberately with a replacer that skips circular refs
  3. Restructure the data to a flat, reference-by-id shape suited to JSON
  4. Use the reported `path` to identify which property creates the loop

Example fix

// before
const child = { parent }; parent.child = child; // cycle
builder.withParams(parent);
// after
const child = { parentId: parent.id }; // reference by id, no cycle
builder.withParams({ ...parent, child });
Defensive patterns

Strategy: validation

Validate before calling

function assertAcyclic(obj, seen = new WeakSet(), path = 'input') {
  if (obj && typeof obj === 'object') {
    if (seen.has(obj)) throw new Error(`cycle at ${path}`);
    seen.add(obj);
    for (const [k, v] of Object.entries(obj)) assertAcyclic(v, seen, `${path}.${k}`);
  }
}
assertAcyclic(params);

Try / catch

try {
  builder.withParams(raw);
} catch (e) {
  if (e instanceof TypeError && e.message.includes('must not contain cycles')) {
    logger.error('Circular reference at', e.message);
  } else throw e;
}

Prevention

When it happens

Trigger: Passing an object graph containing circular references (obj.self = obj; parent.child.parent = parent; circular linked structures) into workflow builder input.

Common situations: Parent/child tree nodes with back-pointers; objects augmented with references to their container; deeply shared state from caches or DI containers reused as config values.

Related errors


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