LinearApi
A Ruby gem for interacting with the Linear GraphQL API. Create, update, and query issues, projects, and labels programmatically. Includes a Rails Engine for automatic error tracking with deduplication.
Installation
Add this line to your application's Gemfile:
gem 'linear_api'
Then run:
bundle install
Or install it directly:
gem install linear_api
For Rails applications, run the migrations:
bin/rails linear_api:install:migrations
bin/rails db:migrate
Configuration
require 'linear_api'
LinearApi.configure do |config|
config.api_key = ENV['LINEAR_API_KEY']
config.team_id = 'your-team-id' # e.g., 'cfe09c0a-dcae-4cf9-be66-621d00798fcf'
config.workspace_slug = 'your-workspace' # Used for building issue URLs (default: 'theownerstack')
config.logger = Rails.logger # Optional: defaults to Rails.logger or stdout
end
Usage
Create an Issue
result = LinearApi.client.create_issue(
title: 'Bug: Login fails with special characters',
description: '## Problem\n\nUsers cannot login when password contains @',
priority: 2, # 1=Urgent, 2=High, 3=Medium, 4=Low
label_ids: ['752f6bfb-9943-4dde-8d36-63b43c34c12f'],
due_date: '2026-03-01', # ISO 8601 date
estimate: 3 # Story points
)
if result.success?
puts "Created: #{result.data.identifier}" # => "TOS-123"
puts "URL: #{result.data.url}"
else
puts "Error: #{result.error}"
end
# Or use value! to raise on failure:
issue = LinearApi.client.create_issue(title: 'Bug').value!
puts issue.identifier
Get an Issue
# Direct lookup by identifier (exact match)
result = LinearApi.client.get_issue('TOS-123')
if result.success?
issue = result.data
puts "Title: #{issue.title}"
puts "State: #{issue.state_name}"
puts "Labels: #{issue.label_names.join(', ')}"
puts "Due: #{issue.due_date}"
puts "Estimate: #{issue.estimate}"
end
Search Issues (Full-Text)
result = LinearApi.client.search_issues(term: 'login bug', limit: 10)
result.data.each do |issue|
puts "#{issue.identifier}: #{issue.title}"
end
Update an Issue
# Update state
result = LinearApi.client.update_issue(
id: 'issue-uuid',
state_id: 'state-uuid'
)
# Add labels
result = LinearApi.client.update_issue(
id: 'issue-uuid',
label_ids: ['label-1', 'label-2']
)
Add a Comment
result = LinearApi.client.add_comment(
issue_id: 'issue-uuid',
body: '## QA Results\n\n✅ All tests passed'
)
List Issues
# Simple list (up to 50)
result = LinearApi.client.list_issues(limit: 50)
result.data.each do |issue|
puts "#{issue.identifier}: #{issue.title} [#{issue.state_name}]"
end
# List ALL issues with automatic pagination
result = LinearApi.client.list_all_issues(batch_size: 50) do |issue|
# Optional: process each issue as it's fetched
puts "Fetched: #{issue.identifier}"
end
puts "Total: #{result.data.length}"
Batch Fetch Issues
# Fetch multiple issues in a single API call (avoids N+1)
result = LinearApi.client.batch_get_issues(ids: ['uuid-1', 'uuid-2', 'uuid-3'])
result.data.each do |issue|
puts "#{issue.identifier}: #{issue.state_name}"
end
List Labels
result = LinearApi.client.list_labels
result.data.each do |label|
puts "#{label.name}: #{label.id}"
end
Projects
# List all projects
result = LinearApi.client.list_projects
result.data.each do |project|
puts "#{project.name}: #{project.id}"
end
# Create a project
result = LinearApi.client.create_project(
name: 'Test Coverage and Automation',
description: 'Track QA automation and test coverage improvements',
color: '#4338ca'
)
if result.success?
puts "Created project: #{result.data.name}"
puts "URL: #{result.data.url}"
end
# Add an issue to a project
result = LinearApi.client.create_issue(
title: 'Add integration tests for payment flow',
description: '## Objective\n\nImprove test coverage for payment processing',
project_id: 'project-uuid',
priority: 2
)
Get Workflow States
result = LinearApi.client.list_states
result.data.each do |state|
puts "#{state['name']} (#{state['type']}): #{state['id']}"
end
List Team Members (for Assignment)
result = LinearApi.client.list_users
result.data.each do |user|
puts "#{user['name']} (#{user['email']}): #{user['id']}"
end
# Assign to a user
LinearApi.client.assign(id: issue.id, assignee_id: 'user-uuid')
Fetch All Metadata (Single API Call)
# Fetch labels, projects, states, and users in one batched call
result = LinearApi.client.
if result.success?
puts "Labels: #{result.data[:labels].length}"
puts "Projects: #{result.data[:projects].length}"
puts "States: #{result.data[:states].length}"
puts "Users: #{result.data[:users].length}"
end
Archive/Unarchive Issues
# Archive (soft delete)
LinearApi.client.archive_issue(id: 'issue-uuid')
# Restore
LinearApi.client.unarchive_issue(id: 'issue-uuid')
Create Labels
result = LinearApi.client.create_label(
name: 'priority:critical',
color: '#ff0000',
description: 'Critical priority issues'
)
puts "Created label: #{result.data.id}"
Sub-Issues
# Create a sub-issue
result = LinearApi.client.create_sub_issue(
parent_id: 'parent-issue-uuid',
title: 'Sub-task: Write tests',
priority: 3
)
# List sub-issues
children = LinearApi.client.list_sub_issues(parent_id: 'parent-issue-uuid')
children.data.each do |child|
puts "#{child.identifier}: #{child.title}"
end
Direct GraphQL Queries
For advanced use cases, execute raw GraphQL:
query = <<~GRAPHQL
query {
viewer {
name
email
}
}
GRAPHQL
result = LinearApi.client.query(query)
puts result.data['viewer']['name']
Error Handling
result = LinearApi.client.create_issue(title: 'Test')
if result.success?
puts result.data.identifier
else
puts "Error: #{result.error}"
end
# Or use value! to raise on failure (good for scripts)
begin
issue = LinearApi.client.create_issue(title: 'Test').value!
puts issue.identifier
rescue LinearApi::Error => e
puts "Failed: #{e.}"
rescue LinearApi::RateLimitError => e
puts "Rate limited, retry after #{e.retry_after}s"
end
Health Check
if LinearApi.healthy?
puts "Linear API is reachable"
else
puts "Cannot connect to Linear"
end
Rails Engine (Auto-Tracking)
The gem includes a Rails Engine for automatic error tracking with deduplication. Errors with the same fingerprint are grouped together instead of creating duplicate issues.
Setup
# Verify Linear API connectivity
bin/rails linear:health
# Create the auto-tracking project and label in Linear
bin/rails linear:setup
# Sync labels, projects, and states from Linear (single batched API call)
bin/rails linear:sync_cache
Track Errors Automatically
# In your service or job
class DailySalesReportJob < ApplicationJob
def perform(account)
# ... generate report ...
rescue StandardError => e
# Track the error in Linear (never raises, returns Result)
LinearApi::IssueTracker.track_error(
exception: e,
context: { date: Date.current, account_id: account.id },
source: account,
labels: [:bug, :sync]
)
raise # Re-raise after tracking
end
end
Track Custom Issues
# Create a tracked issue (not from an exception)
LinearApi::IssueTracker.track_issue(
title: 'Missing API credentials for merchant',
description: "Merchant #{merchant.name} has invalid Clover credentials",
context: { merchant_id: merchant.id },
source: merchant,
labels: [:bug, :area_sync]
)
Security: Sensitive Data Redaction
IssueTracker automatically redacts context values whose keys match sensitive patterns (password, secret, token, api_key, credential, authorization):
LinearApi::IssueTracker.track_error(
exception: e,
context: {
account_id: 123, # Included as-is
api_key: 'sk_live_xxx', # Redacted to '[REDACTED]'
user_email: '[email protected]' # Included as-is
}
)
How Deduplication Works
- Each error generates a fingerprint based on exception class, message, and backtrace
- If an open issue exists with the same fingerprint, a comment is added instead
- Once an issue is marked "Done" or "Canceled", new occurrences create a fresh issue
Rake Tasks
# Verify API connectivity
bin/rails linear:health
# Setup auto-tracking project and label
bin/rails linear:setup
# Sync metadata from Linear API (single batched call)
bin/rails linear:sync_cache
# Refresh local issue states (batched, checks if resolved)
bin/rails linear:refresh_issues
# Show cache statistics
bin/rails linear:stats
# Export cached metadata to JSON
bin/rails linear:export_cache
Cached Metadata
The engine caches Linear metadata locally for fast lookups:
# Find a label by name
label = LinearApi::CachedMetadata.labels.find_by(name: 'type:bug')
# Get all projects
projects = LinearApi::CachedMetadata.projects.pluck(:name, :linear_id)
# Resolve label ID from key
bug_id = LinearApi::CachedMetadata.resolve_label_id('type:bug')
Synced Issues
Track which issues have been created:
# Find all unresolved tracked issues
open_issues = LinearApi::SyncedIssue.unresolved
# Find issues for a specific source
account_issues = LinearApi::SyncedIssue.where(
source_type: 'CloverAccount',
source_id: account.id
)
# Check if an issue is still open
issue = LinearApi::SyncedIssue.find_by(identifier: 'TOS-123')
puts issue.open? # => true/false
puts issue.linear_url # => "https://linear.app/your-workspace/issue/TOS-123"
Resilience Features
- Connection timeouts: 5s connect, 15s read (configurable per-client)
- Automatic retries: 3 retries with exponential backoff on 500/502/503 and network errors
- Rate limit detection: HTTP 429 raises
RateLimitErrorwithretry_after - Thread safety: Module-level client access is protected by
Mutex - Logging: All API calls, retries, and errors logged via configurable
LinearApi.logger - Silent error tracking:
IssueTracker.track_errornever raises -- returns failureResultand logs
Development
# Install dependencies
bundle install
# Run tests
bundle exec rspec
# Run tests with VCR recording (hits live API)
VCR_RECORD=1 bundle exec rspec
# Run linter
bundle exec rubocop
# Run all checks
bundle exec rake
License
MIT License. See LICENSE.txt.