{"record":{"id":"4b9c75e616fda66e","repo":"mastra-ai/mastra","slug":"label-must-be-a-string","errorCode":null,"errorMessage":"${label} must be a string.","messagePattern":"(.+?) must be a string\\.","errorType":"validation","errorClass":"FactoryRuleValidationError","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/validation.ts","lineNumber":72,"sourceCode":"    this.name = 'FactoryRuleValidationError';\n  }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n  const prototype = Object.getPrototypeOf(value);\n  return prototype === Object.prototype || prototype === null;\n}\n\nfunction assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {\n  const allowed = new Set(keys);\n  if (Object.keys(value).some(key => !allowed.has(key))) {\n    throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);\n  }\n}\n\nfunction boundedString(value: unknown, label: string, max: number, pattern?: RegExp): string {\n  if (typeof value !== 'string') throw new FactoryRuleValidationError(`${label} must be a string.`);\n  const normalized = value.trim();\n  if (normalized.length === 0 || normalized.length > max || (pattern && !pattern.test(normalized))) {\n    throw new FactoryRuleValidationError(`${label} is invalid.`);\n  }\n  return normalized;\n}\n\nfunction optionalBoundedString(value: unknown, label: string, max: number): string | undefined {\n  if (value === undefined) return undefined;\n  return boundedString(value, label, max);\n}\n\nfunction enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {\n  if (typeof value !== 'string' || !allowed.includes(value as T)) {\n    throw new FactoryRuleValidationError(`${label} is invalid.`);\n  }\n  return value as T;\n}","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/validation.ts#L54-L90","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst decision = { type: 'sendMessage', idempotencyKey: Date.now(), role: 'builder', message: 'hi' };\n// after\nconst decision = { type: 'sendMessage', idempotencyKey: String(Date.now()), role: 'builder', message: 'hi' };","handlingStrategy":"type-guard","validationCode":"function isNonEmptyString(v: unknown): v is string { return typeof v === 'string'; }\nif (!isNonEmptyString(decision.idempotencyKey)) throw new TypeError('idempotencyKey must be a string');","typeGuard":"const isString = (v: unknown): v is string => typeof v === 'string';","tryCatchPattern":"try {\n  validateFactoryRuleDecision(raw);\n} catch (e) {\n  if (e instanceof FactoryRuleValidationError) {\n    if (e.message.includes('must be a string')) console.error(`Fix field type: ${e.message}`);\n    throw e;\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["validation","typescript","type-mismatch"],"backgroundTag":"schema-validation-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}