mastra-ai/mastra · error

${result.reason}

Error message

${result.reason}

What it means

In useBoardComposer.submit, a manual creation flow first transitions the item on the work board via transition.mutateAsync; if the transition result status is 'rejected', the server-provided reason string is thrown verbatim as an Error. The message content is therefore whatever the board-transition service rejected with.

Source

Thrown at mastracode/factory-ui/src/ui/domains/factory/hooks/useBoardComposer.ts:47

  /**
   * Submits are re-entrant: a rejected transition keeps the composer open, so a
   * retry must update the card it already created instead of filing a duplicate.
   */
  const submit = async (forStage: BoardStageId, title: string) => {
    const pendingItem = pendingItemRef.current?.stage === forStage ? pendingItemRef.current : undefined;
    let item: WorkItem;
    if (pendingItem === undefined) {
      item = await create.mutateAsync({ source: 'manual', sourceKey: null, title, stages: ['intake'] });
      pendingItemRef.current = { stage: forStage, title, item };
    } else if (pendingItem.title !== title) {
      item = await update.mutateAsync({ id: pendingItem.item.id, patch: { title } });
      pendingItemRef.current = { stage: forStage, title, item };
    } else {
      item = pendingItem.item;
    }
    if (forStage !== 'intake') {
      const result = await transition.mutateAsync({ item, board: 'work', stage: forStage, cause: 'manual_creation' });
      if (result.status === 'rejected') throw new Error(result.reason);
    }
    pendingItemRef.current = undefined;
  };

  return {
    stage,
    open: setStage,
    close: (closing: BoardStageId) => {
      if (pendingItemRef.current?.stage === closing) pendingItemRef.current = undefined;
      closedStageRef.current = closing;
      setStage(current => (current === closing ? undefined : current));
    },
    registerTrigger: (forStage: BoardStageId) => (element: HTMLButtonElement | null) => {
      if (element) triggerRefs.current.set(forStage, element);
      else triggerRefs.current.delete(forStage);
    },
    submit,
  };

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Read the thrown message — it is the server's rejection reason — and address that specific rule/permission issue.
  2. Refresh the board/item state before retrying (the local item may be stale).
  3. Verify the target stage is valid for the item type and the user has transition permission.
  4. Prevent concurrent edits (single-tab workflows or optimistic-locking on the server).

Example fix

// before
if (result.status === 'rejected') throw new Error(result.reason);
// after
if (result.status === 'rejected') {
  await queryClient.invalidateQueries(['board', 'work']);
  throw new Error(result.reason);
}
Defensive patterns

Strategy: try-catch

Type guard

type TransitionResult = { status: 'ok' } | { status: 'rejected'; reason: string };
function isRejected(r: TransitionResult): r is { status: 'rejected'; reason: string } {
  return r.status === 'rejected';
}

Try / catch

try {
  await submit({ forStage, title, item });
} catch (e) {
  showToast(e instanceof Error ? e.message : 'Board transition rejected');
  await queryClient.invalidateQueries({ queryKey: ['board'] }); // resync stale state
}

Prevention

When it happens

Trigger: Submitting a composer item for a non-intake stage while the server rejects the board transition (invalid stage for the item, item locked/already moved, permission denied, or conflicting concurrent transition) — result.status === 'rejected' with a reason.

Common situations: Two tabs moving the same card concurrently; target stage not allowed by board rules for that item type; user lacks write permission on the board; stale item state after another actor changed it.

Related errors


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