
using-linear
by WarrenZhu050413
A personal plugin marketplace for Claude Code
SKILL.md
name: Using Linear description: Guide for working with Linear project management via GraphQL API. Use when creating/updating Linear issues, changing status, adding comments, or uploading files/screenshots. Covers raw GraphQL, Python SDK (linear-py), issue search, mutations, and file upload workflows with Google Cloud Storage signed URLs. Tested October 2025.
Using Linear
Comprehensive guide for working with Linear project management system via GraphQL API and Python SDK.
Last tested: October 26, 2025 Test workspace: cs1060f25 Recommended approach: Raw GraphQL with urllib
When to Use This Skill
Use this skill when:
- Creating or updating Linear issues programmatically
- Searching for issues by identifier or filter
- Changing issue status (e.g., marking as "Done")
- Adding comments to issues
- Uploading files or screenshots to Linear issues
- Automating Linear workflows with Python scripts
API Approaches
Raw GraphQL (Recommended) ⭐
Pros: Full feature support, file uploads, variables, no dependencies Cons: More verbose Use when: Need file uploads, complex queries, or full API access
import json
import urllib.request
def graphql_request(query, variables=None):
url = "https://api.linear.app/graphql"
payload = {"query": query}
if variables:
payload["variables"] = variables
req = urllib.request.Request(
url,
data=json.dumps(payload).encode('utf-8'),
headers={
"Authorization": "lin_api_...",
"Content-Type": "application/json"
}
)
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode('utf-8'))
Python SDK (linear-py)
Pros: Simpler API for basic operations
Cons: No file uploads, no variables in queries, limited field support (no estimate!)
Use when: Only need simple reads
from linear import Linear
client = Linear("lin_api_...")
teams = client.teams() # Returns list of dicts
SDK Limitations:
- Uses snake_case (
team_id) not camelCase (teamId) - Cannot pass variables to GraphQL queries
- No file upload support
estimateparameter doesn't work in create_issue
Recommendation: Use raw GraphQL for any real work. SDK only for quick prototypes.
Comprehensive Guide
See references/linear-api-comprehensive-guide.md for:
- Detailed SDK comparison with tested examples
- Advanced GraphQL patterns (pagination, filtering, bulk operations)
- File upload workflow variations
- Common gotchas and solutions
- Testing results from cs1060f25 workspace
API Authentication
Linear uses API tokens for authentication. Include the token in request headers:
headers = {
"Authorization": "lin_api_...",
"Content-Type": "application/json"
}
API Endpoint: https://api.linear.app/graphql
Common Operations
1. Search for Issue by Identifier
To find an issue like "UNIFIED-26":
query = """
query SearchIssues($filter: IssueFilter!) {
issues(filter: $filter) {
nodes {
id
identifier
title
}
}
}
"""
variables = {
"filter": {
"number": {"eq": 26} # Extract number from "UNIFIED-26"
}
}
result = graphql_request(query, variables)
# Find exact match
for issue in result["data"]["issues"]["nodes"]:
if issue["identifier"] == "UNIFIED-26":
return issue
Important: Filter by number first, then match exact identifier to handle multiple teams.
2. Update Issue Status
To mark an issue as "Done":
mutation = """
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
issueUpdate(id: $id, input: $input) {
success
issue {
id
state {
name
}
}
}
}
"""
variables = {
"id": issue_id,
"input": {
"stateId": "done_state_id" # Get from team workflow states
}
}
3. Update Issue Due Date
To set a due date on an issue:
mutation = """
mutation UpdateIssueDueDate($id: String!, $dueDate: TimelessDate!) {
issueUpdate(id: $id, input: {dueDate: $dueDate}) {
success
issue {
id
identifier
dueDate
}
}
}
"""
variables = {
"id": issue_id,
"dueDate": "2025-11-03" # ISO 8601 format: YYYY-MM-DD
}
result = graphql_request(mutation, variables)
TimelessDate Type:
- Scalar type that accepts ISO 8601 date format:
YYYY-MM-DD - Also accepts shortcuts like
"2021"for midnight Jan 01 2021 - Accepts ISO 8601 duration strings added to current date (e.g.,
"-P2W1D"= 2 weeks and 1 day ago) - Common format:
"2025-11-03"for November 3, 2025
4. Update Issue Labels
IMPORTANT: Linear doesn't have issueAddLabel mutation. Use issueUpdate with labelIds array instead.
To add or update labels on an issue:
# First, get current labels
get_issue_query = """{
issues(filter: {identifier: {eq: "UNIFIED-15"}}) {
nodes {
id
labels { nodes { id name } }
}
}
}"""
issue_result = graphql_request(get_issue_query)
issue = issue_result['data']['issues']['nodes'][0]
issue_id = issue['id']
current_label_ids = [label['id'] for label in issue['labels']['nodes']]
# Add new label to existing ones
mutation = """
mutation UpdateIssueLabels($id: String!, $labelIds: [String!]!) {
issueUpdate(id: $id, input: {labelIds: $labelIds}) {
success
issue {
id
labels { nodes { name } }
}
}
}
"""
# Append new label ID to existing labels
new_label_ids = current_label_ids + [new_label_id]
variables = {
"id": issue_id,
"labelIds": new_label_ids # Array of ALL label IDs (existing + new)
}
result = graphql_request(mutation, variables)
Key points:
labelIdsparameter REPLACES all labels (doesn't append)- Always include existing label IDs + new ones
- Use
String!type for label IDs, notID!
4. Add Comment to Issue
To add a text comment (supports Markdown):
mutation = """
mutation CreateComment($input: CommentCreateInput!) {
commentCreate(input: $input) {
success
comment {
id
body
}
}
}
"""
variables = {
"input": {
"issueId": issue_id,
"body": "Comment text with **Markdown** support"
}
}
File Upload Workflow
Uploading files to Linear requires a two-step process with Google Cloud Storage signed URLs.
Overview
- Request upload URL from Linear GraphQL API
- Upload file to Google Cloud Storage with required headers
- Use returned asset URL in comments or issue descriptions
Critical Discovery: Required Headers
Google Cloud Storage signed URLs require EXACT headers that match the cryptographic signature. Linear's API provides these headers - they MUST be included in the upload request.
Step 1: Request Upload URL
Query Linear's fileUpload mutation with file metadata:
query = """
mutation FileUpload($size: Int!, $filename: String!, $contentType: String!) {
fileUpload(size: $size, filename: $filename, contentType: $contentType) {
success
uploadFile {
uploadUrl
assetUrl
headers {
key
value
}
}
}
}
"""
variables = {
"size": os.path.getsize(file_path),
"filename": os.path.basename(file_path),
"contentType": "image/png" # or appropriate MIME type
}
result = graphql_request(query, variables)
upload_data = result["data"]["fileUpload"]["uploadFile"]
Response includes:
uploadUrl: Google Cloud Storage signed URL (valid for 60 seconds)assetUrl: Final Linear CDN URL for the uploaded fileheaders: Array of required headers for upload
Step 2: Upload File with Required Headers
Critical: Include ALL headers returned by Linear API:
curl -X PUT \
-H "Content-Type: image/png" \
-H "x-goog-content-length-range: [exact_size],[exact_size]" \
-H 'Content-Disposition: attachment; filename="..."' \
-T /path/to/file \
"[uploadUrl]"
Headers breakdown:
Content-Type: Must matchcontentTypefrom mutation (part of signature)x-goog-content-length-range: Exact file size range (provided by Linear)Content-Disposition: Filename for download (provided by Linear)
Python example using urllib:
import urllib.request
# Build headers from Linear's response
upload_headers = {"Content-Type": "image/png"}
for header in upload_data["headers"]:
upload_headers[header["key"]] = header["value"]
# Read file
with open(file_path, 'rb') as f:
file_data = f.read()
# Upload with PUT request
req = urllib.request.Request(
upload_data["uploadUrl"],
data=file_data,
headers=upload_headers,
method='PUT'
)
with urllib.request.urlopen(req) as response:
if response.status in [200, 201]:
print(f"✅ Upload successful")
asset_url = upload_data["assetUrl"]
Step 3: Use Asset URL in Comments
Embed the uploaded image in a comment using Markdown:
comment_body = f"""✅ Screenshot uploaded
"""
# Add comment with embedded image
mutation = """
mutation CreateComment($input: CommentCreateInput!) {
commentCreate(input: $input) {
success
}
}
"""
variables = {
"input": {
"issueId": issue_id,
"body": comment_body
}
}
Common Errors and Solutions
HTTP 400: Bad Request (GraphQL Mutation)
Cause: Query syntax error, wrong mutation name, or incorrect variable types
Solution:
- Search for correct syntax - Linear's GraphQL schema may not match documentation
# Use searching-deeply skill or Exa mcp__exa__get_code_context_exa({ query: "Linear API GraphQL mutation [operation] syntax", tokensNum: 3000 }) - Check error message in response for hints
- Verify mutation exists in Linear schema (common mistake:
issueAddLabeldoesn't exist) - Confirm variable types match schema (
String!vsID!) - Look for production code examples showing correct usage
Example: Adding labels returns 400 because issueAddLabel doesn't exist → Use issueUpdate with labelIds instead
HTTP 400: Bad Request (File Upload)
Cause: Missing required headers or header mismatch
Solution:
- Check that
headersfield is included infileUploadmutation query - Add ALL headers from Linear's response to upload request
- Ensure
Content-Typematches exactly
HTTP 403: Signature Does Not Match
Cause: Headers don't match signed URL signature
Solution:
- Ensure headers are added in exact order
- Don't modify header values
- Upload file within 60-second window (signed URLs expire)
Upload Succeeds but Returns Empty Response
Expected behavior: Google Cloud Storage returns empty 200 response on success. Use the assetUrl from Step 1, not the upload response.
Python Template
Complete working example in scripts/upload_to_linear.py demonstrates:
- GraphQL request wrapper
- Issue search by identifier
- File upload with proper headers
- Comment creation with embedded image
Use this template as a reference for Linear automation workflows.
Tips and Best Practices
-
Signed URL expiration: Upload URLs expire in 60 seconds. Get fresh URL for each upload.
-
File size: Include exact file size in mutation. Linear uses this for
x-goog-content-length-rangeheader. -
Error handling: Linear API returns detailed error messages in GraphQL response
errorsfield. -
Rate limiting: Linear has API rate limits. Add delays between bulk operations.
-
Testing: Use Linear's GraphQL explorer at https://linear.app/your-workspace/settings/api for query testing.
-
Asset URLs: Linear asset URLs are permanent CDN links. Safe to store in documentation or external systems.
Reference Script
See scripts/upload_to_linear.py for complete implementation with error handling and retry logic.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon