Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Diagnose and fix common Linear API and SDK errors. Use when encountering Linear API errors, debugging integration issues, or troubleshooting authentication, rate limits, or query problems. Trigger: "linear error", "linear API error", "debug linear", "linear not working", "linear 429", "linear authentication error".
.claude/skills/jeremylongshore-linear-common-errors/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 26% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 13% | 0% |
Quick reference for diagnosing and resolving common Linear API and SDK errors. Linear's GraphQL API returns errors in response.errors[] with extensions.type and extensions.userPresentableMessage fields. HTTP 200 responses can still contain partial errors -- always check the errors array.
typescript// Linear GraphQL error shape interface LinearGraphQLResponse { data: Record<string, any> | null; errors?: Array<{ message: string; path?: string[]; extensions: { type: string; // "authentication_error", "forbidden", "ratelimited", etc. userPresentableMessage?: string; }; }>; } // SDK throws these typed errors import { LinearError, InvalidInputLinearError } from "@linear/sdk"; // LinearError includes: .status, .message, .type, .query, .variables // InvalidInputLinearError extends LinearError for mutation input errors
typescript// extensions.type: "authentication_error" // HTTP 401 or error in response.errors // Diagnostic check async function testAuth(): Promise<void> { try { const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! }); const viewer = await client.viewer; console.log(`OK: ${viewer.name} (${viewer.email})`); } catch (error: any) { if (error.message?.includes("Authentication")) { console.error("API key is invalid or expired."); console.error("Fix: Settings > Account > API > Personal API keys"); } throw error; } }
Quick curl diagnostic:
bashcurl -s -X POST https://api.linear.app/graphql \ -H "Authorization: $LINEAR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "{ viewer { id name email } }"}' | jq .
Linear uses the leaky bucket algorithm with two budgets:
typescript// extensions.type: "ratelimited" // HTTP 429 with rate limit headers async function withRetry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error: any) { const isRateLimited = error.status === 429 || error.message?.includes("rate") || error.type === "ratelimited"; if (!isRateLimited || attempt === maxRetries - 1) throw error; const delay = 1000 * Math.pow(2, attempt) + Math.random() * 500; console.warn(`Rate limited (attempt ${attempt + 1}), waiting ${Math.round(delay)}ms`); await new Promise(r => setTimeout(r, delay)); } } throw new Error("Unreachable"); }
Check rate limit status via headers:
typescriptconst resp = await fetch("https://api.linear.app/graphql", { method: "POST", headers: { Authorization: process.env.LINEAR_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ query: "{ viewer { id } }" }), }); console.log("Requests remaining:", resp.headers.get("x-ratelimit-requests-remaining")); console.log("Requests limit:", resp.headers.get("x-ratelimit-requests-limit")); console.log("Requests reset:", resp.headers.get("x-ratelimit-requests-reset")); console.log("Complexity:", resp.headers.get("x-complexity"));
Each property = 0.1 pt, each object = 1 pt, connections multiply children by the first argument (default 50). Max 10,000 pts per query.
typescript// BAD: ~12,500 complexity (250 * 50 labels) const heavy = await client.issues({ first: 250 }); // GOOD: reduce page size and fetch relations separately const light = await client.issues({ first: 50 });
typescript// extensions.type: "not_found" // Cause: deleted, archived, wrong workspace, or insufficient permissions try { const issue = await client.issue("nonexistent-uuid"); } catch (error: any) { if (error.message?.includes("Entity not found")) { console.error("Issue may be deleted, archived, or in another workspace."); console.error("Try: client.issues({ includeArchived: true })"); } }
typescriptimport { InvalidInputLinearError } from "@linear/sdk"; try { await client.createIssue({ teamId: "invalid-uuid", title: "", // Empty title }); } catch (error) { if (error instanceof InvalidInputLinearError) { console.error("Invalid input:", error.message); // error.query and error.variables contain request details } }
typescript// SDK models lazy-load relations -- they can be null const issue = await client.issue("uuid"); // BAD: crashes if unassigned // const name = (await issue.assignee).name; // GOOD: optional chaining const name = (await issue.assignee)?.name ?? "Unassigned"; const projectName = (await issue.project)?.name ?? "No project";
typescript// Happens when LINEAR_WEBHOOK_SECRET doesn't match the webhook config import crypto from "crypto"; function verifyWebhook(payload: string, signature: string, secret: string): boolean { const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex"); try { return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } catch { return false; // Length mismatch } }
| Error | extensions.type | HTTP | Cause | Fix | |-------|----------------|------|-------|-----| | Authentication required | authentication_error | 401 | Invalid/expired key | Regenerate at Settings > API | | Forbidden | forbidden | 403 | Missing OAuth scope | Re-authorize with correct scopes | | Rate limited | ratelimited | 429 | Budget exceeded | Exponential backoff, reduce complexity | | Query complexity too high | query_error | 400 | Deep nesting or large pages | Reduce first, flatten query | | Entity not found | not_found | 200 | Deleted/archived/wrong workspace | Verify ID, try includeArchived | | Validation error | invalid_input | 200 | Bad mutation input | Check field constraints | | Webhook sig mismatch | N/A (local) | N/A | Wrong signing secret | Match LINEAR_WEBHOOK_SECRET |
typescriptimport { LinearError, InvalidInputLinearError } from "@linear/sdk"; async function handleLinearOp<T>(fn: () => Promise<T>): Promise<T> { try { return await fn(); } catch (error) { if (error instanceof InvalidInputLinearError) { console.error(`Input error: ${error.message}`); } else if (error instanceof LinearError) { console.error(`Linear error [${error.status}]: ${error.message}`); if (error.status === 429) { console.error("Rate limited — implement backoff"); } } else { console.error("Unexpected error:", error); } throw error; } }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | pass→pass | 12,267 | 20,116 | +64% | 1 | 1 | 0% | 2,569 | 4,441 | +73% | 0 | 0 | — |
case-01 | fail→fail | 31,277 | 19,662 | -37% | 1 | 1 | 0% | 4,107 | 4,959 | +21% | 0 | 0 | — |
case-02 | fail→pass | 22,379 | 19,228 | -14% | 1 | 1 | 0% | 3,359 | 5,173 | +54% | 0 | 0 | — |
case-03 | pass→pass | 23,298 | 20,170 | -13% | 1 | 1 | 0% | 3,761 | 5,285 | +41% | 0 | 0 | — |
case-04 | fail→pass | 27,508 | 21,627 | -21% | 1 | 1 | 0% | 3,883 | 5,321 | +37% | 0 | 0 | — |
case-05 | pass→pass | 10,162 | 8,892 | -12% | 1 | 1 | 0% | 979 | 2,802 | +186% | 0 | 0 | — |
case-06 | pass→pass | 17,603 | 14,358 | -18% | 1 | 1 | 0% | 2,538 | 4,141 | +63% | 0 | 0 | — |
case-07 | pass→pass | 20,973 | 19,859 | -5% | 1 | 1 | 0% | 2,806 | 4,839 | +72% | 0 | 0 | — |
case-08 | pass→pass | 18,330 | 23,304 | +27% | 1 | 1 | 0% | 2,463 | 4,890 | +99% | 0 | 0 | — |
case-09 | fail→fail | 19,216 | 19,560 | +2% | 1 | 1 | 0% | 2,710 | 4,290 | +58% | 0 | 0 | — |
case-10 | pass→pass | 11,913 | 17,835 | +50% | 1 | 1 | 0% | 2,381 | 4,606 | +93% | 0 | 0 | — |
case-11 | fail→pass | 25,150 | 21,940 | -13% | 1 | 1 | 0% | 3,830 | 4,814 | +26% | 0 | 0 | — |
case-12 | fail→pass | 20,132 | 9,845 | -51% | 1 | 1 | 0% | 2,157 | 2,684 | +24% | 0 | 0 | — |
case-13 | pass→pass | 22,363 | 15,771 | -29% | 1 | 1 | 0% | 2,274 | 4,320 | +90% | 0 | 0 | — |
case-14 | fail→pass | 18,728 | 7,232 | -61% | 1 | 1 | 0% | 2,164 | 2,454 | +13% | 0 | 0 | — |
case-15 | pass→pass | 12,428 | 4,956 | -60% | 1 | 1 | 0% | 2,275 | 3,080 | +35% | 0 | 0 | — |
case-16 | pass→pass | 22,595 | 13,684 | -39% | 1 | 1 | 0% | 2,275 | 3,306 | +45% | 0 | 0 | — |
case-17 | fail→pass | 8,185 | 4,448 | -46% | 1 | 1 | 0% | 1,404 | 2,671 | +90% | 0 | 0 | — |
case-18 | pass→pass | 11,615 | 10,644 | -8% | 1 | 1 | 0% | 1,845 | 3,885 | +111% | 0 | 0 | — |
case-19 | pass→pass | 7,268 | 7,035 | -3% | 1 | 1 | 0% | 1,020 | 2,415 | +137% | 0 | 0 | — |
case-20 | pass→pass | 22,238 | 20,305 | -9% | 1 | 1 | 0% | 2,698 | 5,127 | +90% | 0 | 0 | — |
case-21 | pass→pass | 15,116 | 17,910 | +18% | 1 | 1 | 0% | 2,017 | 4,946 | +145% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.