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 nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause with errors.As / %v on the returned error to get ADO's status code and message
  2. Verify the PAT is valid and has 'Work Items (Read & Write)' scope for the organization
  3. Confirm sourceID is an existing work item ID in the configured project/org (GET the work item first)
  4. Check the relation payload: the 'url' attribute must be the full ADO API URL of the target work item
  5. 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

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


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