gastownhall/beads · error

linear.state_map type fallback is ambiguous for beads status

Error message

linear.state_map type fallback is ambiguous for beads status %q across Linear states: %s. Set linear.outbound_state_map.%s = "<state name>" to disambiguate

What it means

No state name matched, so the resolver fell back to matching by Linear state type (via linear.state_map.<type> entries), but multiple Linear states share that type (e.g. 'In Progress' and 'In Review' are both 'started'). The error lists the candidates and tells the user exactly how to disambiguate with an outbound_state_map override.

Source

Thrown at internal/linear/mapping.go:431

		return "", fmt.Errorf("linear.state_map maps beads status %q to multiple Linear states: %s", status, strings.Join(names, ", "))
	}

	var typeMatches []State
	for _, state := range cache.States {
		mapped, ok := config.ExplicitStateMap[strings.ToLower(strings.TrimSpace(state.Type))]
		if ok && stateMapMatchesStatus(mapped, status) {
			typeMatches = append(typeMatches, state)
		}
	}
	if len(typeMatches) == 1 {
		return typeMatches[0].ID, nil
	}
	if len(typeMatches) > 1 {
		names := make([]string, 0, len(typeMatches))
		for _, state := range typeMatches {
			names = append(names, state.Name)
		}
		return "", fmt.Errorf("linear.state_map type fallback is ambiguous for beads status %q across Linear states: %s. Set linear.outbound_state_map.%s = \"<state name>\" to disambiguate", status, strings.Join(names, ", "), status)
	}

	return "", fmt.Errorf("linear.state_map has no configured Linear state for beads status %q", status)
}

// ParseBeadsStatus converts a status string to types.Status.
func ParseBeadsStatus(s string) types.Status {
	switch strings.ToLower(s) {
	case "open":
		return types.StatusOpen
	case "in_progress", "in-progress", "inprogress":
		return types.StatusInProgress
	case "blocked":
		return types.StatusBlocked
	case "closed", "done":
		return types.StatusClosed
	case "deferred":
		return types.StatusDeferred

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set linear.outbound_state_map.<status> = "<state name>" to name the exact push target, as the error message instructs.
  2. Add a name-level linear.state_map entry for one specific state instead of the shared type.
  3. Restructure the Linear workflow so the target status is unique per type if you control the workflow.
  4. Re-run 'bd linear link' to generate an unambiguous mapping.

Example fix

// before
[linear.state_map]
started = "in_progress"   # matches both 'In Progress' and 'In Review'
// after
[linear.state_map]
started = "in_progress"
[linear.outbound_state_map]
in_progress = "In Progress"
Defensive patterns

Strategy: fallback

Validate before calling

func needsOutboundOverride(cfg *linear.MappingConfig, cache *linear.StateCache) bool {
	counts := map[string]int{}
	for _, s := range cache.States {
		if _, ok := cfg.ExplicitStateMap[strings.ToLower(strings.TrimSpace(s.Type))]; ok {
			counts[strings.ToLower(s.Type)]++
		}
	}
	for t, n := range counts {
		if n > 1 {
			fmt.Printf("type %q matches %d states; configure linear.outbound_state_map\n", t, n)
			return true
		}
	}
	return false
}

Try / catch

stateID, err := linear.ResolveStateIDForBeadsStatus(cache, status, cfg)
if err != nil && strings.Contains(err.Error(), "Set linear.outbound_state_map") {
	return fmt.Errorf("push blocked; %v", err) // surface the built-in remediation hint
}

Prevention

When it happens

Trigger: Calling ResolveStateIDForBeadsStatus where the explicit state-name pass found nothing, but the type-fallback pass matched 2+ states whose linear.state_map.<state-type> entry matches the status.

Common situations: Workspace with custom workflow states sharing a type ('Started': 'In Progress', 'Code Review', 'QA'); defaults relying on type matching for pull being reused for push.

Related errors


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