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

  1. 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.
  2. Validate/whitelist keys in your own layer before building the updates map.
  3. Do not forward arbitrary user input as update keys.
  4. 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

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


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