Documentation for querying the Linear API directly when the MCP tools don't support a specific operation.
Endpoint: https://api.linear.app/graphql
Authentication Header:
Authorization: <API_KEY>
Personal API keys are available in Linear under Security & access settings.
For ad-hoc queries and automation, use the @linear/sdk package with npx and TypeScript.
The skill includes scripts/query.ts for executing GraphQL queries. Run it with:
LINEAR_API_KEY=lin_api_xxx npm run query -- "query { viewer { id name } }"Environment Variable: The script requires LINEAR_API_KEY to be set. If not provided to the Claude process, you cannot execute GraphQL queries automatically.
Get authenticated user:
query Me {
viewer {
id
name
email
}
}Get team issues:
query Team($teamId: String!) {
team(id: $teamId) {
issues {
nodes {
id
title
state { name }
assignee { name }
}
}
}
}Get user's assigned issues:
query MyIssues {
viewer {
assignedIssues {
nodes {
id
title
state { name }
team { key }
}
}
}
}Create issue:
mutation CreateIssue($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue {
id
identifier
title
}
}
}With variables:
{
"input": {
"teamId": "TEAM_ID",
"title": "Issue title",
"description": "Issue description",
"stateId": "STATE_ID",
"projectId": "PROJECT_ID"
}
}Update issue:
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
issueUpdate(id: $id, input: $input) {
success
issue {
id
title
state { name }
}
}
}Look up project by name:
query ProjectByName($filter: ProjectFilter!) {
projects(filter: $filter, first: 10) {
nodes {
id
name
state
slugId
}
}
}With variables:
{
"filter": {
"name": { "containsIgnoreCase": "Phase 6A" }
}
}Monitor HTTP status codes and handle rate limits appropriately. For real-time updates, Linear recommends using webhooks instead of polling.
- Team IDs: Required for most operations involving issues and projects
- State IDs: Issues default to the team's first Backlog state unless specified
- Archived Resources: Hidden by default; use
includeArchived: trueto retrieve - Error Handling: Always check the
errorsarray in responses before assuming success
For more complex automation, you can use the Linear SDK programmatically:
import { LinearClient } from '@linear/sdk';
const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY
});
// Get viewer
const me = await client.viewer;
console.log(me.name);
// Get issues
const issues = await client.issues({
filter: { assignee: { id: { eq: me.id } } }
});
for (const issue of issues.nodes) {
console.log(`${issue.identifier}: ${issue.title}`);
}When operations take longer than expected, use these patterns to maintain reliability.
For bulk operations, notify the user of progress:
const issues = ['PROJ-101', 'PROJ-102', 'PROJ-103'];
for (let i = 0; i < issues.length; i++) {
console.log(`Processing ${i + 1}/${issues.length}: ${issues[i]}`);
// ... operation
}Break large batches into smaller chunks to avoid timeouts:
const BATCH_SIZE = 10;
const DELAY_MS = 150; // Avoid rate limiting
for (let i = 0; i < issues.length; i += BATCH_SIZE) {
const batch = issues.slice(i, i + BATCH_SIZE);
console.log(`Batch ${Math.floor(i / BATCH_SIZE) + 1}: Processing ${batch.length} issues`);
for (const issue of batch) {
await processIssue(issue);
await new Promise(r => setTimeout(r, DELAY_MS));
}
}Detect timeouts and fall back to GraphQL:
try {
// Try MCP first (faster when it works)
await mcp__linear__linear_search_issues({ query: "keyword" });
} catch (error) {
if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) {
console.log('MCP timed out, falling back to GraphQL...');
// Use GraphQL workaround (see below)
}
}Use scripts/sync.ts for reliable bulk state updates:
# Update multiple issues to Done state (replace PROJ with your team prefix)
LINEAR_API_KEY=lin_api_xxx npm run sync -- --issues PROJ-101,PROJ-102,PROJ-103 --state Done
# Preview changes without applying
LINEAR_API_KEY=lin_api_xxx npm run sync -- --issues PROJ-101,PROJ-102 --state Done --dry-run
# Add comment with state change
LINEAR_API_KEY=lin_api_xxx npm run sync -- --issues PROJ-101 --state Done --comment "Completed in PR #42"When MCP times out or fails, use these direct GraphQL patterns.
IMPORTANT: When writing inline Node.js scripts in bash, avoid JavaScript features that confuse shell parsing:
| Feature | Problem | Solution |
|---|---|---|
Optional chaining ?. |
Shell sees ? as glob |
Use explicit null checks |
Nullish coalescing ?? |
Double ? confuses parser |
Use ternary ? : |
Heredocs with ${} |
Shell interpolation | Use << 'EOF' (quoted) |
Anti-Pattern (breaks in bash):
// ❌ Optional chaining breaks shell parsing
const name = project.status?.name;Correct Pattern:
// ✅ Explicit null check works everywhere
const name = project.status ? project.status.name : 'No status';Heredoc Pattern:
# ✅ Use quoted EOF to prevent shell interpolation
node --input-type=module << 'ENDSCRIPT'
const value = obj.prop ? obj.prop.nested : 'default';
ENDSCRIPT// Inline GraphQL via node --experimental-fetch
node --experimental-fetch -e "
async function searchIssues() {
const query = \`
query {
issues(filter: {
team: { key: { eq: \"TEAM\" } }
state: { type: { nin: [\"completed\", \"canceled\"] } }
}, first: 25) {
nodes {
id identifier title state { name } priority
}
}
}
\`;
const res = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': process.env.LINEAR_API_KEY
},
body: JSON.stringify({ query })
});
const data = await res.json();
data.data.issues.nodes.forEach(i => {
console.log(\`\${i.identifier}: \${i.title} [\${i.state.name}]\`);
});
}
searchIssues();
"// First get the workflow state ID for "Done"
const stateQuery = \`
query {
workflowStates(filter: { team: { key: { eq: \"TEAM\" } }, name: { eq: \"Done\" } }) {
nodes { id name }
}
}
\`;
// Then update the issue
const mutation = \`
mutation {
issueUpdate(id: "\${issueUuid}", input: { stateId: "\${doneStateId}" }) {
success
issue { identifier state { name } }
}
}
\`;// Get issue UUID from identifier
const issueQuery = \`
query {
issues(filter: { number: { in: [123, 124, 125] } }) {
nodes { id identifier }
}
}
\`;
// Add comment using UUID
const mutation = \`
mutation {
commentCreate(input: {
issueId: "\${issueUuid}",
body: "Implementation complete. See PR #42."
}) { success }
}
\`;Pro Tip: Store frequently-used IDs (team UUID, common state UUIDs) in your project's CLAUDE.md to avoid repeated lookups.
Use GraphQL introspection to discover the API schema:
LINEAR_API_KEY=lin_api_xxx npm run query -- "{ __schema { types { name description } } }"