gastownhall/beads · error

transitioning to %q: %w

Error message

transitioning to %q: %w

What it means

transitionWorkItem attempted to set a work item's state directly via UpdateWorkItem and the call failed with an error that is not an APIError 400 (Bad Request). Since only 400s are eligible for multi-hop transition walking, any other failure (auth, network, 404, 500) is wrapped as 'transitioning to "<targetState>"'.

Source

Thrown at internal/ado/statetransition.go:120

// direct state update; if that fails with a 400 error, it walks through the
// known transition path for the work item type.
func (c *Client) transitionWorkItem(ctx context.Context, workItemID int, workItemType, currentState, targetState string) (*WorkItem, error) {
	if currentState == targetState {
		return nil, nil
	}

	// Try direct transition first.
	fields := map[string]interface{}{FieldState: targetState}
	wi, err := c.UpdateWorkItem(ctx, workItemID, fields)
	if err == nil {
		return wi, nil
	}

	// If direct transition failed with a 400 Bad Request, try walking
	// through intermediate states. Any other error is a real failure.
	var apiErr *APIError
	if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusBadRequest {
		return nil, fmt.Errorf("transitioning to %q: %w", targetState, err)
	}

	path := resolveTransitionPath(workItemType, currentState, targetState)
	if len(path) == 0 {
		return nil, fmt.Errorf("no known transition path from %q to %q for %s: %w",
			currentState, targetState, workItemType, err)
	}

	// Walk through intermediate states.
	var lastWI *WorkItem
	for _, intermediate := range path {
		fields := map[string]interface{}{FieldState: intermediate}
		lastWI, err = c.UpdateWorkItem(ctx, workItemID, fields)
		if err != nil {
			return nil, fmt.Errorf("transitioning to intermediate state %q: %w", intermediate, err)
		}
	}
	return lastWI, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap the inner error and check the HTTP status: fix PAT permissions for 401/403.
  2. Confirm the work item still exists (404) — it may have been deleted in ADO.
  3. For 409 conflicts, refresh the work item and retry the transition.
  4. Retry after transient 5xx/network failures; the transition is a single idempotent field update.

Example fix

// before: retrying blindly on any failure
wi, err := c.transitionWorkItem(ctx, id, "Closed")
// after: only retry transient failures
wi, err := c.transitionWorkItem(ctx, id, "Closed")
if err != nil {
    var apiErr *APIError
    if errors.As(err, &apiErr) && apiErr.StatusCode >= 500 {
        time.Sleep(2 * time.Second)
        wi, err = c.transitionWorkItem(ctx, id, "Closed")
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the work item exists and is writable before transitioning
wi, resp, err := client.GetWorkItem(ctx, id, nil)
if err != nil || resp == nil || resp.StatusCode != http.StatusOK {
    return fmt.Errorf("work item %d not accessible: %v", id, err)
}
_ = wi

Type guard

func isRetryableTransitionErr(err error) bool {
    var apiErr *APIError
    if !errors.As(err, &apiErr) { return false }
    return apiErr.StatusCode >= 500 || apiErr.StatusCode == http.StatusTooManyRequests
}

Try / catch

wi, err := client.TransitionWorkItem(ctx, id, "Closed")
if err != nil {
    var apiErr *APIError
    if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusUnauthorized {
        return fmt.Errorf("check ado.pat permissions: %w", err)
    }
    if isRetryableTransitionErr(err) {
        return retryWithBackoff(func() error {
            _, err = client.TransitionWorkItem(ctx, id, "Closed")
            return err
        })
    }
    return err
}

Prevention

When it happens

Trigger: Calling a state transition when UpdateWorkItem fails with 401/403 (permissions or revoked PAT), 404 (work item ID doesn't exist), 500/503 (ADO outage), network timeout, or a non-400 APIError like 409 conflict.

Common situations: PAT lacks write access; the work item was deleted between lookup and update; ADO is temporarily unavailable; stale work item revision causing a 409 conflict.

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