Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Issue lifecycle management with Linear: create, update, transition, relate, comment, and organize issues through the SDK and GraphQL API. Trigger: "linear issue workflow", "linear issue lifecycle", "create linear issues", "update linear issue", "linear state transition", "linear sub-issues", "linear comments".
.claude/skills/jeremylongshore-linear-core-workflow-a/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-17 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 81% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 164% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 116% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 50% | 0% |
Master issue lifecycle management: creating, updating, transitioning states, building parent/sub-issue hierarchies, managing labels, and commenting. Linear issues flow through typed workflow states (triage -> backlog -> unstarted -> started -> completed | canceled), belong to a team, and support priorities 0-4, estimates, due dates, labels, and cycle/project assignment.
@linear/sdk installed with API key or OAuth token configuredtypescriptimport { LinearClient } from "@linear/sdk"; const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! }); // Get team const teams = await client.teams(); const team = teams.nodes.find(t => t.key === "ENG") ?? teams.nodes[0]; // Basic issue const result = await client.createIssue({ teamId: team.id, title: "Implement user authentication", description: "Add OAuth 2.0 login flow with Google and GitHub providers.", priority: 2, // 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low }); if (result.success) { const issue = await result.issue; console.log(`Created: ${issue?.identifier} — ${issue?.title}`); console.log(`URL: ${issue?.url}`); } // Issue with full metadata const labelResult = await client.issueLabels({ filter: { name: { eq: "Bug" } } }); const bugLabel = labelResult.nodes[0]; const states = await team.states(); const todoState = states.nodes.find(s => s.type === "unstarted")!; await client.createIssue({ teamId: team.id, title: "Fix login redirect loop on Safari", description: "Users get stuck in infinite redirect after SSO callback.", priority: 1, stateId: todoState.id, assigneeId: "user-uuid", labelIds: bugLabel ? [bugLabel.id] : [], estimate: 3, dueDate: "2026-04-15", });
typescript// Update by issue ID await client.updateIssue("issue-uuid", { title: "Updated title", priority: 1, estimate: 5, dueDate: "2026-04-30", }); // Find issue by team key + number, then update const issues = await client.issues({ filter: { number: { eq: 123 }, team: { key: { eq: "ENG" } } }, }); const issue = issues.nodes[0]; if (issue) { await issue.update({ priority: 2, description: "Updated description with more details.", }); } // Add/remove labels const featureLabel = (await client.issueLabels({ filter: { name: { eq: "Feature" } }, })).nodes[0]; if (featureLabel) { await client.updateIssue(issue.id, { labelIds: [...(issue.labelIds ?? []), featureLabel.id], }); }
typescript// List all workflow states for a team const teamStates = await team.states(); for (const state of teamStates.nodes) { console.log(`${state.name} (type: ${state.type}, position: ${state.position})`); } // Output example: // Triage (type: triage, position: 0) // Backlog (type: backlog, position: 1) // Todo (type: unstarted, position: 2) // In Progress (type: started, position: 3) // In Review (type: started, position: 4) // Done (type: completed, position: 5) // Canceled (type: canceled, position: 6) // Move to "In Progress" const inProgress = teamStates.nodes.find(s => s.name === "In Progress"); if (inProgress) { await client.updateIssue(issue.id, { stateId: inProgress.id }); } // Complete an issue const done = teamStates.nodes.find(s => s.type === "completed"); if (done) { await issue.update({ stateId: done.id }); }
typescript// Create parent issue const parentResult = await client.createIssue({ teamId: team.id, title: "Auth system overhaul", description: "Epic: modernize authentication infrastructure.", }); const parent = await parentResult.issue; // Create sub-issues under parent await client.createIssue({ teamId: team.id, title: "Implement JWT token refresh", parentId: parent!.id, priority: 2, }); await client.createIssue({ teamId: team.id, title: "Add MFA support", parentId: parent!.id, priority: 3, }); // List sub-issues const children = await parent!.children(); for (const child of children.nodes) { console.log(` Sub: ${child.identifier} — ${child.title}`); }
typescript// Relation types: "blocks", "duplicate", "related" await client.createIssueRelation({ issueId: "blocked-issue-id", relatedIssueId: "blocking-issue-id", type: "blocks", }); // Mark as duplicate await client.createIssueRelation({ issueId: "duplicate-issue-id", relatedIssueId: "original-issue-id", type: "duplicate", }); // List relations on an issue const relations = await issue.relations(); for (const rel of relations.nodes) { const related = await rel.relatedIssue; console.log(`${rel.type}: ${related?.identifier}`); }
typescript// Add a comment (supports Markdown) await client.createComment({ issueId: issue.id, body: "Deployed fix to staging.\n\n```bash\nnpm run test:e2e -- --filter auth\n```\n\nAll 47 tests passing.", }); // List comments on an issue const comments = await issue.comments(); for (const comment of comments.nodes) { const user = await comment.user; console.log(`${user?.name}: ${comment.body.substring(0, 80)}...`); }
typescript// Create a link attachment on an issue await client.createAttachment({ issueId: issue.id, title: "Figma Design", url: "https://figma.com/file/xxx", subtitle: "Login page redesign", });
| Error | Cause | Solution | |-------|-------|----------| | Entity not found | Invalid issue ID or deleted | Verify with client.issue(id) first | | State not found | Wrong team's state ID | List states for correct team: team.states() | | Validation error on create | Missing required field | teamId + title required; priority must be 0-4 | | Circular dependency | Issue blocks itself transitively | Validate relation graph before creating | | Forbidden | No write access to team | Check team membership and API key scope |
typescriptimport { parse } from "csv-parse/sync"; import fs from "fs"; const rows = parse(fs.readFileSync("issues.csv"), { columns: true }); for (const row of rows) { const result = await client.createIssue({ teamId: team.id, title: row.title, description: row.description, priority: parseInt(row.priority) || 3, }); const issue = await result.issue; console.log(`Created: ${issue?.identifier}`); }
typescriptconst stale = await client.issues({ filter: { state: { type: { in: ["unstarted", "started"] } }, updatedAt: { lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString() }, }, first: 50, }); const canceled = (await team.states()).nodes.find(s => s.type === "canceled")!; for (const issue of stale.nodes) { await issue.update({ stateId: canceled.id }); await client.createComment({ issueId: issue.id, body: "Auto-closed: no activity for 90 days.", }); console.log(`Closed: ${issue.identifier} (last updated ${issue.updatedAt})`); }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 14,808 | 12,815 | -13% | 1 | 1 | 0% | 1,903 | 3,447 | +81% | 0 | 0 | — |
case-02 | pass→pass | 10,963 | 9,478 | -14% | 1 | 1 | 0% | 1,186 | 3,129 | +164% | 0 | 0 | — |
case-03 | pass→pass | 12,064 | 9,504 | -21% | 1 | 1 | 0% | 1,481 | 3,202 | +116% | 0 | 0 | — |
case-04 | pass→pass | 16,178 | 9,933 | -39% | 1 | 1 | 0% | 2,198 | 3,298 | +50% | 0 | 0 | — |
case-05 | pass→pass | 12,457 | 11,898 | -4% | 1 | 1 | 0% | 1,536 | 3,654 | +138% | 0 | 0 | — |
case-06 | pass→pass | 11,929 | 10,836 | -9% | 1 | 1 | 0% | 1,352 | 3,309 | +145% | 0 | 0 | — |
case-07 | pass→pass | 11,635 | 9,114 | -22% | 1 | 1 | 0% | 1,312 | 3,101 | +136% | 0 | 0 | — |
case-08 | pass→pass | 14,394 | 11,067 | -23% | 1 | 1 | 0% | 1,859 | 3,467 | +86% | 0 | 0 | — |
case-09 | pass→pass | 13,223 | 11,640 | -12% | 1 | 1 | 0% | 1,600 | 3,668 | +129% | 0 | 0 | — |
case-10 | pass→pass | 13,607 | 11,365 | -16% | 1 | 1 | 0% | 2,034 | 3,562 | +75% | 0 | 0 | — |
case-11 | pass→pass | 11,516 | 9,474 | -18% | 1 | 1 | 0% | 1,246 | 3,099 | +149% | 0 | 0 | — |
case-12 | pass→pass | 17,028 | 11,975 | -30% | 1 | 1 | 0% | 2,189 | 3,576 | +63% | 0 | 0 | — |
case-13 | pass→pass | 16,223 | 4,286 | -74% | 1 | 1 | 0% | 2,001 | 2,813 | +41% | 0 | 0 | — |
case-14 | pass→pass | 16,037 | 8,884 | -45% | 1 | 1 | 0% | 2,273 | 2,977 | +31% | 0 | 0 | — |
case-15 | pass→pass | 8,614 | 9,679 | +12% | 1 | 1 | 0% | 1,295 | 2,894 | +123% | 0 | 0 | — |
case-16 | pass→pass | 16,190 | 9,548 | -41% | 1 | 1 | 0% | 1,635 | 3,013 | +84% | 0 | 0 | — |
case-17 | fail→pass | 10,782 | 11,081 | +3% | 1 | 1 | 0% | 1,716 | 3,152 | +84% | 0 | 0 | — |
case-18 | pass→pass | 14,884 | 12,825 | -14% | 1 | 1 | 0% | 1,947 | 3,446 | +77% | 0 | 0 | — |
case-19 | pass→pass | 13,094 | 11,154 | -15% | 1 | 1 | 0% | 2,563 | 4,429 | +73% | 0 | 0 | — |
case-20 | pass→pass | 12,919 | 14,499 | +12% | 1 | 1 | 0% | 2,740 | 4,551 | +66% | 0 | 0 | — |
case-21 | pass→pass | 19,746 | 8,982 | -55% | 1 | 1 | 0% | 2,415 | 4,231 | +75% | 0 | 0 | — |
case-22 | pass→pass | 18,856 | 15,224 | -19% | 1 | 1 | 0% | 2,236 | 4,510 | +102% | 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. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 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.