gastownhall/beads · error
db: Update: field %q is not allowed
Error message
db: Update: field %q is not allowed
What it means
Direct field-allowlist rejection from Update: every key in the updates map must exist in allowedUpdateFields (status, priority, title, assignee, owner, description, design, acceptance_criteria, notes, issue_type, estimated_minutes, external_ref, spec_id, started_at, closed_at, close_reason, closed_by_session, source_repo, sender, wisp, wisp_type, no_history, pinned, mol_type, event_kind, actor, target, payload, due_at, defer_until, await_id, waiters, metadata). Unknown keys are refused with this error before any SQL is built, preventing accidental column injection.
Source
Thrown at internal/storage/domain/db/issue.go:239
// work. The wrap keeps the sentinels matchable, so a caller distinguishes
// these refusals here exactly as it does on the close path.
if statusChanging {
crossing, err := issueops.CrossesIntoDoneCategoryInTx(ctx, r.runner, oldIssue.Status, updates)
if err != nil {
return fmt.Errorf("db: Update %s: %w", id, err)
}
if crossing {
if _, err := issueops.EnforceClosePolicyInTx(ctx, r.runner, id, forceClosePolicy); err != nil {
return fmt.Errorf("db: Update %s: %w", id, err)
}
}
}
setClauses := make([]string, 0, len(updates)+3)
args := make([]any, 0, len(updates)+4)
for key, value := range updates {
if _, ok := allowedUpdateFields[key]; !ok {
return fmt.Errorf("db: Update: field %q is not allowed", key)
}
column := key
if renamed, ok := updateFieldColumnRename[key]; ok {
column = renamed
}
setClauses = append(setClauses, fmt.Sprintf("`%s` = ?", column))
args = append(args, normalizeUpdateValue(key, value))
}
setClauses = append(setClauses, "updated_at = ?")
args = append(args, time.Now().UTC())
// Lifecycle parity with issueops.updateIssueInTx: auto-manage closed_at and
// started_at from the status transition unless the caller set them
// explicitly.
if statusChanging {
setClauses, args = issueops.ManageClosedAt(oldIssue, updates, setClauses, args)
setClauses, args = issueops.ManageStartedAt(oldIssue, updates, setClauses, args)
}View on GitHub (pinned to 71377f2769)
Solutions
- Compare the offending key (echoed in the error with %q) against allowedUpdateFields in internal/storage/domain/db/issue.go and rename it to the canonical snake_case name.
- Validate/whitelist keys in your own layer before building the updates map.
- Do not forward arbitrary user input as update keys.
- Note 'wisp' is allowed and renamed to the ephemeral column via updateFieldColumnRename — use the field name, not the column name.
Example fix
// before
camelCase from user input
updates := map[string]any{"externalRef": "JIRA-1"}
// after
canonical field name
updates := map[string]any{"external_ref": "JIRA-1"} Defensive patterns
Strategy: validation
Validate before calling
// mirror the repo allowlist before building the map
var allowed = map[string]bool{
"status": true, "priority": true, "title": true, "assignee": true, "owner": true,
"description": true, "design": true, "acceptance_criteria": true, "notes": true,
"issue_type": true, "estimated_minutes": true, "external_ref": true, "spec_id": true,
"started_at": true, "closed_at": true, "close_reason": true, "closed_by_session": true,
"source_repo": true, "sender": true, "wisp": true, "wisp_type": true,
"no_history": true, "pinned": true, "metadata": true,
}
for k := range updates {
if !allowed[k] { return fmt.Errorf("field %q not updatable", k) }
} Type guard
func validUpdateKeys(updates map[string]any, allowed map[string]struct{}) bool {
for k := range updates {
if _, ok := allowed[k]; !ok { return false }
}
return true
} Try / catch
if err := repo.Update(ctx, id, updates, actor, opts); err != nil {
var bad string
if n, _ := fmt.Sscanf(err.Error(), "db: Update: field %q is not allowed", &bad); n == 1 {
delete(updates, bad) // or rename to the canonical field and retry
}
} Prevention
- Use the canonical snake_case field names from allowedUpdateFields
- Convert camelCase/API names before building the map
- Never forward raw user-supplied keys into updates
- Use 'wisp' (field name), not 'ephemeral' (column name) — renaming is handled by updateFieldColumnRename
When it happens
Trigger: Calling Update with an updates map containing a key not in allowedUpdateFields — typos (e.g. 'acceptance' instead of 'acceptance_criteria'), camelCase keys ('externalRef'), internal-only columns, or keys that only exist on create.
Common situations: Hand-building update maps and misspelling a field; mapping API/JSON field names (camelCase) directly into the updates map; forwarding user-supplied key/value pairs straight into Update; code written against a different backend whose field set differs.
Related errors
- db: Exists: id must not be empty
- db: CountForPrefix: prefix must not be empty
- db: NextCounterID: prefix must not be empty
- remote URL %q does not match any allowed pattern
- database name too long
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/b2c7e55c0b6e8268.
Report an issue: GitHub.