
Linear
- 1 installs
- 327 repo stars
- Updated May 25, 2026
- letta-ai/lettabot
This is a copy of linear by letta-ai - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
linear is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- linear
- AI & Agent Building
- AI-coding skill
Linear by the numbers
- 1 all-time installs (skills.sh)
- Data as of Jul 16, 2026 (Skillselion catalog sync)
npx skills add https://github.com/letta-ai/lettabot --skill linearAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 327 |
| Last updated | May 25, 2026 |
| Repository | letta-ai/lettabot ↗ |
What it does
Helps with ai & agent building tasks.
Files
Linear
Interact with Linear's GraphQL API to manage issues.
When to Use
- User asks about Linear issues, tickets, or project management
- Need to triage, prioritize, or update issue status
- Want to search or comment on issues
- Managing a backlog or sprint
Setup
Requires LINEAR_API_KEY environment variable. Get one from Linear Settings > API > Personal API keys.
Quick Operations
List issues by status
npx tsx scripts/linear.ts list --state "Triage"
npx tsx scripts/linear.ts list --state "In Progress"
npx tsx scripts/linear.ts list --state "Backlog"List issues assigned to someone
npx tsx scripts/linear.ts list --assignee "cameron"Get issue details
npx tsx scripts/linear.ts get <issue-id>Update issue priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
npx tsx scripts/linear.ts update <issue-id> --priority 2Update issue state
npx tsx scripts/linear.ts update <issue-id> --state "In Progress"Add comment
npx tsx scripts/linear.ts comment <issue-id> "Your comment here"Search issues
npx tsx scripts/linear.ts search "search query"Triage Workflow
1. List triage issues: list --state "Triage" 2. Review each issue, decide priority 3. Update priority and move to appropriate state 4. Add comments for context if needed
Output Format
All commands output JSON for easy parsing. Use jq for filtering if needed.
Battle-Tested Insights
GraphQL Query Patterns
- Linear uses GraphQL with nested filtering. State filters need the exact format:
state: { name: { eq: "State Name" } } - Assignee filters are case-insensitive with
containsIgnoreCase - When updating state, you need to fetch the state ID first - state names alone won't work in mutations
Common Pitfalls
- Issue IDs vs Identifiers: The API accepts both UUID-style IDs and human-readable identifiers (e.g., "ENG-123"), but some endpoints prefer one over the other
- Priority is numeric (0-4), not a string
- Rate limits are generous but exist - batch operations if doing bulk updates
#!/usr/bin/env npx tsx
/**
* Linear CLI for triage workflows
* Usage: npx tsx linear.ts <command> [options]
*/
const LINEAR_API = "https://api.linear.app/graphql";
const API_KEY = process.env.LINEAR_API_KEY;
// Type definitions
interface GraphQLError {
message: string;
}
interface WorkflowState {
id: string;
name: string;
}
interface IssueUpdateInput {
priority?: number;
stateId?: string;
}
if (!API_KEY) {
console.error("Error: LINEAR_API_KEY environment variable not set");
process.exit(1);
}
async function graphql(query: string, variables: Record<string, unknown> = {}) {
const res = await fetch(LINEAR_API, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": API_KEY!,
},
body: JSON.stringify({ query, variables }),
});
const data = await res.json();
if (data.errors) {
throw new Error(data.errors.map((e: GraphQLError) => e.message).join(", "));
}
return data.data;
}
async function listIssues(filters: { state?: string; assignee?: string; limit?: number }) {
const limit = filters.limit || 50;
let filterStr = "";
const filterParts: string[] = [];
if (filters.state) {
filterParts.push(`state: { name: { eq: "${filters.state}" } }`);
}
if (filters.assignee) {
filterParts.push(`assignee: { name: { containsIgnoreCase: "${filters.assignee}" } }`);
}
if (filterParts.length > 0) {
filterStr = `filter: { ${filterParts.join(", ")} }`;
}
const query = `
query {
issues(first: ${limit}, ${filterStr}) {
nodes {
id
identifier
title
priority
state { name }
assignee { name }
createdAt
url
}
}
}
`;
const data = await graphql(query);
return data.issues.nodes;
}
async function getIssue(idOrIdentifier: string) {
// Try by identifier first (e.g., "ENG-123")
const query = `
query($id: String!) {
issue(id: $id) {
id
identifier
title
description
priority
state { name }
assignee { name }
labels { nodes { name } }
createdAt
updatedAt
url
comments {
nodes {
body
user { name }
createdAt
}
}
}
}
`;
try {
const data = await graphql(query, { id: idOrIdentifier });
return data.issue;
} catch {
// Try searching by identifier
const searchQuery = `
query($filter: IssueFilter) {
issues(filter: $filter, first: 1) {
nodes {
id
identifier
title
description
priority
state { name }
assignee { name }
labels { nodes { name } }
createdAt
updatedAt
url
}
}
}
`;
const data = await graphql(searchQuery, {
filter: { number: { eq: parseInt(idOrIdentifier.replace(/\D/g, "")) } }
});
return data.issues.nodes[0];
}
}
async function updateIssue(id: string, updates: { priority?: number; state?: string }) {
// First get the issue to find its ID
const issue = await getIssue(id);
if (!issue) {
throw new Error(`Issue not found: ${id}`);
}
const input: IssueUpdateInput = {};
if (updates.priority !== undefined) {
input.priority = updates.priority;
}
if (updates.state) {
// Need to find the state ID
const statesQuery = `
query {
workflowStates {
nodes {
id
name
}
}
}
`;
const statesData = await graphql(statesQuery);
const state = statesData.workflowStates.nodes.find(
(s: WorkflowState) => s.name.toLowerCase() === updates.state!.toLowerCase()
);
if (!state) {
throw new Error(`State not found: ${updates.state}`);
}
input.stateId = state.id;
}
const mutation = `
mutation($id: String!, $input: IssueUpdateInput!) {
issueUpdate(id: $id, input: $input) {
issue {
id
identifier
title
priority
state { name }
}
}
}
`;
const data = await graphql(mutation, { id: issue.id, input });
return data.issueUpdate.issue;
}
async function addComment(id: string, body: string) {
const issue = await getIssue(id);
if (!issue) {
throw new Error(`Issue not found: ${id}`);
}
const mutation = `
mutation($issueId: String!, $body: String!) {
commentCreate(input: { issueId: $issueId, body: $body }) {
comment {
id
body
createdAt
}
}
}
`;
const data = await graphql(mutation, { issueId: issue.id, body });
return data.commentCreate.comment;
}
async function searchIssues(query: string) {
const searchQuery = `
query($query: String!) {
searchIssues(query: $query, first: 20) {
nodes {
id
identifier
title
priority
state { name }
assignee { name }
url
}
}
}
`;
const data = await graphql(searchQuery, { query });
return data.searchIssues.nodes;
}
// CLI parsing
const args = process.argv.slice(2);
const command = args[0];
async function main() {
try {
switch (command) {
case "list": {
const filters: { state?: string; assignee?: string; limit?: number } = {};
for (let i = 1; i < args.length; i++) {
if (args[i] === "--state" && args[i + 1]) {
filters.state = args[++i];
} else if (args[i] === "--assignee" && args[i + 1]) {
filters.assignee = args[++i];
} else if (args[i] === "--limit" && args[i + 1]) {
filters.limit = parseInt(args[++i]);
}
}
const issues = await listIssues(filters);
console.log(JSON.stringify(issues, null, 2));
break;
}
case "get": {
const id = args[1];
if (!id) {
console.error("Usage: get <issue-id>");
process.exit(1);
}
const issue = await getIssue(id);
console.log(JSON.stringify(issue, null, 2));
break;
}
case "update": {
const id = args[1];
if (!id) {
console.error("Usage: update <issue-id> [--priority N] [--state STATE]");
process.exit(1);
}
const updates: { priority?: number; state?: string } = {};
for (let i = 2; i < args.length; i++) {
if (args[i] === "--priority" && args[i + 1]) {
updates.priority = parseInt(args[++i]);
} else if (args[i] === "--state" && args[i + 1]) {
updates.state = args[++i];
}
}
const issue = await updateIssue(id, updates);
console.log(JSON.stringify(issue, null, 2));
break;
}
case "comment": {
const id = args[1];
const body = args[2];
if (!id || !body) {
console.error("Usage: comment <issue-id> <body>");
process.exit(1);
}
const comment = await addComment(id, body);
console.log(JSON.stringify(comment, null, 2));
break;
}
case "search": {
const query = args.slice(1).join(" ");
if (!query) {
console.error("Usage: search <query>");
process.exit(1);
}
const issues = await searchIssues(query);
console.log(JSON.stringify(issues, null, 2));
break;
}
default:
console.log(`
Linear Triage CLI
Commands:
list [--state STATE] [--assignee NAME] [--limit N]
get <issue-id>
update <issue-id> [--priority N] [--state STATE]
comment <issue-id> <body>
search <query>
Priority levels: 0=none, 1=urgent, 2=high, 3=medium, 4=low
`);
}
} catch (error) {
console.error("Error:", error instanceof Error ? error.message : error);
process.exit(1);
}
}
main();