gastownhall/beads · error

no Linear client available

Error message

no Linear client available

What it means

fetchIssueAcrossTeams needs a Linear client to resolve an issue identifier when the tracker has at most one team configured. When t.primaryClient() is nil — the tracker was constructed without an API client — there is no way to fetch anything, so it returns this error instead of panicking.

Source

Thrown at internal/linear/parent_reconcile.go:26

// fetchIssueAcrossTeams locates an issue by its Linear identifier across
// all configured team clients. Single-team setups hit the primary client
// directly; multi-team setups fall through each client in order until one
// returns a non-nil issue. Returns (issue, hostClient, nil) on success;
// the hostClient is the team's client that owned the issue and should be
// reused for subsequent mutations (so the update path doesn't re-probe
// and silently fall back to the wrong client on transient probe errors —
// see clientForExternalID's fallback behavior in tracker.go).
//
// Returns (nil, nil, nil) when no team has the issue.
func (t *Tracker) fetchIssueAcrossTeams(ctx context.Context, identifier string) (*Issue, *Client, error) {
	if identifier == "" {
		return nil, nil, nil
	}
	if len(t.teamIDs) <= 1 {
		client := t.primaryClient()
		if client == nil {
			return nil, nil, errors.New("no Linear client available")
		}
		li, err := client.FetchIssueByIdentifier(ctx, identifier)
		if err != nil {
			return nil, nil, err
		}
		if li == nil {
			return nil, nil, nil
		}
		return li, client, nil
	}
	// Multi-team: try each client. First non-nil result wins. Rate-limit
	// errors abort immediately (the cross-team probe shouldn't burn
	// through quota when the circuit breaker has already tripped).
	for _, teamID := range t.teamIDs {
		client := t.clients[teamID]
		if client == nil {
			continue
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Initialize the Tracker with a valid Linear API client (API key from the environment/config) before reconciling
  2. If this instance is not supposed to talk to Linear, skip reconciliation when primaryClient() == nil instead of calling it
  3. For multi-team setups, ensure the per-team clients map is populated so the multi-team path is used

Example fix

// before
tracker := &linear.Tracker{}
stats, err := tracker.ReconcileParents(ctx, links, false)
// after
if tracker.PrimaryClient() == nil {
    return errors.New("linear: cannot reconcile parents without a client; set the API key")
}
stats, err := tracker.ReconcileParents(ctx, links, false)
Defensive patterns

Strategy: type-guard

Validate before calling

if tracker.PrimaryClient() == nil {
    return errors.New("linear client not configured; set the Linear API key")
}

Type guard

func (t *Tracker) HasClient() bool { return t.PrimaryClient() != nil }

Try / catch

li, _, err := fetchIssueAcrossTeams(ctx, tracker, identifier)
if err != nil && err.Error() == "no Linear client available" {
    return nil, fmt.Errorf("linear: configure an API client before fetching %s", identifier)
}

Prevention

When it happens

Trigger: Calling fetchIssueAcrossTeams (via parent-link reconciliation flows) on a Tracker whose primaryClient() returns nil: no Linear API key/client was provided at construction, or the multi-team path was taken with a single teamID and no primary client.

Common situations: Running parent reconciliation in an environment without LINEAR_API_KEY configured; constructing linear.Tracker with zero-value options in tests or partial setups; a config migration dropping the client field.

Related errors


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