gastownhall/beads · error

no known transition path from

Error message

no known transition path from %q to %q for %s: %w

What it means

After a direct state update failed with HTTP 400, transitionWorkItem consults resolveTransitionPath for a known multi-hop path between the current and target state for that work item type. If no path is registered (or current==target makes one impossible), the original 400 is re-wrapped as 'no known transition path from "<from>" to "<to>" for <type>'.

Solutions

  1. Check the target state name for typos and confirm it exists in the ADO process (GetWorkItemStates can list valid states).
  2. Transition manually to a known intermediate state (e.g. Active) or update the transition table in resolveTransitionPath to cover your process.
  3. If using a custom process, map your states to the built-in ones or configure the state graph.
  4. Reproduce the underlying 400 (wrapped as %w) to confirm the direct transition was genuinely invalid.

Example fix

// before: hardcoded paths only for built-in processes
path := resolveTransitionPath(workItemType, currentState, targetState)
// after: validate target state exists before transitioning
states, err := c.GetWorkItemStates(ctx, project, workItemType)
if err == nil && !containsState(states, targetState) {
    return nil, fmt.Errorf("state %q does not exist for %s in this process; valid: %v", targetState, workItemType, states)
}
path := resolveTransitionPath(workItemType, currentState, targetState)
Defensive patterns

Strategy: validation

Validate before calling

states, err := client.GetWorkItemStates(ctx, project, workItemType)
if err != nil { return err }
valid := map[string]bool{}
for _, s := range states { valid[s.Name] = true }
if !valid[targetState] {
    return fmt.Errorf("state %q not valid for %s; valid: %v", targetState, workItemType, states)
}

Type guard

func hasTransitionPath(workItemType, from, to string) bool {
    return len(resolveTransitionPath(workItemType, from, to)) > 0
}

Try / catch

wi, err := client.TransitionWorkItem(ctx, id, target)
if err != nil && strings.Contains(err.Error(), "no known transition path") {
    // Fall back: transition to the default intermediate first, then retry
    if _, err2 := client.TransitionWorkItem(ctx, id, "Active"); err2 == nil {
        _, err = client.TransitionWorkItem(ctx, id, target)
    }
}

Prevention

When it happens

Trigger: Setting a state whose direct transition is rejected with 400 while resolveTransitionPath(workItemType, currentState, targetState) returns an empty slice — i.e. the (currentState, targetState, workItemType) tuple isn't in the hardcoded transition table.

Common situations: Using a state name not present in your ADO process template (typo, renamed column); a custom/inherited process whose state graph differs from the built-in table; transitioning between states on an unexpected work item type (e.g. Bug vs User Story rules).

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at internal/ado/statetransition.go:125

	}

	// 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)