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

  1. Pass scorers as the static array form (fixed list of scorer ids/refs) so they round-trip.
  2. If selection must be dynamic, apply scorers at run time in the host application rather than in the persisted step options.
  3. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/b25664162c713e51. Report an issue: GitHub.