Skip to main content
linear-sdk-patterns TypeScript/JavaScript SDK patterns and best practices for Linear.
Use when learning SDK idioms, implementing pagination,
filtering, relation loading, or custom GraphQL queries.
Trigger: "linear SDK patterns", "linear best practices",
"linear typescript", "linear API patterns", "linear pagination".
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/jeremylongshore/claude-code-plugins-plus-skills --skill linear-sdk-patterns命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... jeremylongshore
jeremylongshore/claude-code-plugins-plus-skills
打开 GitHub 仓库 name linear-sdk-patterns description TypeScript/JavaScript SDK patterns and best practices for Linear.
Use when learning SDK idioms, implementing pagination,
filtering, relation loading, or custom GraphQL queries.
Trigger: "linear SDK patterns", "linear best practices",
"linear typescript", "linear API patterns", "linear pagination".
allowed-tools Read, Write, Edit, Grep version 1.12.0 license MIT author Jeremy Longshore <jeremy@intentsolutions.io> tags ["saas","linear","api","typescript"] compatibility Designed for Claude Code, also compatible with Codex and OpenClaw
Linear SDK Patterns
Overview
Production patterns for @linear/sdk. The SDK wraps Linear's GraphQL API with strongly-typed models, cursor-based pagination (fetchNext()/fetchPrevious()), lazy-loaded relations, and typed error classes. Understanding these patterns avoids N+1 queries and rate limit waste.
Prerequisites
@linear/sdk installed
TypeScript project with strict: true
Understanding of async/await and GraphQL concepts
Instructions
Pattern 1: Client Singleton
import { LinearClient } from "@linear/sdk" ;
let _client : LinearClient | null = null ;
export function getLinearClient ( ): LinearClient {
if (!_client) {
const apiKey = process.env .LINEAR_API_KEY ;
if (!apiKey) throw new Error ("LINEAR_API_KEY is required" );
_client = new LinearClient ({ apiKey });
}
return _client;
}
const clientCache = new Map <string , LinearClient >();
export function getClientForUser (userId : string , : ): {
(!clientCache. (userId)) {
clientCache. (userId, ({ accessToken }));
}
clientCache. (userId)!;
}
accessToken
string
LinearClient
if
has
set
new
LinearClient
return
get
Pattern 2: Cursor-Based Pagination Linear uses Relay-style cursor pagination. The SDK provides fetchNext() and fetchPrevious() helpers, plus raw pageInfo for manual control.
const firstPage = await client.issues ({ first : 50 });
console .log (`Page 1: ${firstPage.nodes.length} issues` );
if (firstPage.pageInfo .hasNextPage ) {
const secondPage = await firstPage.fetchNext ();
console .log (`Page 2: ${secondPage.nodes.length} issues` );
}
async function * paginateAll<T>(
fetchPage : (cursor ?: string ) => Promise <{
nodes : T[];
pageInfo : { hasNextPage : boolean ; endCursor : string };
}>
): AsyncGenerator <T> {
let cursor : string | undefined ;
let hasNext = true ;
while (hasNext) {
const page = await fetchPage (cursor);
for (const node of page.nodes ) yield node;
hasNext = page.pageInfo .hasNextPage ;
cursor = page.pageInfo .endCursor ;
}
}
for await (const issue of paginateAll (c => client.issues ({ first : 50 , after : c }))) {
console .log (`${issue.identifier} : ${issue.title} ` );
}
Pattern 3: Relation Loading (Avoiding N+1) SDK models lazy-load relations. Accessing .assignee triggers a separate API call. Use raw GraphQL to batch-fetch relations in one request.
const issues = await client.issues ({ first : 50 });
for (const issue of issues.nodes ) {
const assignee = await issue.assignee ;
console .log (`${issue.identifier} : ${assignee?.name} ` );
}
const response = await client.client .rawRequest (`
query TeamIssues($teamKey: String!) {
issues(first: 50, filter: { team: { key: { eq: $teamKey } } }) {
nodes {
id identifier title priority
assignee { name email }
state { name type }
labels { nodes { name color } }
project { name }
}
}
}
` , { teamKey : "ENG" });
async function enrichIssue (issue : any ) {
const [assignee, state, team, labels] = await Promise .all ([
issue.assignee ,
issue.state ,
issue.team ,
issue.labels (),
]);
return { ...issue, _assignee : assignee, _state : state, _team : team, _labels : labels.nodes };
}
Pattern 4: Filtering with Comparators Linear supports eq, neq, in, nin, lt, lte, gt, gte, startsWith, contains, and logical and/or operators.
const bugs = await client.issues ({
first : 50 ,
filter : {
priority : { lte : 2 },
state : { type : { nin : ["completed" , "canceled" ] } },
labels : { name : { eq : "Bug" } },
team : { key : { eq : "ENG" } },
},
});
const filtered = await client.issues ({
filter : {
or : [
{ assignee : { email : { eq : "alice@company.com" } } },
{ assignee : { email : { eq : "bob@company.com" } } },
],
state : { type : { eq : "started" } },
},
});
const results = await client.issueSearch ("authentication bug" );
const recent = await client.issues ({
filter : {
updatedAt : { gte : new Date (Date .now () - 24 * 60 * 60 * 1000 ).toISOString () },
},
orderBy : "updatedAt" ,
first : 100 ,
});
Pattern 5: Type-Safe Error Handling import { LinearError , InvalidInputLinearError } from "@linear/sdk" ;
type Result <T> = { ok : true ; data : T } | { ok : false ; error : string ; retryable : boolean };
async function safeCall<T>(fn : () => Promise <T>): Promise <Result <T>> {
try {
return { ok : true , data : await fn () };
} catch (error) {
if (error instanceof InvalidInputLinearError ) {
return { ok : false , error : `Invalid input: ${error.message} ` , retryable : false };
}
if (error instanceof LinearError ) {
const retryable = error.status === 429 || error.status === 503 ;
return { ok : false , error : `[${error.status} ] ${error.message} ` , retryable };
}
return { ok : false , error : String (error), retryable : false };
}
}
const result = await safeCall (() => client.issue ("issue-uuid" ));
if (result.ok ) {
console .log (result.data .title );
} else if (result.retryable ) {
console .warn ("Transient error, retry:" , result.error );
}
Pattern 6: Custom GraphQL Client Access the underlying LinearGraphQLClient for full control.
const graphQLClient = client.client ;
graphQLClient.setHeader ("X-Request-Id" , crypto.randomUUID ());
const data = await graphQLClient.rawRequest (`
query Cycle($id: String!) {
cycle(id: $id) {
id name startsAt endsAt
issues { nodes { identifier title state { name } } }
}
}
` , { id : "cycle-uuid" });
const batchResult = await graphQLClient.rawRequest (`
mutation BatchUpdate {
a: issueUpdate(id: "id1", input: { priority: 1 }) { success }
b: issueUpdate(id: "id2", input: { priority: 1 }) { success }
c: issueUpdate(id: "id3", input: { priority: 1 }) { success }
}
` );
Error Handling Error Cause Solution Cannot read properties of nullNullable relation not checked Use (await issue.assignee)?.name Type is not assignableSDK/TypeScript version mismatch Update @linear/sdk to latest Promise rejection unhandledMissing try/catch on async Wrap in safeCall() or .catch() Query complexity too highToo many nested relations Use rawRequest() with flat field selection
Examples
Create Issue with Full Metadata const teams = await client.teams ();
const eng = teams.nodes .find (t => t.key === "ENG" )!;
const states = await eng.states ();
const todo = states.nodes .find (s => s.type === "unstarted" )!;
const labels = await client.issueLabels ({ filter : { name : { eq : "Bug" } } });
await client.createIssue ({
teamId : eng.id ,
title : "Login page crashes on Safari" ,
description : "## Steps to reproduce\n1. Open login in Safari 17\n2. Click Sign in\n3. Crash" ,
stateId : todo.id ,
priority : 1 ,
labelIds : [labels.nodes [0 ].id ],
estimate : 3 ,
});
Resources