gastownhall/beads · error

cannot extract work item ID from URL: %s

Error message

cannot extract work item ID from URL: %s

What it means

extractWorkItemID parses a numeric work item ID out of an Azure DevOps work item URL using the pattern /(\d+)(?:\?|$). If the URL does not end (before an optional query string) in a numeric segment, no ID can be extracted and this error is returned. It protects link-sync code from fabricating dependency edges with bogus IDs.

Source

Thrown at internal/ado/links.go:33

// and ADO work item relations.
type LinkResolver struct {
	Client *Client
}

// NewLinkResolver creates a new LinkResolver with the given client.
func NewLinkResolver(client *Client) *LinkResolver {
	return &LinkResolver{Client: client}
}

// workItemIDPattern extracts a work item ID from an ADO API URL.
// Handles URLs with query parameters (e.g. ?api-version=7.1).
var workItemIDPattern = regexp.MustCompile(`/(\d+)(?:\?|$)`)

// extractWorkItemID extracts the numeric ID from an ADO work item API URL.
func extractWorkItemID(url string) (int, error) {
	matches := workItemIDPattern.FindStringSubmatch(url)
	if len(matches) < 2 {
		return 0, fmt.Errorf("cannot extract work item ID from URL: %s", url)
	}
	id, err := strconv.Atoi(matches[1])
	if err != nil {
		return 0, fmt.Errorf("invalid work item ID in URL %s: %w", url, err)
	}
	return id, nil
}

// isLinkRelation checks if a relation type is a work item link (vs attachment, etc).
func isLinkRelation(rel string) bool {
	return strings.HasPrefix(rel, "System.LinkTypes.")
}

// discoveredFromComment is the marker attribute used to identify discovered-from links
// stored as ADO Related relations.
const discoveredFromComment = "beads:discovered-from"

// adoRelToBeadsDep maps an ADO relation type to a beads dependency type.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the relation URL in the error and confirm it ends in a numeric work item ID (optionally followed by a query string).
  2. Fix or remove the malformed relation on the ADO work item, then re-run sync.
  3. If the URL comes from another tool, normalize it to the ADO API form .../_apis/wit/workItems/<id> before passing it in.
  4. Pre-filter relations (e.g. with isLinkRelation / URL validation) so non-work-item links are skipped instead of parsed.

Example fix

// before: parsing every relation URL blindly
id, err := extractWorkItemID(rel.URL)
// after: skip URLs that aren't API work item URLs
if !strings.Contains(rel.URL, "/_apis/wit/workItems/") {
    continue // not a work item link
}
id, err := extractWorkItemID(rel.URL)
Defensive patterns

Strategy: validation

Validate before calling

var workItemURLRe = regexp.MustCompile(`/\d+(?:\?|$)`)
func isParseableWorkItemURL(u string) bool { return workItemURLRe.MatchString(u) }

Type guard

func extractWorkItemIDSafe(url string) (int, bool) {
    id, err := extractWorkItemID(url)
    return id, err == nil
}

Try / catch

id, err := extractWorkItemID(rel.URL)
if err != nil {
    log.Printf("skipping unparseable relation URL %q: %v", rel.URL, err)
    continue
}

Prevention

When it happens

Trigger: Passing ExtractLinkDeps or PushLinks a relation URL that does not end in /<digits> or /<digits>?query — e.g. a truncated URL, an HTML work-item UI URL like .../_workitems/edit/123 with extra path segments, or an empty/garbage url field.

Common situations: Hand-crafted relations in ADO pointing at non-work-item artifacts; URLs with trailing slashes; copying browser URLs instead of API URLs; importing data from another tracker where relation.target.url points elsewhere.

Related errors


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