gastownhall/beads · warning

ErrNotClaimable

ErrNotClaimable

Error message

issue not claimable

What it means

ErrNotClaimable is returned when attempting to claim an issue that is not in a claimable state, such as closed, deferred, or already in progress without the same actor owning the claim. ClaimConflictError wraps it while carrying the state (assignee and status) that refused the claim, so the sentinel still matches via errors.Is.

Source

Thrown at issueops/errors.go:17

package issueops

import (
	"errors"
	"fmt"

	"github.com/steveyegge/beads/beadserrors"
)

// ErrAlreadyClaimed is returned when attempting to claim an issue that is already
// claimed by another user. The error message contains the current assignee.
var ErrAlreadyClaimed = errors.New("issue already claimed")

// ErrNotClaimable is returned when attempting to claim an issue that is not in a
// claimable state, such as closed, deferred, or already in progress without the
// same actor owning the claim.
var ErrNotClaimable = errors.New("issue not claimable")

// ClaimConflictError reports the state that refused a claim — the current
// assignee and status, read inside the same transaction that lost the
// compare-and-set. It wraps the refusal rather than replacing it, so the
// sentinel still matches, the refusal's carefully-worded prose survives
// byte-for-byte, and a caller can classify the conflict from typed fields
// instead of parsing that prose.
//
// Err is set by every implementation that returns this type; a Claimer whose
// same-transaction re-read fails returns the bare refusal instead.
type ClaimConflictError struct {
	// IssueID names the issue that refused the claim.
	IssueID string
	// Assignee is the holder observed by the losing transaction. It is empty
	// when the refusal was about the status rather than a foreign holder.
	Assignee string
	// Status is the status observed by the losing transaction.
	Status Status

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the issue's status first (via ClaimConflictError fields or a fresh read) and only claim open/unassigned issues.
  2. If the issue is in progress and you are the same actor, re-assert your claim rather than a fresh claim.
  3. Review the configured active/WIP status settings if the refusal state seems wrong for your workflow.
  4. If the issue is closed or deferred, reopen it (if policy allows) before claiming.

Example fix

// before
err := claimer.Claim(ctx, req) // refused: status=closed

// after
iss, _ := store.GetIssue(ctx, req.ID)
if iss.Status == "open" {
    err = claimer.Claim(ctx, req)
} else if errors.Is(err, issueops.ErrNotClaimable) {
    var c *issueops.ClaimConflictError
    errors.As(err, &c) // c.Status explains the refusal
}
Defensive patterns

Strategy: validation

Validate before calling

iss, _ := store.GetIssue(ctx, id)
claimable := iss.Status == "open" || (iss.Status == wipStatus && iss.Assignee == actor)
if !claimable {
    return fmt.Errorf("issue %s not claimable (status %s)", id, iss.Status)
}

Try / catch

if errors.Is(err, issueops.ErrNotClaimable) {
    var c *issueops.ClaimConflictError
    errors.As(err, &c) // c.Status explains what refused the claim
}

Prevention

When it happens

Trigger: Claiming an issue whose status is closed, deferred, or a configured WIP/ineligible status (e.g. built-in ineligible statuses or configured active statuses per RunClaimer tests); claiming while progress-status config rejects the transition.

Common situations: Race where the issue was closed between listing and claiming; claiming an issue someone already moved to in_progress; misconfigured active/WIP status lists in config making valid-looking claims refused.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/617401a61d89f3b6. Report an issue: GitHub.