gastownhall/beads · error · issueops.ErrValidation

: apply batch item has a metadata_ref with an empty key

Error message

%w: apply batch item %d has a metadata_ref with an empty key

What it means

Each entry in CreateItem.MetadataRefs is keyed by a metadata key; an empty-string map key is meaningless and rejected. This error fires when a create item's MetadataRefs map contains an entry whose key is "". Wraps issueops.ErrValidation.

Solutions

  1. Filter out entries with empty metadata keys before building MetadataRefs
  2. Fix the upstream source so every metadata entry has a non-empty name
  3. Skip (or error at ingest) records missing a metadata key rather than adding them with ""

Example fix

// before
refs[k.Name] = issueops.Ref{Key: k.RefKey} // k.Name may be ""
// after
if k.Name == "" { return fmt.Errorf("metadata entry missing name") }
refs[k.Name] = issueops.Ref{Key: k.RefKey}
Defensive patterns

Strategy: validation

Validate before calling

for i, it := range items {
  if it.Create == nil { continue }
  for k := range it.Create.MetadataRefs {
    if k == "" { return fmt.Errorf("create item %d has empty metadata key", i) }
  }
}

Type guard

func hasEmptyMetaKey(it *issueops.CreateItem) bool {
  if it == nil { return false }
  _, bad := it.MetadataRefs[""]
  return bad
}

Try / catch

if err := store.PlanApplyBatch(plan); err != nil {
  if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "empty key") {
    // sanitize MetadataRefs keys and rebuild the plan
  }
  return err
}

Prevention

When it happens

Trigger: PlanApplyBatch with a create item built like MetadataRefs: map[string]issueops.Ref{"": {Key: "other"}} — e.g. dynamically populating the map from data where the metadata key was missing.

Common situations: Populating MetadataRefs by iterating over a source map that has empty keys; slicing/config where the metadata name column was blank; a nil/zero metadata name before insertion.

Related errors


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

Appendix: source

Thrown at internal/storage/batch_apply.go:201

			return err
		}
		item.DepAdd = &edge
		return nil
	}
	return fmt.Errorf("%w: apply batch item %d has unknown kind %q", issueops.ErrValidation, index, item.Kind)
}

// planApplyBatchCreate checks a create's metadata refs and records the row it
// mints as touched.
//
// A METADATA REF MAY NAME ANY KEY, including this item's own and one declared
// later. Every id is minted before any splice runs, so the direction cannot
// matter — see issueops.CreateItem.MetadataRefs. What it may not do is name a
// key no item declares.
func planApplyBatchCreate(item *issueops.CreateItem, index int, keyIndex map[string]int, touched map[string]bool) error {
	for metaKey, ref := range item.MetadataRefs {
		if metaKey == "" {
			return fmt.Errorf("%w: apply batch item %d has a metadata_ref with an empty key", issueops.ErrValidation, index)
		}
		if err := validateApplyRef(ref, index, "metadata_ref "+metaKey); err != nil {
			return err
		}
		if ref.Key == "" {
			continue
		}
		if _, ok := keyIndex[ref.Key]; !ok {
			return &issueops.RefError{Index: index, Member: "metadata_ref " + metaKey, Key: ref.Key}
		}
	}
	if item.Key != "" {
		touched[applyTouchKeyRef(issueops.Ref{Key: item.Key})] = true
	}
	if item.Issue != nil && item.Issue.ID != "" {
		touched[applyTouchKeyRef(issueops.Ref{ID: item.Issue.ID})] = true
	}
	return nil

View on GitHub (pinned to 71377f2769)