mastra-ai/mastra · error · Error

Loop step "${getSingleStepEntryId(entry.step)}" cannot be st

Error message

Loop step "${getSingleStepEntryId(entry.step)}" cannot be stored: closure predicates do not round-trip. Use the declarative form ({ predicate: {...} }).

What it means

A `loop` entry's `predicate` (the while/until condition controlling iteration) must be a declarative object to be storable. Closure predicates cannot be serialized, so `toStorableGraph` throws when the predicate is missing, null, or a function. The error names the loop's inner step id so the author can locate the offending loop.

Source

Thrown at packages/core/src/workflows/dynamic/serialize.ts:89

      };
    case 'conditional': {
      const predicates = entry.predicates;
      if (!predicates || predicates.some(p => !p || typeof p !== 'object')) {
        throw new Error(
          `Conditional (branch) step cannot be stored: closure predicates do not round-trip. Use the declarative form ({ predicate: {...} }) for each branch.`,
        );
      }
      return {
        type: 'conditional',
        steps: entry.steps.map(s => serializeSingleEntry(s)),
        serializedConditions: entry.serializedConditions,
        predicates,
      };
    }
    case 'loop': {
      const predicate = entry.predicate;
      if (!predicate || typeof predicate !== 'object') {
        throw new Error(
          `Loop step "${getSingleStepEntryId(entry.step)}" cannot be stored: closure predicates do not round-trip. Use the declarative form ({ predicate: {...} }).`,
        );
      }
      return {
        type: 'loop',
        step: serializeSingleEntry(entry.step),
        serializedCondition: entry.serializedCondition,
        loopType: entry.loopType,
        predicate,
      };
    }
    default: {
      const _exhaustive: never = entry;
      throw new Error(`Unknown step entry type: ${JSON.stringify(_exhaustive)}`);
    }
  }
}

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Convert the loop predicate to the declarative form `{ predicate: {...} }` (field/operator/value data) instead of a closure.
  2. If the condition needs arbitrary computation, move it into a plain step inside the loop that sets a flag, and loop on a declarative predicate over that flag.
  3. Keep the loop workflow unpersisted if a closure is truly required.
  4. Pre-validate: `entry.type === 'loop' && entry.predicate && typeof entry.predicate === 'object'`.

Example fix

// before
loop(processStep, (data) => data.done !== true)

// after
loop(processStep, { predicate: { path: 'done', operator: 'neq', value: true } })
Defensive patterns

Strategy: validation

Validate before calling

for (const e of stepFlow) {
  if (e.type === 'loop' && (!e.predicate || typeof e.predicate !== 'object')) {
    throw new Error(`loop "${e.step?.id}" needs a declarative { predicate: {...} }`);
  }
}

Type guard

const hasDeclarativeLoopPredicate = (e: StepFlowEntry): e is StepFlowEntry & { type: 'loop'; predicate: object } =>
  e.type === 'loop' && !!e.predicate && typeof e.predicate === 'object';

Try / catch

try {
  storable = toStorableGraph(stepFlow);
} catch (e) {
  if (/Loop step .* cannot be stored/.test(e.message)) {
    // replace the closure predicate with a declarative one and retry
  } else throw e;
}

Prevention

When it happens

Trigger: Persisting a `stepFlow` containing a `loop` entry built with a function predicate (e.g. `.while((data) => data.attempts < 5)`) or with no predicate at all.

Common situations: Authors write loop exit conditions as arrow functions against loop data, which works at runtime but fails when the workflow is saved via the dynamic workflow persistence path (e.g. Studio-saved or API-stored workflows).

Related errors


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