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

  1. Check the key against IsAllowedUpdateField (or the allowlist constant) before adding it to the map.
  2. Fix the spelling/case of the field name.
  3. 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

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


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