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.message}"
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()
    # ... 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: .id },
      source: ,
      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

  1. Each error generates a fingerprint based on exception class, message, and backtrace
  2. If an open issue exists with the same fingerprint, a comment is added instead
  3. 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
 = LinearApi::SyncedIssue.where(
  source_type: 'CloverAccount',
  source_id: .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 RateLimitError with retry_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_error never raises -- returns failure Result and 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.