mastra-ai/mastra · error · FactoryRuleValidationError
${label}.${stage} must be an object.
Error message
${label}.${stage} must be an object. What it means
Within a board, each key must be a valid FACTORY_RULE_STAGES enum value whose value is a plain object mapping sources to leaf handlers. This error fires when a stage entry's value is not a plain object (after the stage name itself passed the enum check).
Source
Thrown at mastracode/factory/src/rules/validation.ts:146
seen.delete(value as object);
}
}
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');View on GitHub (pinned to 75dd419e61)
Solutions
- Make the stage value an object keyed by valid source names: { 'github-issue': { onEnter: fn, onExit: fn } }.
- Convert arrays of handlers into per-source leaf objects.
- Verify no extra nesting level was removed/added when refactoring the rules shape.
Example fix
// before
work: { plan: [myEnterHandler] }
// after
work: { plan: { 'github-issue': { onEnter: myEnterHandler } } } Defensive patterns
Strategy: validation
Validate before calling
const STAGES = ['todo', 'in-progress', 'review', 'done']; // match FACTORY_RULE_STAGES
for (const [stage, sources] of Object.entries(rules.work ?? {})) {
if (!STAGES.includes(stage)) throw new Error(`Unknown stage: ${stage}`);
if (typeof sources !== 'object' || sources === null || Array.isArray(sources)) {
throw new Error(`rules.work.${stage} must be a plain object of source -> handlers`);
}
} Type guard
const isStageMap = (v: unknown): v is Record<string, Record<string, unknown>> => typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every(s => typeof s === 'object' && s !== null && !Array.isArray(s));
Try / catch
try {
prepare(rules);
} catch (e) {
if (e.name === 'FactoryRuleValidationError' && /must be an object/.test(e.message)) {
console.error(`Fix rules structure at: ${e.message.split(' ')[0]}`);
} else throw e;
} Prevention
- Model rules with the FactoryBoardRules type so stage/source nesting is enforced statically.
- Keep exactly three levels per board: stage -> source -> { onEnter, onExit }.
- Write a snapshot test over your default rules object that runs assertFactoryRules.
When it happens
Trigger: rules.work.<stage> or rules.review.<stage> set to an array, null, string, or class instance while <stage> is a valid stage name, evaluated during assertFactoryRules via prepare or defaultFactoryRules.
Common situations: Defining stages as arrays of handlers (rules.work.plan = [fn]); typo'd nested structure where a handler was hoisted to the stage level; config that collapsed a level after a schema change.
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}.${source} must be an object.
- ${label}.${stage}.${source} handlers must be functions.
- Factory rules.tools 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/81d1b05aa4021396.
Report an issue: GitHub.