mastra-ai/mastra · error · FactoryRuleValidationError
${label}.${stage}.${source} handlers must be functions.
Error message
${label}.${stage}.${source} handlers must be functions. What it means
The onEnter/onExit fields of a stage+source leaf must be functions or omitted (undefined). This error fires when either handler value is present but not callable — the library cannot invoke it during board transitions.
Source
Thrown at mastracode/factory/src/rules/validation.ts:153
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.');
for (const [toolName, leaf] of Object.entries(rules.tools)) {
boundedString(toolName, 'Factory tool name', 128, IDENTIFIER_RE);
if (!isPlainObject(leaf))
throw new FactoryRuleValidationError(`Factory rules.tools.${toolName} must be an object.`);View on GitHub (pinned to 75dd419e61)
Solutions
- Pass the function reference itself, not a call result or a string name.
- If rules must be serialized, store identifiers and resolve them to functions at load time before calling assertFactoryRules.
- Remove the key entirely if no handler is needed (undefined is allowed).
Example fix
// before
work: { plan: { 'github-pr': { onEnter: 'onPlanEnter' } } }
// after
work: { plan: { 'github-pr': { onEnter: onPlanEnter } } } Defensive patterns
Strategy: validation
Validate before calling
for (const [stage, sources] of Object.entries(rules.work ?? {})) {
for (const [source, leaf] of Object.entries(sources ?? {})) {
for (const key of ['onEnter', 'onExit']) {
const h = leaf?.[key];
if (h !== undefined && typeof h !== 'function') {
throw new Error(`work.${stage}.${source}.${key} must be a function or omitted`);
}
}
}
} Type guard
const isHandler = (v: unknown): v is ((...args: never[]) => unknown) | undefined => v === undefined || typeof v === 'function';
Try / catch
try {
assertFactoryRules(rules);
} catch (e) {
if (e.name === 'FactoryRuleValidationError' && /handlers must be functions/.test(e.message)) {
console.error(`Non-function handler: ${e.message}`); // resolve names/strings to functions before validating
} else throw e;
} Prevention
- Never serialize rule objects containing handlers through JSON; resolve string references at load time.
- Avoid `handler && fn` conditionals; use `enabled ? fn : undefined`.
- Enable no-implicit-any so a mistyped handler fails typecheck, not at runtime.
When it happens
Trigger: Setting rules.work.<stage>.<source>.onEnter or .onExit to a string naming a function, an async result, a Promise, an object, or a number instead of a function, via assertFactoryRules.
Common situations: Serializing rules to JSON (functions become strings or are lost) and reloading them; assigning a thunk `myFn()` result instead of `myFn`; config-driven rules where handlers are referenced by name but never resolved.
Related errors
- ${label} must be an object.
- ${label}.${stage} must be an object.
- ${label}.${stage}.${source} must be an object.
- 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/6aa6c604187160e8.
Report an issue: GitHub.