mastra-ai/mastra · error

${moved.code}: ${moved.reason}

Error message

${moved.code}: ${moved.reason}

What it means

After the intake stage, the dispatcher transitions the work item to its destination stage. If that destination transition is rejected by the rules engine (moved.status === 'rejected'), a plain Error '<code>: <reason>' is thrown. Unlike the intake failure, the work item already exists, so nothing is rolled back here.

Source

Thrown at mastracode/factory/src/rules/dispatcher.ts:804

        throw new Error(`${initial.code}: ${initial.reason}`);
      }
      expectedRevision = initial.revision;
    }
    if (decision.stage === 'intake') return;

    const moved = await this.#transitionService.transition({
      orgId: record.orgId,
      factoryProjectId: record.factoryProjectId,
      workItemId: result.item.id,
      board,
      stage: decision.stage,
      expectedRevision,
      actor: { type: 'system', id: 'factory-rule-dispatcher' },
      ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:destination` },
      cause: materializedByDecision ? 'linked_item_materialized' : 'linked_item_reconciled',
      causalChain,
    });
    if (moved.status === 'rejected') throw new Error(`${moved.code}: ${moved.reason}`);
  }

  async #requireItem(record: FactoryDeferredDecisionRecord) {
    if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');
    const item = await this.#storage.get({ orgId: record.orgId, id: record.workItemId });
    if (!item) throw new Error('Factory work item not found.');
    return item;
  }

  async #findBinding(
    record: FactoryDeferredDecisionRecord,
    role?: string,
  ): Promise<FactoryRunBindingRecord | undefined> {
    if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');
    const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId);
    return bindings
      .filter(candidate => candidate.status === 'active' && (role === undefined || candidate.role === role))
      .sort((left, right) => {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Parse '<code>: <reason>' from the message to find the rejected transition.
  2. Check the destination stage is valid and allowed from the current stage in the active rule version.
  3. Review rule guards for the destination stage against the item's metadata.
  4. Roll forward the item manually or adjust rules, then re-dispatch to reconcile.

Example fix

// before: assuming dispatch always completes
await dispatcher.dispatch(record);
// after
const res = await dispatcher.dispatch(record).catch(e => { alertOps(e.message); throw e; });
Defensive patterns

Strategy: try-catch

Validate before calling

if (!allowedTransitions(item.stage).includes(destinationStage)) throw new Error(`stage ${item.stage} -> ${destinationStage} not allowed`);

Type guard

function parseRejected(e: unknown): { code: string; reason: string } | null {
  const m = /^([^:]+): (.+)$/.exec(e instanceof Error ? e.message : '');
  return m ? { code: m[1], reason: m[2] } : null;
}

Try / catch

try {
  await dispatcher.dispatch(record);
} catch (e) {
  const r = parseRejected(e);
  if (r) await reconcileItemStage(record, r);
  else throw e;
}

Prevention

When it happens

Trigger: Dispatching a deferred decision where the materialization succeeded but the destination-stage transition (with cause linked_item_materialized or linked_item_reconciled) is rejected by rule evaluation.

Common situations: Stage ordering changed in the new rule version so the destination stage is unreachable; rule conditions for the destination stage fail; reconciling a linked item whose new state violates stage guards.

Related errors


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