{"record":{"id":"b25664162c713e51","repo":"mastra-ai/mastra","slug":"kind-agent-agent-tool-step-ent-b25664","errorCode":null,"errorMessage":"${kind === 'agent' ? 'Agent' : 'Tool'} step \"${entryId}\" cannot be stored: \"scorers\" is a function; only the static array form round-trips.","messagePattern":"(.+?) step \"(.+?)\" cannot be stored: \"scorers\" is a function; only the static array form round-trips\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/workflows/dynamic/serialize.ts","lineNumber":227,"sourceCode":"  // the workflow author immediately learns their step won't persist rather\n  // than discovering it in production when the callback silently no-ops.\n  const forbidden: Array<{ key: string; hint: string }> = [\n    { key: 'onFinish', hint: 'callback closure' },\n    { key: 'onChunk', hint: 'callback closure' },\n    { key: 'onError', hint: 'callback closure' },\n    { key: 'onStepFinish', hint: 'callback closure' },\n    { key: 'onAbort', hint: 'callback closure' },\n    { key: 'toolChoice', hint: 'may be a function' },\n  ];\n  for (const { key, hint } of forbidden) {\n    if (typeof options[key] === 'function') {\n      throw new Error(\n        `${kind === 'agent' ? 'Agent' : 'Tool'} step \"${entryId}\" cannot be stored: option \"${key}\" is a ${hint} that does not round-trip. Remove it or move that logic outside the persisted workflow.`,\n      );\n    }\n  }\n  if (typeof options.scorers === 'function') {\n    throw new Error(\n      `${kind === 'agent' ? 'Agent' : 'Tool'} step \"${entryId}\" cannot be stored: \"scorers\" is a function; only the static array form round-trips.`,\n    );\n  }\n\n  const out: SerializedStepOptions = {};\n  if (typeof options.retries === 'number') out.retries = options.retries;\n  if (options.metadata && typeof options.metadata === 'object') {\n    out.metadata = options.metadata as Record<string, any>;\n  }\n  return Object.keys(out).length > 0 ? out : undefined;\n}\n\n/**\n * If the agent-step options carry `structuredOutput.schema`, that schema IS\n * the step's output shape (see `createStepFromAgent`). Emit it as JSON Schema\n * so rehydration can wire the same structured output back in.\n */\nfunction extractStructuredOutputJsonSchema(options: any, entryId: string): Record<string, any> | undefined {","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/workflows/dynamic/serialize.ts#L209-L245","documentation":"The `scorers` option on an agent/tool step accepts a function form (dynamic scorer selection) or a static form. Only the static form is JSON-safe, so persistence throws when `scorers` is a function. This is a dedicated check next to the generic forbidden-option list because `scorers` deserves a clearer message.","triggerScenarios":"Persisting an agent or tool step whose options include `scorers` supplied as a function, e.g. `scorers: (ctx) => [...]` for runtime-dependent scorer selection.","commonSituations":"Authors pick scorers dynamically based on request context or model output; live runs succeed but saving the dynamic workflow fails at `toStorableGraph`.","solutions":["Pass scorers as the static array form (fixed list of scorer ids/refs) so they round-trip.","If selection must be dynamic, apply scorers at run time in the host application rather than in the persisted step options.","Pre-validate: `typeof options.scorers !== 'function'` before persisting."],"exampleFix":"// before\nagent('myAgent', { scorers: (ctx) => ctx.requestContext.get('scorers') })\n\n// after\nagent('myAgent', { scorers: ['toxicity-scorer', 'relevance-scorer'] })","handlingStrategy":"type-guard","validationCode":"for (const e of stepFlow) {\n  if ((e.type === 'agent' || e.type === 'tool') && e.options && typeof e.options.scorers === 'function') {\n    throw new Error(`${e.type} \"${e.id}\" scorers must be the static array form`);\n  }\n}","typeGuard":"const hasStaticScorers = (e: StepFlowEntry): boolean =>\n  !(e.type === 'agent' || e.type === 'tool') || !e.options || typeof (e.options as any).scorers !== 'function';","tryCatchPattern":"try {\n  storable = toStorableGraph(stepFlow);\n} catch (e) {\n  if (/\"scorers\" is a function/.test(e.message)) {\n    // replace with a static scorer list and retry\n  } else throw e;\n}","preventionTips":["Always declare scorers as a static array in persisted workflows.","Do dynamic scorer selection in the host application before/after the run.","Include `scorers` in pre-save option validation."],"tags":["workflows","serialization","closure","scorers"],"backgroundTag":"non-serializable-closure","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T08:17:16.595Z"}