gastownhall/beads · error
%s must be a string, got %T
Error message
%s must be a string, got %T
What it means
The value provided for the append-notes operation is not a string. OpAppendNotes appends text to the issue's existing notes, so its value must be a Go string; any other type is rejected with this type-error message that reports the actual type received.
Source
Thrown at internal/storage/issueops/update.go:975
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
}
return json.RawMessage(result), nil
}
// resolveNotesAppendOp folds OpAppendNotes into a concrete "notes" value on
// resolved, appending to oldIssue.Notes (read in the same mutation transaction).
// It is a no-op when the append op is absent.
func resolveNotesAppendOp(oldIssue *types.Issue, updates, resolved map[string]interface{}) error {
raw, ok := updates[OpAppendNotes]
if !ok {
return nil
}
if _, direct := resolved["notes"]; direct {
return fmt.Errorf("%w: cannot combine a notes replacement with %s", storage.ErrValidation, OpAppendNotes)
}
text, ok := raw.(string)
if !ok {
return fmt.Errorf("%s must be a string, got %T", OpAppendNotes, raw)
}
combined := oldIssue.Notes
if combined != "" {
combined += "\n"
}
combined += text
resolved["notes"] = combined
return nil
}
// mergeOpStrings coerces a merge-operation value to []string. Accepts
// []interface{} of strings as well, so operation maps survive a JSON
// round-trip (e.g. daemon transports).
func mergeOpStrings(op string, value interface{}, present bool) ([]string, error) {
if !present {
return nil, nil
}
switch v := value.(type) {View on GitHub (pinned to 71377f2769)
Solutions
- Convert the value to a string before inserting: fmt.Sprintf, strconv, or explicit dereference of *string
- Use a typed update builder that enforces string for append-notes
- Check the reported %T in the message to see which type actually arrived
Example fix
// before
var note *string = getNote()
updates[issueops.OpAppendNotes] = note // *string, not string
// after
if note != nil {
updates[issueops.OpAppendNotes] = *note // plain string
} Defensive patterns
Strategy: type-guard
Validate before calling
if v, ok := updates[issueops.OpAppendNotes]; ok {
if _, isStr := v.(string); !isStr {
return fmt.Errorf("OpAppendNotes must be a string, got %T", v)
}
} Type guard
func isString(v any) bool { _, ok := v.(string); return ok } Try / catch
if err := issueops.ResolveMergeOps(issue, updates, resolved); err != nil {
if strings.Contains(err.Error(), "must be a string, got") {
return fmt.Errorf("coerce the append-notes value to string before retrying: %w", err)
}
} Prevention
- Dereference *string values before inserting into the updates map
- Use typed builders instead of map[string]interface{} for updates
- Check the %T in the error message to identify the offending type
When it happens
Trigger: Passing a non-string (int, []string, fmt.Stringer, *string, etc.) as the OpAppendNotes value in an update map passed to ResolveMergeOps.
Common situations: Building updates with a generic map[string]interface{} where a helper inserts the wrong type, passing a *string instead of dereferencing it, or deserializing a JSON number/bool into the append field.
Understand the failure class
Background: "Wrong argument type", "must be a string", "expected Array or Prism::Scope": TypeError and ArgumentError when a library receives a value of the wrong type — this error's family across 28 libraries.
Related errors
- %w: cannot combine a notes replacement with %s
- update: notes and append notes cannot both be set
- no store is open for this workspace
- not found
- no absolute native user directory is available
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/1be72c355fecb27b.
Report an issue: GitHub.