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 コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/jeremylongshore/claude-code-plugins-plus-skills --skill linear-sdk-patternsコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... このリポジトリの他の Skills Implement user sign-up and sign-in flows with Clerk.
Use when building authentication UI, customizing sign-in experience,
or implementing OAuth social login.
Trigger with phrases like "clerk sign-in", "clerk sign-up",
"clerk login flow", "clerk OAuth", "clerk social login".
Implement session management and middleware with Clerk.
Use when managing user sessions, configuring route protection,
or implementing token refresh and custom JWT templates.
Trigger with phrases like "clerk session", "clerk middleware",
"clerk route protection", "clerk token", "clerk JWT".
Configure enterprise SSO, role-based access control, and organization management.
Use when implementing SSO integration, configuring role-based permissions,
or setting up organization-level controls.
Trigger with phrases like "clerk SSO", "clerk RBAC",
"clerk enterprise", "clerk roles", "clerk permissions", "clerk organizations".
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