gastownhall/beads · error
invalid field for update: %s
Error message
invalid field for update: %s
What it means
updateIssueInTx rejects an update map containing a key that is not in the allowlist of updatable fields (IsAllowedUpdateField). This prevents arbitrary or misspelled column names from reaching the SQL SET clause.
Source
Thrown at internal/storage/issueops/update.go:426
return nil, err
}
if crossing {
if _, err := EnforceClosePolicyInTx(ctx, tx, id, forceClosePolicy); err != nil {
return nil, err
}
}
if err := ValidateScalarUpdates(ctx, tx, updates); err != nil {
return nil, err
}
// Build SET clauses.
setClauses := []string{"updated_at = ?"}
args := []interface{}{time.Now().UTC()}
for key, value := range updates {
if !IsAllowedUpdateField(key) {
return nil, fmt.Errorf("invalid field for update: %s", key)
}
columnName := key
if key == "wisp" {
columnName = "ephemeral"
}
setClauses = append(setClauses, fmt.Sprintf("`%s` = ?", columnName))
// Handle JSON serialization for array fields stored as TEXT.
if key == "waiters" {
waitersJSON, _ := json.Marshal(value)
args = append(args, string(waitersJSON))
} else if key == "metadata" {
metadataStr, err := storage.NormalizeMetadataValue(value)
if err != nil {
return nil, fmt.Errorf("invalid metadata: %w", err)
}
args = append(args, metadataStr)View on GitHub (pinned to 71377f2769)
Solutions
- Check the key against IsAllowedUpdateField (or the allowlist constant) before adding it to the map.
- Fix the spelling/case of the field name.
- Remove non-updatable fields (id, created_at) from the map; use dedicated APIs for them.
Example fix
// before
updates := map[string]interface{}{"Assignee": "alice"}
// after
updates := map[string]interface{}{"assignee": "alice"} Defensive patterns
Strategy: validation
Validate before calling
for key := range updates {
if !issueops.IsAllowedUpdateField(key) {
return fmt.Errorf("field %q is not updatable", key)
}
} Type guard
func filterUpdatable(updates map[string]interface{}) map[string]interface{} {
out := map[string]interface{}{}
for k, v := range updates {
if issueops.IsAllowedUpdateField(k) {
out[k] = v
}
}
return out
} Try / catch
if _, err := storage.UpdateIssue(ctx, id, updates, actor); err != nil {
if strings.Contains(err.Error(), "invalid field for update") {
return fmt.Errorf("update rejected (bad field): %w", err)
}
return err
} Prevention
- Run IsAllowedUpdateField on every key before building the map.
- Use the same field-name constants as the schema; avoid hand-typed strings.
- Remember wisp maps to the ephemeral column internally.
- Never include id/created_at in update maps.
When it happens
Trigger: Calling UpdateIssueInTx / UpdateIssueWithoutEventInTx with a map key like "assignee " (trailing space), a wrong-cased key ("Assignee"), a non-column key ("title_extra"), or a field that exists on the struct but is not updatable (e.g. "id", "created_at").
Common situations: Refactors renaming struct fields; hand-built update maps with typos; passing computed labels instead of column names; trying to update read-only fields like id or created_at.
Related errors
- no store is open for this workspace
- not found
- db: ChildCounterSQLRepository.NextChildID: parentID must not
- db: DependencySQLRepository.Insert: dep must not be nil
- db: DependencySQLRepository.Insert: IssueID must not be empt
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5df4ae370c1d2608.
Report an issue: GitHub.