mastra-ai/mastra · error · FactoryRuleValidationError

${label} must be a string.

Error message

${label} must be a string.

What it means

Factory rule validation requires every string field (idempotencyKey, role, title, reason, message, tool names, etc.) to actually be a string at runtime. boundedString is the shared gate used by decision validators and assertFactoryRules; if the value passed for a labeled field is not typeof 'string' (e.g. null, number, undefined for a required field), it throws FactoryRuleValidationError with code 'invalid_factory_rule'. This guards against non-string values coming from JSON payloads, env vars, or dynamic rule code.

Source

Thrown at mastracode/factory/src/rules/validation.ts:72

    this.name = 'FactoryRuleValidationError';
  }
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
  const allowed = new Set(keys);
  if (Object.keys(value).some(key => !allowed.has(key))) {
    throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
  }
}

function boundedString(value: unknown, label: string, max: number, pattern?: RegExp): string {
  if (typeof value !== 'string') throw new FactoryRuleValidationError(`${label} must be a string.`);
  const normalized = value.trim();
  if (normalized.length === 0 || normalized.length > max || (pattern && !pattern.test(normalized))) {
    throw new FactoryRuleValidationError(`${label} is invalid.`);
  }
  return normalized;
}

function optionalBoundedString(value: unknown, label: string, max: number): string | undefined {
  if (value === undefined) return undefined;
  return boundedString(value, label, max);
}

function enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {
  if (typeof value !== 'string' || !allowed.includes(value as T)) {
    throw new FactoryRuleValidationError(`${label} is invalid.`);
  }
  return value as T;
}

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Coerce the labeled field (see message for which one) to a string, e.g. String(value) or a template literal, before building the decision.
  2. If the field is optional (role, arguments, precedingMessage), either omit the key entirely or pass a real string; undefined only works for optional fields.
  3. If the value comes from external input, validate/normalize it upstream (JSON.parse with a schema like zod) before calling the factory APIs.
  4. Run validateFactoryRuleDecision in a try/catch during development to surface exactly which label failed.

Example fix

// before
const decision = { type: 'sendMessage', idempotencyKey: Date.now(), role: 'builder', message: 'hi' };
// after
const decision = { type: 'sendMessage', idempotencyKey: String(Date.now()), role: 'builder', message: 'hi' };
Defensive patterns

Strategy: type-guard

Validate before calling

function isNonEmptyString(v: unknown): v is string { return typeof v === 'string'; }
if (!isNonEmptyString(decision.idempotencyKey)) throw new TypeError('idempotencyKey must be a string');

Type guard

const isString = (v: unknown): v is string => typeof v === 'string';

Try / catch

try {
  validateFactoryRuleDecision(raw);
} catch (e) {
  if (e instanceof FactoryRuleValidationError) {
    if (e.message.includes('must be a string')) console.error(`Fix field type: ${e.message}`);
    throw e;
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a non-string to a validated field: idempotencyKey as a number, role undefined in a sendMessage decision, title as null in an upsertLinkedWorkItem decision, rules.version as a number, or a metadata key path via boundedString(key,...) where key is never non-string. Most common: calling validateFactoryRuleDecision with a decision object whose required string field is omitted or typed wrong.

Common situations: Constructing decisions in JavaScript without TypeScript checks, deserializing decisions from JSON where empty values became null, reading config from env or YAML where numbers/undefined appear where strings are expected, and rules authored by LLM/tool output with wrong field types.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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