mastra-ai/mastra · error · Error
${kind === 'agent' ? 'Agent' : 'Tool'} step "${entryId}" can
Error message
${kind === 'agent' ? 'Agent' : 'Tool'} step "${entryId}" cannot be stored: "scorers" is a function; only the static array form round-trips. What it means
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.
Source
Thrown at packages/core/src/workflows/dynamic/serialize.ts:227
// the workflow author immediately learns their step won't persist rather
// than discovering it in production when the callback silently no-ops.
const forbidden: Array<{ key: string; hint: string }> = [
{ key: 'onFinish', hint: 'callback closure' },
{ key: 'onChunk', hint: 'callback closure' },
{ key: 'onError', hint: 'callback closure' },
{ key: 'onStepFinish', hint: 'callback closure' },
{ key: 'onAbort', hint: 'callback closure' },
{ key: 'toolChoice', hint: 'may be a function' },
];
for (const { key, hint } of forbidden) {
if (typeof options[key] === 'function') {
throw new Error(
`${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.`,
);
}
}
if (typeof options.scorers === 'function') {
throw new Error(
`${kind === 'agent' ? 'Agent' : 'Tool'} step "${entryId}" cannot be stored: "scorers" is a function; only the static array form round-trips.`,
);
}
const out: SerializedStepOptions = {};
if (typeof options.retries === 'number') out.retries = options.retries;
if (options.metadata && typeof options.metadata === 'object') {
out.metadata = options.metadata as Record<string, any>;
}
return Object.keys(out).length > 0 ? out : undefined;
}
/**
* If the agent-step options carry `structuredOutput.schema`, that schema IS
* the step's output shape (see `createStepFromAgent`). Emit it as JSON Schema
* so rehydration can wire the same structured output back in.
*/
function extractStructuredOutputJsonSchema(options: any, entryId: string): Record<string, any> | undefined {View on GitHub (pinned to 75dd419e61)
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.
Example fix
// before
agent('myAgent', { scorers: (ctx) => ctx.requestContext.get('scorers') })
// after
agent('myAgent', { scorers: ['toxicity-scorer', 'relevance-scorer'] }) Defensive patterns
Strategy: type-guard
Validate before calling
for (const e of stepFlow) {
if ((e.type === 'agent' || e.type === 'tool') && e.options && typeof e.options.scorers === 'function') {
throw new Error(`${e.type} "${e.id}" scorers must be the static array form`);
}
} Type guard
const hasStaticScorers = (e: StepFlowEntry): boolean => !(e.type === 'agent' || e.type === 'tool') || !e.options || typeof (e.options as any).scorers !== 'function';
Try / catch
try {
storable = toStorableGraph(stepFlow);
} catch (e) {
if (/"scorers" is a function/.test(e.message)) {
// replace with a static scorer list and retry
} else throw e;
} Prevention
- 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.
When it happens
Trigger: Persisting an agent or tool step whose options include `scorers` supplied as a function, e.g. `scorers: (ctx) => [...]` for runtime-dependent scorer selection.
Common situations: Authors pick scorers dynamically based on request context or model output; live runs succeed but saving the dynamic workflow fails at `toStorableGraph`.
Related errors
- SleepUntil step "${entry.id}" cannot be stored: dynamic date
- Conditional (branch) step cannot be stored: closure predicat
- 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
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/b25664162c713e51.
Report an issue: GitHub.