{"record":{"id":"6aa6c604187160e8","repo":"mastra-ai/mastra","slug":"label-stage-source-handlers-must-be-funct","errorCode":null,"errorMessage":"${label}.${stage}.${source} handlers must be functions.","messagePattern":"(.+?)\\.(.+?)\\.(.+?) handlers must be functions\\.","errorType":"validation","errorClass":"FactoryRuleValidationError","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/rules/validation.ts","lineNumber":153,"sourceCode":"  if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');\n  if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {\n    throw new FactoryRuleValidationError('Rule metadata is too large.');\n  }\n  return sanitized;\n}\n\nfunction validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {\n  if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);\n  for (const [stage, sources] of Object.entries(rules)) {\n    enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);\n    if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);\n    for (const [source, leaf] of Object.entries(sources)) {\n      enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);\n      if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);\n      assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);\n      for (const handler of Object.values(leaf)) {\n        if (handler !== undefined && typeof handler !== 'function') {\n          throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);\n        }\n      }\n    }\n  }\n}\n\nexport function assertFactoryRules(rules: unknown): asserts rules is FactoryRules {\n  if (!isPlainObject(rules)) throw new FactoryRuleValidationError('Factory rules must be an object.');\n  assertExactKeys(rules, ['version', 'work', 'review', 'tools', 'github', 'linear'], 'Factory rules');\n  boundedString(rules.version, 'Factory rule version', MAX_VERSION_LENGTH);\n  validateBoardRules(rules.work, 'Factory rules.work');\n  validateBoardRules(rules.review, 'Factory rules.review');\n\n  if (!isPlainObject(rules.tools)) throw new FactoryRuleValidationError('Factory rules.tools must be an object.');\n  for (const [toolName, leaf] of Object.entries(rules.tools)) {\n    boundedString(toolName, 'Factory tool name', 128, IDENTIFIER_RE);\n    if (!isPlainObject(leaf))\n      throw new FactoryRuleValidationError(`Factory rules.tools.${toolName} must be an object.`);","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/rules/validation.ts#L135-L171","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nwork: { plan: { 'github-pr': { onEnter: 'onPlanEnter' } } }\n// after\nwork: { plan: { 'github-pr': { onEnter: onPlanEnter } } }","handlingStrategy":"validation","validationCode":"for (const [stage, sources] of Object.entries(rules.work ?? {})) {\n  for (const [source, leaf] of Object.entries(sources ?? {})) {\n    for (const key of ['onEnter', 'onExit']) {\n      const h = leaf?.[key];\n      if (h !== undefined && typeof h !== 'function') {\n        throw new Error(`work.${stage}.${source}.${key} must be a function or omitted`);\n      }\n    }\n  }\n}","typeGuard":"const isHandler = (v: unknown): v is ((...args: never[]) => unknown) | undefined =>\n  v === undefined || typeof v === 'function';","tryCatchPattern":"try {\n  assertFactoryRules(rules);\n} catch (e) {\n  if (e.name === 'FactoryRuleValidationError' && /handlers must be functions/.test(e.message)) {\n    console.error(`Non-function handler: ${e.message}`); // resolve names/strings to functions before validating\n  } else throw e;\n}","preventionTips":["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."],"tags":["validation","handlers","type-mismatch","factory-rules"],"backgroundTag":"handler-not-a-function","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}