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
- Coerce the labeled field (see message for which one) to a string, e.g. String(value) or a template literal, before building the decision.
- If the field is optional (role, arguments, precedingMessage), either omit the key entirely or pass a real string; undefined only works for optional fields.
- If the value comes from external input, validate/normalize it upstream (JSON.parse with a schema like zod) before calling the factory APIs.
- 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
- Author decisions in TypeScript with the FactoryRuleDecision type so wrong types fail at compile time.
- Never build decision objects from untyped JSON without a schema pass (zod/valibot).
- Coerce ids and timestamps with String() at construction time.
- Omit optional keys entirely instead of passing undefined/null.
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
- ${label} must be an object.
- ${label}.${stage} must be an object.
- ${label}.${stage}.${source} handlers must be functions.
- Factory rules.tools.${toolName}.onResult must be a function.
- Factory skill cancelInFlight must be a boolean.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/4b9c75e616fda66e.
Report an issue: GitHub.