gastownhall/beads · error · issueops.ErrValidation
%w: apply batch accepts at most %d items, got %d
Error message
%w: apply batch accepts at most %d items, got %d
What it means
PlanApplyBatch enforces a hard upper bound of issueops.MaxApplyBatchItems per request to keep planning and application bounded. Exceeding it fails with the configured maximum and the actual count, wrapped with issueops.ErrValidation.
Source
Thrown at internal/storage/batch_apply.go:77
// PlanApplyBatch validates an apply-batch request and normalizes its waits-for
// gate metadata. It is the whole of the role's request validation: every
// implementation calls it before touching a substrate, so a refused request
// costs no database work anywhere.
//
// THE ORDER OF THE CHECKS IS PART OF THE CONTRACT, because a request can be
// wrong in several ways at once and a caller fixing them one at a time needs
// the same answer every time. Request-level shape first, then per-item shape,
// then the ref graph, then the guards.
func PlanApplyBatch(in issueops.ApplyBatchRequest) (ApplyBatchPlan, error) {
if in.Actor == "" {
return ApplyBatchPlan{}, fmt.Errorf("%w: apply batch requires an actor", issueops.ErrValidation)
}
if len(in.Items) == 0 {
return ApplyBatchPlan{}, fmt.Errorf("%w: apply batch requires at least one item", issueops.ErrValidation)
}
if len(in.Items) > issueops.MaxApplyBatchItems {
return ApplyBatchPlan{}, fmt.Errorf("%w: apply batch accepts at most %d items, got %d",
issueops.ErrValidation, issueops.MaxApplyBatchItems, len(in.Items))
}
keyIndex, err := planApplyBatchKeys(in.Items)
if err != nil {
return ApplyBatchPlan{}, err
}
plan := ApplyBatchPlan{
Actor: in.Actor,
Provenance: in.Provenance,
ForceIDPrefix: in.ForceIDPrefix,
SkipPerEdgeCycleCheck: in.SkipPerEdgeCycleCheck,
Items: make([]issueops.ApplyItem, len(in.Items)),
KeyIndex: keyIndex,
}
copy(plan.Items, in.Items)
View on GitHub (pinned to 71377f2769)
Solutions
- Split the item list into chunks of at most issueops.MaxApplyBatchItems and plan/apply each chunk.
- Cap the batch size at call sites by slicing: items[:MaxApplyBatchItems], then continue in a loop.
- Reduce batch size at the source (stream or paginate the input) if memory is also a concern.
Example fix
// before
plan, err := PlanApplyBatch(req{Actor: a, Items: all500Items})
// after
for chunk := range slices.Chunk(all500Items, issueops.MaxApplyBatchItems) {
plan, err := PlanApplyBatch(req{Actor: a, Items: chunk})
// apply plan...
} Defensive patterns
Strategy: validation
Validate before calling
if len(req.Items) > issueops.MaxApplyBatchItems {
return fmt.Errorf("batch too large: %d > %d", len(req.Items), issueops.MaxApplyBatchItems)
} Try / catch
_, err := storage.PlanApplyBatch(req)
if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "at most") {
return chunkAndApply(req) // split and retry per chunk
} Prevention
- Always chunk bulk imports to MaxApplyBatchItems up front.
- Import MaxApplyBatchItems from the library rather than hardcoding limits.
- Stream large inputs instead of accumulating one giant slice.
When it happens
Trigger: Calling PlanApplyBatch with len(Items) > MaxApplyBatchItems — e.g. importing a large export file in one call instead of chunking.
Common situations: Bulk imports/migrations from CSV or another tracker; scripts that accumulate all changes without batching; users passing an entire .jsonl export at once.
Related errors
- %w: apply batch requires an actor
- %w: apply batch requires at least one item
- %w: apply batch item %d must carry exactly one payload, got
- %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/673cb724fe2175e4.
Report an issue: GitHub.