gastownhall/beads · error · issueops.ErrValidation

%w: apply batch requires an actor

Error message

%w: apply batch requires an actor

What it means

PlanApplyBatch requires a non-empty Actor because every planned mutation must be attributable for audit/history. An empty Actor is the first request-shape check and fails before any item validation, wrapped with issueops.ErrValidation so callers can errors.Is on it.

Source

Thrown at internal/storage/batch_apply.go:71

	Items []issueops.ApplyItem
	// KeyIndex maps each create item's Key to the index that declares it. The
	// body resolves refs through it and the wire half reports an unknown key
	// 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,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set Actor to the acting user/bot identity before calling PlanApplyBatch.
  2. If the caller is a service, propagate the authenticated identity into the request instead of leaving it zero.
  3. Handle the error with errors.Is(err, issueops.ErrValidation) to distinguish validation failures from storage errors.

Example fix

// before
plan, err := PlanApplyBatch(issueops.ApplyBatchRequest{Items: items})
// after
plan, err := PlanApplyBatch(issueops.ApplyBatchRequest{Actor: "agent:ci-bot", Items: items})
Defensive patterns

Strategy: validation

Validate before calling

if req.Actor == "" {
    return fmt.Errorf("actor is required for batch apply")
}

Try / catch

_, err := storage.PlanApplyBatch(req)
if errors.Is(err, issueops.ErrValidation) {
    return fmt.Errorf("invalid batch request: %w", err)
}

Prevention

When it happens

Trigger: Calling PlanApplyBatch with issueops.ApplyBatchRequest{Actor: ""} — e.g. agent/CLI code that never set the acting user, or a struct literal omitting the Actor field.

Common situations: Headless scripts or bots constructing the request programmatically and forgetting the actor; refactors that renamed/dropped the Actor field; tests reusing a zero-value request struct.

Related errors


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