mastra-ai/mastra · error · FactoryRuleValidationError

A rejection cannot be persisted with commit decisions.

Error message

A rejection cannot be persisted with commit decisions.

What it means

A batch passed to validateFactoryRuleDecisions is either a pure commit (create/update/link/message/skill/notification decisions) or a single reject — never both. If any validated decision has type 'reject' the whole batch is thrown out, because persisting a rejection alongside commit decisions would make the transaction semantics ambiguous.

Source

Thrown at mastracode/factory/src/rules/validation.ts:366

        title: boundedString(value.title, 'Factory notification title', MAX_TITLE_LENGTH),
        ...(body ? { body } : {}),
        ...(level ? { level } : {}),
      };
    }
    default:
      throw new FactoryRuleValidationError('Factory rule decision type is unsupported.');
  }
}

export function validateFactoryRuleDecisions(values: readonly unknown[], causalDepth = 0): FactoryCommitDecision[] {
  if (values.length > MAX_JSON_COLLECTION_SIZE) {
    throw new FactoryRuleValidationError('Factory rule produced too many decisions.');
  }
  const decisions: FactoryCommitDecision[] = [];
  for (const value of values) {
    const decision = validateFactoryRuleDecision(value, causalDepth);
    if (decision.type === 'reject') {
      throw new FactoryRuleValidationError('A rejection cannot be persisted with commit decisions.');
    }
    decisions.push(decision);
  }
  const keys = decisions.map(decision => decision.idempotencyKey);
  if (new Set(keys).size !== keys.length) {
    throw new FactoryRuleValidationError('Factory decisions require unique idempotency keys.');
  }
  return decisions;
}

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Return either [reject] alone, or only commit decisions — early-return the reject instead of appending it.
  2. Restructure the rule so a failing condition short-circuits to a single reject decision.
  3. Convert the reject into an error/exception path (or a notification decision) if partial commits are intended.

Example fix

// before
const out = [commitDecision()];
if (bad) out.push({ type: 'reject', ... });
return out;
// after
if (bad) return [{ type: 'reject', ... }];
return [commitDecision()];
Defensive patterns

Strategy: validation

Validate before calling

function checkNoRejectMix(decisions) {
  const hasReject = decisions.some(d => d.type === 'reject');
  if (hasReject && decisions.length > 1) {
    throw new Error('reject must be the only decision in a batch');
  }
}

Type guard

function isPureRejectBatch(ds: Array<{ type: string }>): ds is [{ type: 'reject' }] {
  return ds.length === 1 && ds[0].type === 'reject';
}

Try / catch

try {
  return validateFactoryRuleDecisions(decisions);
} catch (e) {
  if (e instanceof FactoryRuleValidationError && /rejection cannot be persisted/.test(e.message)) {
    const reject = decisions.find(d => d.type === 'reject');
    return validateFactoryRuleDecisions(reject ? [reject] : []); // fail the batch outright
  }
  throw e;
}

Prevention

When it happens

Trigger: A rule returning an array like [{type:'reject',...},{type:'message',...}] from evaluate, ingestToolResult, transition, or the other listed callers — i.e. any mix containing a reject decision with at least one commit decision.

Common situations: Rules that conditionally push a reject into a shared decisions array while other branches push commits; aggregating results from multiple sub-checks where one fails with reject while others succeeded.

Related errors


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