Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Production-grade Linear integration architecture patterns. Use when designing system architecture, choosing integration patterns, or reviewing architectural decisions for Linear integrations. Trigger: "linear architecture", "linear system design", "linear integration patterns", "linear best practices architecture".
.claude/skills/jeremylongshore-linear-reference-architecture/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 33% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 101% | 0% |
Production-grade architectural patterns for Linear integrations. Choose the right pattern based on team size, complexity, and real-time requirements.
| Pattern | Best For | Complexity | Rate Budget | Example | |---------|----------|------------|-------------|---------| | Simple | Single app, small team | Low | < 500 req/hr | Internal dashboard | | Service-Oriented | Multiple apps, shared state | Medium | 500-2,000 req/hr | Platform with Linear sync | | Event-Driven | Real-time needs, many consumers | High | < 500 req/hr + webhooks | Multi-service notification system | | CQRS | Audit trails, complex queries | Very High | Minimal API calls | Compliance-grade tracking |
Direct SDK calls from your application. Best for scripts, internal tools, and prototypes.
typescript// src/linear.ts — single module, shared client import { LinearClient } from "@linear/sdk"; const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! }); // Direct SDK calls from any part of your app export async function getOpenIssues(teamKey: string) { return client.issues({ first: 50, filter: { team: { key: { eq: teamKey } }, state: { type: { nin: ["completed", "canceled"] } }, }, orderBy: "priority", }); } export async function createBugReport(teamId: string, title: string, description: string) { const labels = await client.issueLabels({ filter: { name: { eq: "Bug" } } }); return client.createIssue({ teamId, title, description, priority: 2, labelIds: labels.nodes.length ? [labels.nodes[0].id] : [], }); }
Centralized Linear access through a gateway service with caching and rate limiting.
typescript// src/linear-gateway.ts import { LinearClient } from "@linear/sdk"; class LinearGateway { private client: LinearClient; private cache = new Map<string, { data: any; expiresAt: number }>(); private requestQueue: Array<{ fn: () => Promise<any>; resolve: Function; reject: Function }> = []; private processing = false; constructor(apiKey: string) { this.client = new LinearClient({ apiKey }); } // Cached reads async getTeams() { return this.cachedQuery("teams", () => this.client.teams().then(r => r.nodes), 600); } async getStates(teamId: string) { return this.cachedQuery(`states:${teamId}`, async () => { const team = await this.client.team(teamId); return (await team.states()).nodes; }, 1800); } // Rate-limited writes async createIssue(input: any) { return this.enqueue(() => this.client.createIssue(input)); } async updateIssue(id: string, input: any) { return this.enqueue(() => this.client.updateIssue(id, input)); } // Custom queries through the gateway async rawQuery(query: string, variables?: any) { return this.enqueue(() => this.client.client.rawRequest(query, variables)); } // Cache invalidation (called from webhook handler) invalidate(pattern: string) { for (const key of this.cache.keys()) { if (key.startsWith(pattern)) this.cache.delete(key); } } private async cachedQuery<T>(key: string, fn: () => Promise<T>, ttlSec: number): Promise<T> { const cached = this.cache.get(key); if (cached && Date.now() < cached.expiresAt) return cached.data; const data = await this.enqueue(fn); this.cache.set(key, { data, expiresAt: Date.now() + ttlSec * 1000 }); return data; } private async enqueue<T>(fn: () => Promise<T>): Promise<T> { return new Promise((resolve, reject) => { this.requestQueue.push({ fn, resolve, reject }); if (!this.processing) this.processQueue(); }); } private async processQueue() { this.processing = true; while (this.requestQueue.length > 0) { const { fn, resolve, reject } = this.requestQueue.shift()!; try { resolve(await fn()); } catch (e) { reject(e); } if (this.requestQueue.length > 0) { await new Promise(r => setTimeout(r, 100)); // 10 req/sec max } } this.processing = false; } } export const gateway = new LinearGateway(process.env.LINEAR_API_KEY!);
Webhook-centric architecture. Minimal API calls, real-time processing.
typescript// src/event-processor.ts import express from "express"; import crypto from "crypto"; import { EventEmitter } from "events"; // Internal event bus const bus = new EventEmitter(); // Webhook ingester const app = express(); app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => { const sig = req.headers["linear-signature"] as string; const body = req.body.toString(); const expected = crypto.createHmac("sha256", process.env.LINEAR_WEBHOOK_SECRET!) .update(body).digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).end(); } const event = JSON.parse(body); res.json({ ok: true }); // Emit to internal consumers bus.emit(`${event.type}.${event.action}`, event); bus.emit(event.type, event); bus.emit("*", event); }); // Consumer: Slack notifications bus.on("Issue.update", async (event) => { if (event.updatedFrom?.stateId && event.data.state?.type === "completed") { await notifySlack(`Done: ${event.data.identifier} ${event.data.title}`); } }); // Consumer: Database sync bus.on("Issue", async (event) => { if (event.action === "create") await db.issues.insert(event.data); if (event.action === "update") await db.issues.update(event.data.id, event.data); if (event.action === "remove") await db.issues.softDelete(event.data.id); }); // Consumer: Cache invalidation bus.on("*", (event) => { gateway.invalidate(event.type.toLowerCase()); });
Separate read and write paths. Full local state for complex queries, API for writes.
typescript// Write side: mutations go through Linear API async function createIssue(input: any) { const result = await gateway.createIssue(input); // Local state updated via webhook, not here return result; } // Read side: queries against local database (no API calls) async function getSprintVelocity(teamKey: string, sprints: number) { return db.query(` SELECT c.name, SUM(i.estimate) as velocity FROM cycles c JOIN issues i ON i.cycle_id = c.id AND i.state_type = 'completed' WHERE c.team_key = ? AND c.completed_at IS NOT NULL ORDER BY c.completed_at DESC LIMIT ? `, [teamKey, sprints]); } // Sync: webhook events keep local state fresh // Full sync: daily consistency check (see linear-data-handling)
src/
linear/
gateway.ts # Rate-limited, cached API access
webhook-handler.ts # Signature verification + routing
event-bus.ts # Internal event distribution
cache.ts # TTL cache with invalidation
services/
issue-service.ts # Business logic
sync-service.ts # Data synchronization
config/
linear.ts # Environment config + validation| Error | Cause | Solution | |-------|-------|----------| | Rate limit exceeded | Too many direct API calls | Route all calls through gateway | | Stale cache | TTL too long, missed webhook | Webhook invalidation + periodic full sync | | Event loss | Webhook delivery failure | Idempotent handlers + consistency checks | | Schema drift | SDK version mismatch | Pin version, test upgrades in staging |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 26,446 | 24,582 | -7% | 1 | 1 | 0% | 4,625 | 6,493 | +40% | 0 | 0 | — |
case-02 | fail→fail | 24,342 | 21,012 | -14% | 1 | 1 | 0% | 3,796 | 5,587 | +47% | 0 | 0 | — |
case-03 | pass→pass | 23,847 | 17,606 | -26% | 1 | 1 | 0% | 3,026 | 4,393 | +45% | 0 | 0 | — |
case-04 | pass→pass | 22,177 | 17,254 | -22% | 1 | 1 | 0% | 2,583 | 4,296 | +66% | 0 | 0 | — |
case-05 | fail→pass | 18,755 | 13,866 | -26% | 1 | 1 | 0% | 2,404 | 4,018 | +67% | 0 | 0 | — |
case-06 | pass→pass | 19,200 | 14,909 | -22% | 1 | 1 | 0% | 2,741 | 4,157 | +52% | 0 | 0 | — |
case-07 | pass→pass | 28,456 | 16,212 | -43% | 1 | 1 | 0% | 3,632 | 4,310 | +19% | 0 | 0 | — |
case-08 | fail→pass | 17,577 | 10,904 | -38% | 1 | 1 | 0% | 2,304 | 3,074 | +33% | 0 | 0 | — |
case-20 | pass→pass | 11,413 | 18,739 | +64% | 1 | 1 | 0% | 1,785 | 4,315 | +142% | 0 | 0 | — |
case-09 | pass→pass | 8,291 | 17,341 | +109% | 1 | 1 | 0% | 1,572 | 3,953 | +151% | 0 | 0 | — |
case-10 | fail→pass | 20,815 | 20,267 | -3% | 1 | 1 | 0% | 3,102 | 5,067 | +63% | 0 | 0 | — |
case-11 | pass→pass | 22,124 | 20,854 | -6% | 1 | 1 | 0% | 3,131 | 4,561 | +46% | 0 | 0 | — |
case-12 | fail→pass | 25,425 | 16,097 | -37% | 1 | 1 | 0% | 3,013 | 5,553 | +84% | 0 | 0 | — |
case-13 | pass→pass | 18,407 | 16,639 | -10% | 1 | 1 | 0% | 3,153 | 4,272 | +35% | 0 | 0 | — |
case-14 | fail→fail | 18,932 | 17,175 | -9% | 1 | 1 | 0% | 3,211 | 4,905 | +53% | 0 | 0 | — |
case-15 | fail→fail | 14,821 | 9,729 | -34% | 1 | 1 | 0% | 1,528 | 3,599 | +136% | 0 | 0 | — |
case-16 | pass→pass | 15,506 | 24,832 | +60% | 1 | 1 | 0% | 2,270 | 5,647 | +149% | 0 | 0 | — |
case-17 | pass→pass | 20,049 | 24,493 | +22% | 1 | 1 | 0% | 2,335 | 4,974 | +113% | 0 | 0 | — |
case-18 | fail→pass | 10,479 | 6,372 | -39% | 1 | 1 | 0% | 1,673 | 3,364 | +101% | 0 | 0 | — |
case-19 | fail→pass | 25,424 | 22,321 | -12% | 1 | 1 | 0% | 3,197 | 5,653 | +77% | 0 | 0 | — |
case-21 | pass→pass | 15,395 | 16,198 | +5% | 1 | 1 | 0% | 2,095 | 4,071 | +94% | 0 | 0 | — |
case-22 | fail→fail | 15,844 | 20,858 | +32% | 1 | 1 | 0% | 1,740 | 4,811 | +176% | 0 | 0 | — |
case-23 | pass→pass | 18,934 | 13,718 | -28% | 1 | 1 | 0% | 3,091 | 4,036 | +31% | 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. 23 cases were attempted. The headline lift of +26 percentage points is the difference between those two pass rates over the 23 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.