gastownhall/beads · error

add link %s to %d: %w

Error message

add link %s to %d: %w

What it means

During PushLinks, links desired in beads but missing in Azure DevOps are added via AddWorkItemLink with the mapped relation type (rel) and target work item ID. Failures are accumulated and wrapped as 'add link <relationType> to <targetID>'. Like removals, one bad add does not abort the rest of the sync.

Source

Thrown at internal/ado/links.go:329

	for _, idx := range removeIndices {
		if err := r.Client.RemoveWorkItemLink(ctx, workItemID, idx); err != nil {
			errs = append(errs, fmt.Errorf("remove relation %d: %w", idx, err))
		}
	}

	// Find relations to add (in desired but not current).
	for key, dep := range desired {
		if currentSet[key] {
			continue
		}
		targetURL := r.buildWorkItemURL(key.TargetID)
		rel := beadsDepToADORel(dep.Type)
		comment := ""
		if dep.Type == "discovered-from" {
			comment = discoveredFromComment
		}
		if err := r.Client.AddWorkItemLink(ctx, workItemID, targetURL, rel, comment); err != nil {
			errs = append(errs, fmt.Errorf("add link %s to %d: %w", rel, key.TargetID, err))
		}
	}

	return errs
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped inner error: 404 means the target work item doesn't exist — push/mirror the target bead to ADO first, then resync.
  2. Verify PAT permissions include Work Items Write if 401/403.
  3. Confirm beadsDepToADORel maps the dependency type to a relation valid in your ADO process (e.g. System.LinkTypes.Dependency-Forward).
  4. Retry after transient network errors; additions are re-attempted on the next sync since current/desired sets are recomputed.

Example fix

// before: adding links whose targets may not exist in ADO
if err := r.Client.AddWorkItemLink(ctx, workItemID, targetURL, rel, comment); err != nil {
    errs = append(errs, fmt.Errorf("add link %s to %d: %w", rel, key.TargetID, err))
}
// after: verify target exists first
if err := r.Client.AddWorkItemLink(ctx, workItemID, targetURL, rel, comment); err != nil {
    var apiErr *APIError
    if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
        errs = append(errs, fmt.Errorf("add link %s to %d: target work item missing in ADO, push it first", rel, key.TargetID))
        continue
    }
    errs = append(errs, fmt.Errorf("add link %s to %d: %w", rel, key.TargetID, err))
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the target work item exists before adding the link
_, resp, err := client.GetWorkItem(ctx, key.TargetID, nil)
if err != nil || resp == nil || resp.StatusCode == http.StatusNotFound {
    return fmt.Errorf("target work item %d missing in ADO; push it first", key.TargetID)
}

Type guard

func isMissingTargetErr(err error) bool {
    var apiErr *APIError
    return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound
}

Try / catch

if err := r.Client.AddWorkItemLink(ctx, workItemID, targetURL, rel, comment); err != nil {
    var apiErr *APIError
    if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
        log.Printf("defer link %s->%d: target not yet in ADO", rel, key.TargetID)
        continue
    }
    errs = append(errs, fmt.Errorf("add link %s to %d: %w", rel, key.TargetID, err))
}

Prevention

When it happens

Trigger: AddWorkItemLink returns an error: target work item ID (key.TargetID) does not exist in ADO (deleted/not-yet-mirrored), PAT lacks write permission, the relation type is invalid for the work item type, a duplicate/self link is rejected, or a network failure occurs.

Common situations: Beads contains a dependency whose counterpart bead has never been pushed to ADO (targetID has no ADO work item); blocked-by relations on work item types that disallow them; expired PAT; the dependency target was deleted in ADO.

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/428d341926ea240a. Report an issue: GitHub.