{"record":{"id":"4377f50b35e78587","repo":"mastra-ai/mastra","slug":"cannot-rehydrate-loop-step-missing-declarative-pr","errorCode":null,"errorMessage":"Cannot rehydrate loop step: missing declarative predicate or loopType. Only declarative predicate loops round-trip.","messagePattern":"Cannot rehydrate loop step: missing declarative predicate or loopType\\. Only declarative predicate loops round-trip\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/workflows/dynamic/rehydrate.ts","lineNumber":183,"sourceCode":"      // Wire graphs may omit the Studio-facing condition labels; derive them\n      // from the predicates (same convention as the fluent builder).\n      const serializedConditions =\n        entry.serializedConditions ??\n        steps.map((s, i) => ({ id: `${getSingleStepEntryId(s)}-condition`, fn: derivePredicateLabel(predicates[i]!) }));\n      const live: StepFlowEntry = {\n        type: 'conditional',\n        steps,\n        conditions: predicates.map(p => predicateToCondition(p!)),\n        serializedConditions,\n        predicates,\n      };\n      wf.__pushStepFlowEntry(live, { ...entry, serializedConditions });\n      return;\n    }\n    case 'loop': {\n      const { predicate, loopType } = entry;\n      if (!predicate || (loopType !== 'dowhile' && loopType !== 'dountil')) {\n        throw new Error(\n          `Cannot rehydrate loop step: missing declarative predicate or loopType. Only declarative predicate loops round-trip.`,\n        );\n      }\n      const step = rehydrateSingleEntry(entry.step, mastra, schemaOpts);\n      const serializedCondition = entry.serializedCondition ?? {\n        id: `${getSingleStepEntryId(step)}-condition`,\n        fn: derivePredicateLabel(predicate),\n      };\n      const live: StepFlowEntry = {\n        type: 'loop',\n        step,\n        condition: predicateToCondition(predicate),\n        loopType,\n        serializedCondition,\n        predicate,\n      };\n      wf.__pushStepFlowEntry(live, { ...entry, serializedCondition });\n      return;","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/workflows/dynamic/rehydrate.ts#L165-L201","documentation":"A stored 'loop' entry can only be rehydrated if it has a declarative (serializable) `predicate` and an explicit `loopType` of 'dowhile' or 'dountil'. Loops whose predicate is a runtime function cannot be serialized, so the library refuses to reconstruct them rather than guessing loop semantics and silently changing behavior.","triggerScenarios":"rehydrateWorkflow encounters a stored graph entry with type 'loop' where entry.predicate is falsy, or entry.loopType is anything other than 'dowhile'/'dountil' (e.g. undefined because the original loop used a function predicate that failed to serialize).","commonSituations":"Persisting workflows built with `.dowhile(...)`/`.dountil(...)` using inline function predicates, then rehydrating them on another server or after restart; older stored graphs saved before declarative loop predicates were supported; hand-edited stored JSON dropping the loopType field.","solutions":["Rebuild the loop with the declarative predicate API (serializable condition object) and re-serialize the workflow.","Verify the stored loop entry contains both `predicate` and `loopType: 'dowhile' | 'dountil'`.","Re-export stored graphs with the current serializer if they were produced by an older version.","Keep function-predicate loops as code-defined workflows instead of stored/rehydrated dynamic workflows."],"exampleFix":"// stored entry (broken)\n{ type: 'loop', step: loopStep } // no predicate/loopType\n\n// fixed stored entry\n{ type: 'loop', step: loopStep, loopType: 'dowhile', predicate: { id: 'check', ... } }","handlingStrategy":"validation","validationCode":"function hasRoundTrippableLoop(entry) {\n  return entry.type !== 'loop' ||\n    (Boolean(entry.predicate) && (entry.loopType === 'dowhile' || entry.loopType === 'dountil'));\n}","typeGuard":"function isRehydratableLoop(e: unknown): e is { type: 'loop'; predicate: unknown; loopType: 'dowhile' | 'dountil'; step: unknown } {\n  const x = e as any;\n  return !!x && x.type === 'loop' && !!x.predicate && (x.loopType === 'dowhile' || x.loopType === 'dountil');\n}","tryCatchPattern":"try {\n  const wf = rehydrateWorkflow(stored, mastra);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('Cannot rehydrate loop step')) {\n    // rebuild loop with declarative predicate from source\n  }\n  throw err;\n}","preventionTips":["Use declarative loop predicates (serializable condition objects) with .dowhile()/.dountil().","Never hand-edit stored loop entries; regenerate them via serialization.","Test store -> rehydrate round-trips in CI to catch non-declarative predicates early."],"tags":["workflow","serialization","loop","rehydration"],"backgroundTag":"workflow-rehydration-unsupported-predicate","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}