{"record":{"id":"66e7323e4e99d6b8","repo":"gastownhall/beads","slug":"w-apply-batch-requires-at-least-one-item","errorCode":null,"errorMessage":"%w: apply batch requires at least one item","messagePattern":"%w: apply batch requires at least one item","errorType":"validation","errorClass":"issueops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/batch_apply.go","lineNumber":74,"sourceCode":"\t// from it, so the two cannot disagree about what a key means.\n\tKeyIndex map[string]int\n}\n\n// PlanApplyBatch validates an apply-batch request and normalizes its waits-for\n// gate metadata. It is the whole of the role's request validation: every\n// implementation calls it before touching a substrate, so a refused request\n// costs no database work anywhere.\n//\n// THE ORDER OF THE CHECKS IS PART OF THE CONTRACT, because a request can be\n// wrong in several ways at once and a caller fixing them one at a time needs\n// the same answer every time. Request-level shape first, then per-item shape,\n// then the ref graph, then the guards.\nfunc PlanApplyBatch(in issueops.ApplyBatchRequest) (ApplyBatchPlan, error) {\n\tif in.Actor == \"\" {\n\t\treturn ApplyBatchPlan{}, fmt.Errorf(\"%w: apply batch requires an actor\", issueops.ErrValidation)\n\t}\n\tif len(in.Items) == 0 {\n\t\treturn ApplyBatchPlan{}, fmt.Errorf(\"%w: apply batch requires at least one item\", issueops.ErrValidation)\n\t}\n\tif len(in.Items) > issueops.MaxApplyBatchItems {\n\t\treturn ApplyBatchPlan{}, fmt.Errorf(\"%w: apply batch accepts at most %d items, got %d\",\n\t\t\tissueops.ErrValidation, issueops.MaxApplyBatchItems, len(in.Items))\n\t}\n\n\tkeyIndex, err := planApplyBatchKeys(in.Items)\n\tif err != nil {\n\t\treturn ApplyBatchPlan{}, err\n\t}\n\n\tplan := ApplyBatchPlan{\n\t\tActor:                 in.Actor,\n\t\tProvenance:            in.Provenance,\n\t\tForceIDPrefix:         in.ForceIDPrefix,\n\t\tSkipPerEdgeCycleCheck: in.SkipPerEdgeCycleCheck,\n\t\tItems:                 make([]issueops.ApplyItem, len(in.Items)),\n\t\tKeyIndex:              keyIndex,","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/batch_apply.go#L56-L92","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nplan, err := PlanApplyBatch(issueops.ApplyBatchRequest{Actor: actor, Items: items})\n// after\nif len(items) == 0 {\n    return nil // nothing to apply\n}\nplan, err := PlanApplyBatch(issueops.ApplyBatchRequest{Actor: actor, Items: items})","handlingStrategy":"validation","validationCode":"if len(req.Items) == 0 {\n    return nil // nothing to do\n}","typeGuard":null,"tryCatchPattern":"_, err := storage.PlanApplyBatch(req)\nif errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), \"at least one item\") {\n    return nil // treat as no-op\n}","preventionTips":["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."],"tags":["validation","batch","empty-input"],"backgroundTag":"validation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}