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
- Rewrite each branch predicate in the declarative form: `{ predicate: { ... } }` describing the condition as data (field/operator/value) instead of a closure.
- 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.
- 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
- Prefer the declarative predicate form from the start of workflow authoring.
- Never pass arrow functions as branch conditions in workflows intended for persistence.
- Route complex decisions through a plain step whose output drives declarative branches.
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
- SleepUntil step "${entry.id}" cannot be stored: dynamic date
- Loop step "${getSingleStepEntryId(entry.step)}" cannot be st
- Mapping step "${entry.id}" cannot be stored: the function fo
- Mapping step "${entry.id}" key "${key}" cannot be stored: so
- ${kind === 'agent' ? 'Agent' : 'Tool'} step "${entryId}" can
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/178982553e9f61e5.
Report an issue: GitHub.