gastownhall/beads · error · issueops.ErrValidation
%w: apply batch item %d must carry exactly one payload, got
Error message
%w: apply batch item %d must carry exactly one payload, got %d
What it means
Each ApplyBatchRequest item must carry exactly one payload (exactly one of Create, Update, Close, DepAdd non-nil). planApplyBatchKeys counts the non-nil payloads and fails with the item index and actual count, wrapped with issueops.ErrValidation. Zero payloads and multiple payloads are both rejected.
Source
Thrown at internal/storage/batch_apply.go:123
return plan, nil
}
// planApplyBatchKeys checks every item's kind/payload agreement and collects
// the create keys. It runs as its own pass because a ref may name a key
// declared by a LATER item, which is a different diagnosis from a key nothing
// declares — and telling them apart means knowing every key before checking any
// ref.
func planApplyBatchKeys(items []issueops.ApplyItem) (map[string]int, error) {
keyIndex := make(map[string]int, len(items))
for i, item := range items {
payloads := 0
for _, present := range []bool{item.Create != nil, item.Update != nil, item.Close != nil, item.DepAdd != nil} {
if present {
payloads++
}
}
if payloads != 1 {
return nil, fmt.Errorf("%w: apply batch item %d must carry exactly one payload, got %d",
issueops.ErrValidation, i, payloads)
}
var matches bool
switch item.Kind {
case issueops.ItemCreate:
matches = item.Create != nil
case issueops.ItemUpdate:
matches = item.Update != nil
case issueops.ItemClose:
matches = item.Close != nil
case issueops.ItemDepAdd:
matches = item.DepAdd != nil
default:
return nil, fmt.Errorf("%w: apply batch item %d has unknown kind %q", issueops.ErrValidation, i, item.Kind)
}
if !matches {
return nil, fmt.Errorf("%w: apply batch item %d is kind %q but carries another kind's payload",
issueops.ErrValidation, i, item.Kind)View on GitHub (pinned to 71377f2769)
Solutions
- Ensure each item sets exactly one of Create/Update/Close/DepAdd and leaves the others nil.
- If reusing a struct, construct a fresh ApplyBatchItem per operation instead of mutating a shared one.
- Match the payload to item.Kind — also see the kind/payload mismatch error that follows this check.
Example fix
// before
item := issueops.ApplyBatchItem{Kind: issueops.ItemCreate, Create: cr, Close: cl} // 2 payloads
// after
item := issueops.ApplyBatchItem{Kind: issueops.ItemCreate, Create: cr} Defensive patterns
Strategy: validation
Validate before calling
func payloadCount(it issueops.ApplyBatchItem) int {
n := 0
for _, p := range []*issueops.CreatePayload{it.Create} { if p != nil { n++ } }
if it.Update != nil { n++ }
if it.Close != nil { n++ }
if it.DepAdd != nil { n++ }
return n
} Try / catch
_, err := storage.PlanApplyBatch(req)
if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "exactly one payload") {
return fmt.Errorf("malformed batch item: %w", err)
} Prevention
- Build items via constructors that take one payload and set Kind accordingly.
- Never reuse/mutate a shared ApplyBatchItem across operations.
- Validate batch items in unit tests with table-driven payloads.
When it happens
Trigger: Building an item with no payload set, or with two payloads set (e.g. both Create and Close non-nil), or reusing a single item struct across kinds without clearing other fields.
Common situations: Generic builder code that fills multiple payload pointers; copy-pasted item literals; deserialization bugs where JSON fills unexpected payload fields.
Related errors
- %w: apply batch requires an actor
- %w: apply batch requires at least one item
- %w: apply batch accepts at most %d items, got %d
- %w: apply batch item %d has unknown kind %q
- ErrValidation
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0f03b98bd80a773f.
Report an issue: GitHub.