mastra-ai/mastra · error · FactoryRuleValidationError
Factory rule decision type is required.
Error message
Factory rule decision type is required.
What it means
A rule decision object must include a string `type` field that selects the decision kind (e.g. 'reject', 'transition'). If value.type is missing or not a string, this error is thrown. The type drives the switch that validates the rest of the decision shape.
Source
Thrown at mastracode/factory/src/rules/validation.ts:211
if (leaf.onEvent !== undefined && typeof leaf.onEvent !== 'function') {
throw new FactoryRuleValidationError(`Factory rules.linear.${event}.onEvent must be a function.`);
}
}
}
function commonCommitFields(value: Record<string, unknown>): { idempotencyKey: string } {
return {
idempotencyKey: boundedString(value.idempotencyKey, 'Factory decision idempotencyKey', MAX_IDEMPOTENCY_KEY_LENGTH),
};
}
export function validateFactoryRuleDecision(value: unknown, causalDepth = 0): FactoryRuleDecision {
if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
throw new FactoryRuleValidationError('Factory rule causal depth exceeded.');
}
if (!isPlainObject(value)) throw new FactoryRuleValidationError('Factory rule decision must be an object.');
const type = value.type;
if (typeof type !== 'string') throw new FactoryRuleValidationError('Factory rule decision type is required.');
switch (type) {
case 'reject': {
assertExactKeys(value, ['type', 'code', 'reason'], 'Factory reject decision');
return {
type,
code: enumValue(value.code, REJECTION_CODES, 'Factory rejection code'),
reason: boundedString(value.reason, 'Factory rejection reason', MAX_REASON_LENGTH),
};
}
case 'transition': {
assertExactKeys(
value,
['type', 'idempotencyKey', 'board', 'stage', 'message', 'reenter'],
'Factory transition decision',
);
if (value.reenter !== undefined && typeof value.reenter !== 'boolean') {
throw new FactoryRuleValidationError('Factory transition reenter must be a boolean.');View on GitHub (pinned to 75dd419e61)
Solutions
- Always include a valid type string: { type: 'reject', code, reason } or { type: 'transition', ... }
- Check the switch in validateFactoryRuleDecision for the exact accepted type values ('reject', 'transition', etc.) and use one verbatim
- Ensure type is a plain string, not an enum wrapper object from another lib
- Add a pre-call assertion: if (typeof d.type !== 'string') throw ...
Example fix
// before
decision({ code: 'BLOCKED', reason: 'policy' })
// after
decision({ type: 'reject', code: 'BLOCKED', reason: 'policy' }) Defensive patterns
Strategy: validation
Validate before calling
const DECISION_TYPES = ['reject', 'transition']; // see switch in validateFactoryRuleDecision
function assertDecisionType(v) {
if (typeof v?.type !== 'string' || !DECISION_TYPES.includes(v.type)) {
throw new TypeError(`decision.type must be one of: ${DECISION_TYPES.join(', ')}`);
}
} Type guard
const hasDecisionType = (v): v is { type: string } & Record<string, unknown> =>
typeof (v as any)?.type === 'string'; Try / catch
try {
return decision(d);
} catch (e) {
if (e instanceof FactoryRuleValidationError && e.message === 'Factory rule decision type is required.') {
logger.error('Decision missing type discriminator', { keys: Object.keys(d ?? {}) });
return decision({ type: 'reject', code: 'INVALID_DECISION', reason: 'missing type' });
}
throw e;
} Prevention
- Always build decisions through a single factory function that sets type
- Keep type as a string literal, not an enum object
- Spread carefully; spread after setting type so it can't be overwritten with undefined
- Add a schema check (zod discriminated union) before calling decision()
When it happens
Trigger: decision({ code: 'deny', reason: 'x' }) with type omitted; type set to null/undefined; building the decision with a spread that drops type; type coming from config as a non-string.
Common situations: Constructing decisions dynamically where the type key was conditional and never set; JSON payloads from an external source missing the discriminator field; renaming a variable and dropping the type property.
Understand the failure class
Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.
Related errors
- Factory rule version is required.
- ${label} contains an unsupported field.
- Rule metadata is too large.
- ${label} must be an object.
- ${label}.${stage} must be an object.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/116436220af3ac34.
Report an issue: GitHub.