gastownhall/beads · error
failed to add work item link: %w
Error message
failed to add work item link: %w
What it means
This error wraps any failure returned by the underlying HTTP layer when AddWorkItemLink PATCHes a JSON-Patch 'add /relations/-' operation to the Azure DevOps work item identified by sourceID. It is a wrapped error (%w), so the original cause — auth failure, 404, network error, invalid relation payload — is preserved and reachable via errors.Is/As. The library throws it to give a single, recognizable message at the 'add link' call boundary.
Source
Thrown at internal/ado/client.go:545
// The comment parameter sets the relation comment attribute; pass "" for no comment.
func (c *Client) AddWorkItemLink(ctx context.Context, sourceID int, targetURL, linkType, comment string) error {
ops := []PatchOperation{
{
Op: "add",
Path: "/relations/-",
Value: map[string]interface{}{
"rel": linkType,
"url": targetURL,
"attributes": map[string]interface{}{
"comment": comment,
},
},
},
}
urlStr := addAPIVersion(fmt.Sprintf("%s/wit/workitems/%d", c.apiBase(), sourceID))
_, err := c.doRequest(ctx, http.MethodPatch, urlStr, "application/json-patch+json", ops)
if err != nil {
return fmt.Errorf("failed to add work item link: %w", err)
}
return nil
}
// RemoveWorkItemLink removes a relation link by index from the given work item.
func (c *Client) RemoveWorkItemLink(ctx context.Context, sourceID, relationIndex int) error {
ops := []PatchOperation{
{
Op: "remove",
Path: fmt.Sprintf("/relations/%d", relationIndex),
},
}
urlStr := addAPIVersion(fmt.Sprintf("%s/wit/workitems/%d", c.apiBase(), sourceID))
_, err := c.doRequest(ctx, http.MethodPatch, urlStr, "application/json-patch+json", ops)
if err != nil {
return fmt.Errorf("failed to remove work item link: %w", err)
}
return nilView on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause with errors.As / %v on the returned error to get ADO's status code and message
- Verify the PAT is valid and has 'Work Items (Read & Write)' scope for the organization
- Confirm sourceID is an existing work item ID in the configured project/org (GET the work item first)
- Check the relation payload: the 'url' attribute must be the full ADO API URL of the target work item
- Verify the client's org/project base URL configuration (apiBase) points at the right organization
Example fix
// before
if err := client.AddWorkItemLink(ctx, 42, rel); err != nil {
return err
}
// after
if err := client.AddWorkItemLink(ctx, 42, rel); err != nil {
var httpErr *ado.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 404 {
return fmt.Errorf("work item 42 not found: %w", err)
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
// preflight: ensure the source work item exists and target URL is well-formed
wi, err := client.GetWorkItem(ctx, sourceID)
if err != nil {
return fmt.Errorf("source work item %d not accessible: %w", sourceID, err)
}
u, err := url.Parse(rel.URL)
if err != nil || !strings.Contains(u.Path, "/_apis/wit/workItems/") {
return fmt.Errorf("invalid relation url %q", rel.URL)
} Type guard
func asHTTPError(err error) (statusCode int, ok bool) {
var he interface{ StatusCode() int }
if errors.As(err, &he) {
return he.StatusCode(), true
}
return 0, false
} Try / catch
if err := client.AddWorkItemLink(ctx, sourceID, rel); err != nil {
var transient interface{ Temporary() bool }
if errors.As(err, &transient) && transient.Temporary() {
// retry with backoff
}
return fmt.Errorf("add link to %d: %w", sourceID, err)
} Prevention
- GET the work item before linking to confirm the source ID exists
- Always pass the fully-qualified ADO API URL of the target work item in the relation
- Keep the PAT scoped to Work Items Read & Write and rotate before expiry
- Treat 429/5xx causes as retryable with exponential backoff
When it happens
Trigger: Calling Client.AddWorkItemLink(ctx, sourceID, ...) when doRequest fails: invalid/expired PAT (401), nonexistent sourceID (404), malformed relation URL/target ID rejected by ADO (400), network/DNS failure, or server 5xx after doRequest retries are exhausted.
Common situations: Linking a work item to another whose ID was mistyped; a PAT that lacks Work Items write scope; org/project URL misconfiguration in the client; ADO returning 400 because the relation 'url' field is not a fully-qualified work item URL.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
- failed to fetch work items: %w
- failed to remove work item link: %w
- failed to list projects: %w
- transient error %d (attempt %d/%d)
- max retries (%d) exceeded: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/2c9f3bb3c313ce0a.
Report an issue: GitHub.