mastra-ai/mastra · error · FactoryRuleValidationError
Factory rule causal depth exceeded.
Error message
Factory rule causal depth exceeded.
What it means
validateFactoryRuleDecision guards against unbounded recursive/nested causal decisions via a causalDepth counter (default 0). If a decision chain exceeds MAX_FACTORY_RULE_CAUSAL_DEPTH, this error is thrown to stop runaway recursion. Each nested decision validation increments the depth.
Source
Thrown at mastracode/factory/src/rules/validation.ts:207
for (const [event, leaf] of Object.entries(rules.linear)) {
enumValue(event, FACTORY_LINEAR_EVENTS, 'Factory Linear event');
if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`Factory rules.linear.${event} must be an object.`);
assertExactKeys(leaf, ['onEvent'], `Factory rules.linear.${event}`);
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'],View on GitHub (pinned to 75dd419e61)
Solutions
- Break the recursion: stop returning decisions that embed prior decision objects in causal chains
- Flatten the decision payload — pass IDs/references instead of nested decision objects
- Increase review of reenter flags that let a rule re-fire on its own transition
- Inspect MAX_FACTORY_RULE_CAUSAL_DEPTH in validation.ts to know the limit and count your chain depth
Example fix
// before
const decision = { type: 'transition', ..., causal: parentDecision } // nests prior decision
// after
const decision = { type: 'transition', ..., causalId: parentDecision.idempotencyKey } Defensive patterns
Strategy: validation
Validate before calling
function assertCausalChainDepth(decision, max = MAX_DEPTH) {
let d = decision, depth = 0;
while (d?.causal) { d = d.causal; if (++depth > max) throw new Error('causal chain too deep'); }
} Type guard
const isShallowDecision = (d, max = 3) => {
let cur = d, depth = 0;
while (cur?.causal) { cur = cur.causal; if (++depth > max) return false; }
return true;
}; Try / catch
try {
return decision(input);
} catch (e) {
if (e instanceof FactoryRuleValidationError && e.message === 'Factory rule causal depth exceeded.') {
logger.error('Runaway rule recursion; flattening decision chain');
return decision(flattenDecision(input));
}
throw e;
} Prevention
- Reference prior decisions by idempotencyKey, not by embedding them
- Audit rules combined with reenter: true for self-triggering loops
- Add a depth counter in your own rule handlers
- Keep decision payloads flat and id-based
When it happens
Trigger: A rule's onEvent handler returning a decision that itself references/produces nested decisions recursively, e.g. a decision whose payload contains another decision chain longer than the max depth; mutually recursive rules feeding each other's decisions.
Common situations: A transition decision's message or linked data embedding previously produced decisions in a loop; rules that re-trigger themselves (possibly combined with reenter: true) causing ever-deeper causal chains.
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/bfb01e7d78bf1dc2.
Report an issue: GitHub.