gastownhall/beads · error · issueops.ErrValidation
%w: apply batch requires at least one item
Error message
%w: apply batch requires at least one item
What it means
PlanApplyBatch rejects batches with zero items: an empty batch is meaningless work and is refused as a request-shape error wrapped with issueops.ErrValidation. This is the second check, after the Actor check, per the documented contract order.
Source
Thrown at internal/storage/batch_apply.go:74
// from it, so the two cannot disagree about what a key means.
KeyIndex map[string]int
}
// 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,View on GitHub (pinned to 71377f2769)
Solutions
- Guard the call site: skip PlanApplyBatch (and any follow-up apply) when len(items) == 0.
- If empty batches should be legal no-ops, handle that in the caller before constructing the request.
- Check upstream filters/queries that unexpectedly produced zero items if an empty batch is a surprise.
Example fix
// before
plan, err := PlanApplyBatch(issueops.ApplyBatchRequest{Actor: actor, Items: items})
// after
if len(items) == 0 {
return nil // nothing to apply
}
plan, err := PlanApplyBatch(issueops.ApplyBatchRequest{Actor: actor, Items: items}) Defensive patterns
Strategy: validation
Validate before calling
if len(req.Items) == 0 {
return nil // nothing to do
} Try / catch
_, err := storage.PlanApplyBatch(req)
if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "at least one item") {
return nil // treat as no-op
} Prevention
- Guard batch call sites with an empty-check before invoking.
- Log when upstream filters produce zero items so empty batches are visible.
- Return early from pipelines when no operations were collected.
When it happens
Trigger: Calling PlanApplyBatch with Items: nil or an empty slice — typically when an upstream filter produced no operations but the code still invoked the batch API.
Common situations: Automation that collects changes and finds none (e.g. no issues matched a query) but unconditionally calls ApplyBatch; a loop that filters out all invalid items leaving an empty list.
Related errors
- %s cannot be empty
- no rules to compact
- memory content cannot be empty
- memory key must not be empty
- %w: apply batch requires an actor
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/66e7323e4e99d6b8.
Report an issue: GitHub.