mastra-ai/mastra · error · FactoryRuleValidationError
${label}.${stage}.${source} must be an object.
Error message
${label}.${stage}.${source} must be an object. What it means
Each stage+source leaf in a board must be a plain object holding at most onEnter/onExit handlers. This error fires when the leaf value for a recognized source key is not a plain object.
Source
Thrown at mastracode/factory/src/rules/validation.ts:149
function sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {
if (value === undefined) return undefined;
const sanitized = normalizeFactoryRuleJsonValue(value);
if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');
if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
throw new FactoryRuleValidationError('Rule metadata is too large.');
}
return sanitized;
}
function validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {
if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);
for (const [stage, sources] of Object.entries(rules)) {
enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);
if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);
for (const [source, leaf] of Object.entries(sources)) {
enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);
if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);
assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);
for (const handler of Object.values(leaf)) {
if (handler !== undefined && typeof handler !== 'function') {
throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);
}
}
}
}
}
export function assertFactoryRules(rules: unknown): asserts rules is FactoryRules {
if (!isPlainObject(rules)) throw new FactoryRuleValidationError('Factory rules must be an object.');
assertExactKeys(rules, ['version', 'work', 'review', 'tools', 'github', 'linear'], 'Factory rules');
boundedString(rules.version, 'Factory rule version', MAX_VERSION_LENGTH);
validateBoardRules(rules.work, 'Factory rules.work');
validateBoardRules(rules.review, 'Factory rules.review');
if (!isPlainObject(rules.tools)) throw new FactoryRuleValidationError('Factory rules.tools must be an object.');View on GitHub (pinned to 75dd419e61)
Solutions
- Wrap the callback in a leaf object: { onEnter: handler } or { onExit: handler }.
- Replace null placeholders with {} or omit the source entirely.
- If multiple handlers were merged, ensure the merged value is still an object with onEnter/onExit keys.
Example fix
// before
work: { plan: { 'github-pr': onPlanEnter } }
// after
work: { plan: { 'github-pr': { onEnter: onPlanEnter } } } Defensive patterns
Strategy: type-guard
Validate before calling
function isRuleLeaf(v) {
return v !== null && typeof v === 'object' && !Array.isArray(v) &&
Object.keys(v).every(k => k === 'onEnter' || k === 'onExit');
}
for (const [stage, sources] of Object.entries(rules.work ?? {})) {
for (const [source, leaf] of Object.entries(sources ?? {})) {
if (!isRuleLeaf(leaf)) throw new Error(`work.${stage}.${source} must be { onEnter?, onExit? }`);
}
} Type guard
const isRuleLeaf = (v: unknown): v is { onEnter?: FactoryRuleHandler; onExit?: FactoryRuleHandler } =>
typeof v === 'object' && v !== null && !Array.isArray(v); Try / catch
try {
assertFactoryRules(rules);
} catch (e) {
if (e.name === 'FactoryRuleValidationError') {
const path = e.message.match(/^(\S+) must be an object/)?.[1];
console.error(`Malformed rule leaf at ${path}; expected { onEnter?, onExit? }`);
} else throw e;
} Prevention
- Always wrap callbacks in { onEnter: fn } / { onExit: fn } leaf objects.
- Type leaves with the FactoryBoardRules type; avoid unknown/any when assembling rules.
- Reject null leaf placeholders in your config loader (map to {}).
When it happens
Trigger: rules.work.<stage>.<source> (e.g. rules.work.plan['github-pr']) is a function, null, array, or primitive instead of { onEnter?, onExit? }, during assertFactoryRules.
Common situations: Assigning a handler function directly to the source instead of nesting it under onEnter/onExit; null placeholder values; accidental overwriting of a leaf object with a single callback during merges.
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}.${stage} must be an object.
- ${label}.${stage}.${source} handlers must be functions.
- Factory rules.tools must be an object.
- Factory rules.tools.${toolName} must be an object.
- Factory rules.github must be an object.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/bad3414cd412e64d.
Report an issue: GitHub.