{"record":{"id":"178982553e9f61e5","repo":"mastra-ai/mastra","slug":"conditional-branch-step-cannot-be-stored-closur","errorCode":null,"errorMessage":"Conditional (branch) step cannot be stored: closure predicates do not round-trip. Use the declarative form ({ predicate: {...} }) for each branch.","messagePattern":"Conditional \\(branch\\) step cannot be stored: closure predicates do not round-trip\\. Use the declarative form \\((.+?) \\}\\) for each branch\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/workflows/dynamic/serialize.ts","lineNumber":75,"sourceCode":"      return { type: 'parallel', steps: entry.steps.map(s => serializeSingleEntry(s)) };\n    case 'foreach':\n      if (entry.step.type === 'mapping') {\n        throw new Error(\n          `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.`,\n        );\n      }\n      return {\n        type: 'foreach',\n        step: serializeSingleEntry(entry.step),\n        opts:\n          typeof entry.opts.concurrency === 'function'\n            ? { fn: entry.opts.concurrency.toString() }\n            : { concurrency: entry.opts.concurrency },\n      };\n    case 'conditional': {\n      const predicates = entry.predicates;\n      if (!predicates || predicates.some(p => !p || typeof p !== 'object')) {\n        throw new Error(\n          `Conditional (branch) step cannot be stored: closure predicates do not round-trip. Use the declarative form ({ predicate: {...} }) for each branch.`,\n        );\n      }\n      return {\n        type: 'conditional',\n        steps: entry.steps.map(s => serializeSingleEntry(s)),\n        serializedConditions: entry.serializedConditions,\n        predicates,\n      };\n    }\n    case 'loop': {\n      const predicate = entry.predicate;\n      if (!predicate || typeof predicate !== 'object') {\n        throw new Error(\n          `Loop step \"${getSingleStepEntryId(entry.step)}\" cannot be stored: closure predicates do not round-trip. Use the declarative form ({ predicate: {...} }).`,\n        );\n      }\n      return {","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/workflows/dynamic/serialize.ts#L57-L93","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconditional([\n  { when: (data) => data.score > 0.5, step: approveStep },\n  { when: (data) => data.score <= 0.5, step: rejectStep },\n])\n\n// after\nconditional([\n  { predicate: { path: 'score', operator: 'gt', value: 0.5 }, step: approveStep },\n  { predicate: { path: 'score', operator: 'lte', value: 0.5 }, step: rejectStep },\n])","handlingStrategy":"validation","validationCode":"for (const e of stepFlow) {\n  if (e.type === 'conditional' && (!e.predicates || !e.predicates.every(p => p && typeof p === 'object'))) {\n    throw new Error('conditional branches need declarative { predicate: {...} } objects');\n  }\n}","typeGuard":"const hasDeclarativePredicates = (e: StepFlowEntry): e is StepFlowEntry & { type: 'conditional'; predicates: object[] } =>\n  e.type === 'conditional' && Array.isArray(e.predicates) && e.predicates.every(p => !!p && typeof p === 'object');","tryCatchPattern":"try {\n  storable = toStorableGraph(stepFlow);\n} catch (e) {\n  if (/Conditional \\(branch\\) step cannot be stored/.test(e.message)) {\n    // convert closure predicates to declarative form and retry\n  } else throw e;\n}","preventionTips":["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."],"tags":["workflows","serialization","closure","conditional"],"backgroundTag":"non-serializable-closure","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}