gastownhall/beads · error · storage.ErrValidation
ErrValidation
ErrValidation
Error message
%w: update requires actor and issue ID
What it means
ExecuteUpdate applies a guarded issue update inside a transaction and requires an explicit Actor and IssueID on the request. When either is empty it returns storage.ErrValidation wrapped with this message, before any validation or database work runs. This forces audit-attributable, explicitly targeted updates.
Source
Thrown at internal/storage/issueops/execution.go:135
// skippedDependencyError refuses a guarded create whose requested edges were
// not all written. The batch engine drops a dangling edge so a partial import
// still lands, but a guarded create that reported success while silently
// discarding a parent, waits-for, or explicit dependency is data loss: the
// caller has no way to learn the relationship is missing. Refusing rolls the
// whole create back with the enclosing transaction.
func skippedDependencyError(skipped []skippedDependency) error {
edges := make([]string, 0, len(skipped))
for _, edge := range skipped {
edges = append(edges, fmt.Sprintf("%s -> %s (%s)", edge.issueID, edge.dependsOnID, edge.reason))
}
return fmt.Errorf("create: dependencies could not be created: %s: %w", strings.Join(edges, "; "), storage.ErrNotFound)
}
// ExecuteUpdate applies a guarded update in tx and reports durable tables changed.
func ExecuteUpdate(ctx context.Context, tx *sql.Tx, request publicops.UpdateRequest) (publicops.UpdateResult, ChangedTables, error) {
attempt := CloneUpdateRequest(request)
if attempt.Actor == "" || attempt.IssueID == "" {
return publicops.UpdateResult{}, nil, fmt.Errorf("%w: update requires actor and issue ID", storage.ErrValidation)
}
if err := ValidateUpdateRequest(attempt); err != nil {
return publicops.UpdateResult{}, nil, err
}
if err := ValidateMetadataPatch(attempt.Patch.Metadata); err != nil {
return publicops.UpdateResult{}, nil, err
}
// The plane restriction is resolved HERE, inside the update's own
// transaction, so a caller that serves durable issues only cannot be handed
// a wisp by a resolve that ran earlier.
if attempt.IssuePlaneOnly && IsActiveWispInTx(ctx, tx, attempt.IssueID) {
return publicops.UpdateResult{}, nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, attempt.IssueID)
}
tables := ChangedTables{}
before, err := GetIssueInTx(ctx, tx, attempt.IssueID)
if err != nil {
return publicops.UpdateResult{}, nil, err
}View on GitHub (pinned to 71377f2769)
Solutions
- Set Actor on the UpdateRequest (e.g. the CLI username or service identity) before calling.
- Set IssueID to the target issue's ID (bd-xxxx).
- If building requests from flags, default Actor from config/env at the CLI boundary.
- Run storage.ValidateUpdateRequest-style checks client-side before invoking.
Example fix
// before
req := publicops.UpdateRequest{IssueID: "bd-42", Patch: patch}
// after
req := publicops.UpdateRequest{IssueID: "bd-42", Actor: "agent", Patch: patch} Defensive patterns
Strategy: validation
Validate before calling
func validUpdate(r publicops.UpdateRequest) error {
if r.Actor == "" {
return fmt.Errorf("update requires actor")
}
if r.IssueID == "" {
return fmt.Errorf("update requires issue ID")
}
return nil
} Try / catch
res, _, err := ExecuteUpdate(ctx, tx, req)
if errors.Is(err, storage.ErrValidation) {
return fmt.Errorf("request rejected: %w (set Actor and IssueID)", err)
} Prevention
- Build requests through a constructor that requires actor and issueID parameters.
- Default Actor from config/env at the CLI boundary so it is never empty.
- Validate requests with storage validation helpers before opening a transaction.
When it happens
Trigger: Calling ExecuteUpdate (directly or via applyUpdate / spliceMetadataRefs) with a publicops.UpdateRequest where Actor == "" or IssueID == "".
Common situations: Programmatic updates that set Patch fields but forget Actor; constructing UpdateRequest from CLI flags where the actor flag was omitted; ID left empty when the caller expected auto-detection from context.
Understand the failure class
Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.
Related errors
- proxy.ForceStopUnverified: at most one options value is allo
- %w: apply batch requires an actor
- proxy.ForceStopUnverified: timeout must be positive, got %s
- issue must not be nil
- add dep: dep must not be nil
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/93a161abc96f6cc6.
Report an issue: GitHub.