mastra-ai/mastra · error · Error

Conditional (branch) step cannot be stored: closure predicat

Error message

Conditional (branch) step cannot be stored: closure predicates do not round-trip. Use the declarative form ({ predicate: {...} }) for each branch.

What it means

A `conditional` (branch) entry whose branch predicates are closure functions cannot be persisted: functions don't survive JSON storage, so the branching logic would be lost on reload. The library only round-trips the declarative predicate form (plain objects, `{ predicate: {...} }`) and throws when any predicate is missing, null, or not an object.

Source

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

      return { type: 'parallel', steps: entry.steps.map(s => serializeSingleEntry(s)) };
    case 'foreach':
      if (entry.step.type === 'mapping') {
        throw new Error(
          `Foreach step cannot iterate a mapping: mappings project data, they don't execute per item. Use an agent, tool, or plain step as the foreach body.`,
        );
      }
      return {
        type: 'foreach',
        step: serializeSingleEntry(entry.step),
        opts:
          typeof entry.opts.concurrency === 'function'
            ? { fn: entry.opts.concurrency.toString() }
            : { concurrency: entry.opts.concurrency },
      };
    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 {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Rewrite each branch predicate in the declarative form: `{ predicate: { ... } }` describing the condition as data (field/operator/value) instead of a closure.
  2. If the condition genuinely requires arbitrary code, keep the workflow in-memory only, or move the decision into a plain step whose output drives a declarative branch.
  3. Pre-validate: `entry.type === 'conditional' && entry.predicates?.every(p => p && typeof p === 'object')` before persisting.

Example fix

// before
conditional([
  { when: (data) => data.score > 0.5, step: approveStep },
  { when: (data) => data.score <= 0.5, step: rejectStep },
])

// after
conditional([
  { predicate: { path: 'score', operator: 'gt', value: 0.5 }, step: approveStep },
  { predicate: { path: 'score', operator: 'lte', value: 0.5 }, step: rejectStep },
])
Defensive patterns

Strategy: validation

Validate before calling

for (const e of stepFlow) {
  if (e.type === 'conditional' && (!e.predicates || !e.predicates.every(p => p && typeof p === 'object'))) {
    throw new Error('conditional branches need declarative { predicate: {...} } objects');
  }
}

Type guard

const hasDeclarativePredicates = (e: StepFlowEntry): e is StepFlowEntry & { type: 'conditional'; predicates: object[] } =>
  e.type === 'conditional' && Array.isArray(e.predicates) && e.predicates.every(p => !!p && typeof p === 'object');

Try / catch

try {
  storable = toStorableGraph(stepFlow);
} catch (e) {
  if (/Conditional \(branch\) step cannot be stored/.test(e.message)) {
    // convert closure predicates to declarative form and retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `toStorableGraph` on a `stepFlow` containing a `conditional` entry whose `predicates` array is undefined/empty-of-objects, or contains function (closure) predicates built with the callback form of branch conditions.

Common situations: Authors write branch conditions as inline arrow functions (natural in code), then try to save/persist the workflow. Also occurs after refactors where `serializedConditions` was populated but raw closure `predicates` were left in place.

Related errors


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