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

  1. Always include a valid type string: { type: 'reject', code, reason } or { type: 'transition', ... }
  2. Check the switch in validateFactoryRuleDecision for the exact accepted type values ('reject', 'transition', etc.) and use one verbatim
  3. Ensure type is a plain string, not an enum wrapper object from another lib
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/116436220af3ac34. Report an issue: GitHub.