Skip to content

Instantly share code, notes, and snippets.

@MaxGhenis
Last active October 2, 2025 12:27
Show Gist options
  • Select an option

  • Save MaxGhenis/a428df8ee9d424b15dd3bb880cf89ef0 to your computer and use it in GitHub Desktop.

Select an option

Save MaxGhenis/a428df8ee9d424b15dd3bb880cf89ef0 to your computer and use it in GitHub Desktop.
GitHub Projects v2 vs Linear API comparison - deadline filtering and automation

API Comparison: GitHub Projects vs Linear vs Jira - Deadline Filtering

The Problem: You Can't Easily Query Deadlines in GitHub Projects

GitHub Projects v2 API Limitation

GitHub Projects v2 uses GraphQL, but filtering by custom fields (like due dates) in the API is extremely cumbersome:

# GitHub Projects v2 - Complex and Limited
query {
  organization(login: "PolicyEngine") {
    projectV2(number: 1) {
      items(first: 100) {
        nodes {
          content {
            ... on Issue {
              title
              number
            }
          }
          fieldValues(first: 20) {
            nodes {
              ... on ProjectV2ItemFieldDateValue {
                date  # This is your due date
                field {
                  ... on ProjectV2FieldCommon {
                    name
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Problems:

  • ❌ Can't filter by date in the query (must fetch everything then filter client-side)
  • ❌ Custom fields are nested and awkward to access
  • ❌ No built-in "due soon" or date range filtering
  • ❌ Pagination is complex for large issue sets
  • ❌ Need to know project ID and field structure upfront

Linear API - Clean and Flexible

# Linear - Simple and Powerful
query {
  issues(
    filter: {
      dueDate: { 
        lte: "2025-10-09"  # Due within next week
        gte: "2025-10-02"  # Starting from today
      }
      state: { type: { nin: ["completed", "canceled"] } }
    }
    orderBy: dueDate
  ) {
    nodes {
      id
      title
      dueDate
      assignee { name }
      team { name }
      priority
      url
    }
  }
}

Advantages:

  • ✅ Server-side filtering by due date
  • ✅ Date range queries built-in (lte, gte, eq, etc.)
  • ✅ Combine with other filters (state, assignee, team)
  • ✅ Order by due date directly
  • ✅ Clean, flat data structure
  • ✅ Efficient - only returns what you need

Jira API - Powerful but Complex

Jira uses JQL (Jira Query Language) and REST API with good server-side filtering:

// Jira REST API v3
const response = await fetch(
  'https://policyengine.atlassian.net/rest/api/3/search',
  {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${btoa(email + ':' + apiToken)}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      jql: 'duedate >= now() AND duedate <= endOfWeek() AND status != Done ORDER BY duedate ASC',
      fields: ['summary', 'duedate', 'assignee', 'status', 'priority']
    })
  }
);
const data = await response.json();

Or using JQL directly:

duedate >= "2025-10-02" AND duedate <= "2025-10-09"
AND status not in (Done, Canceled)
ORDER BY duedate ASC

Advantages:

  • ✅ Server-side filtering by due date (JQL is powerful)
  • ✅ Date range queries built-in (>=, <=, between)
  • ✅ Combine with complex filters (JQL is very expressive)
  • ✅ Order by any field
  • ✅ Mature, well-documented API
  • ✅ Rich ecosystem of libraries

Disadvantages:

  • ⚠️ JQL learning curve (custom query language)
  • ⚠️ REST-only (no GraphQL)
  • ⚠️ More verbose than Linear
  • ⚠️ Authentication more complex (API tokens, OAuth)
  • ⚠️ Response structure deeply nested
  • ⚠️ Date functions can be confusing (startOfWeek(), endOfMonth(), etc.)

Real-World Use Case: "What deadlines are coming up?"

With GitHub Projects API (Current Pain)

// Fetch ALL issues from project
const allIssues = await fetchAllProjectIssues(projectId);

// Filter client-side (inefficient)
const upcomingDeadlines = allIssues
  .filter(issue => {
    const dueDate = issue.fieldValues.nodes.find(
      fv => fv.field?.name === 'Due Date'
    )?.date;
    if (!dueDate) return false;
    const days = daysBetween(new Date(), new Date(dueDate));
    return days >= 0 && days <= 7;
  })
  .sort((a, b) => a.dueDate - b.dueDate);

// This fetches hundreds of issues to find 10 with upcoming deadlines

Problems:

  • Downloads all issues (slow, wasteful)
  • Complex client-side filtering
  • Requires custom date logic
  • Breaks with large projects (100+ items pagination)

With Linear API (What You Want)

// Fetch only upcoming deadlines (efficient)
const upcomingDeadlines = await linear.issues({
  filter: {
    dueDate: {
      gte: new Date().toISOString().split('T')[0],
      lte: addDays(new Date(), 7).toISOString().split('T')[0]
    },
    state: { type: { nin: ['completed', 'canceled'] } }
  },
  orderBy: 'dueDate'
});

// Returns only relevant issues, pre-sorted

Benefits:

  • Server does the filtering (fast)
  • Only transfers relevant data
  • Works at any scale
  • Clean, readable code

With Jira API (Middle Ground)

// Using Jira's REST API
const jiraClient = require('jira-client');
const jira = new jiraClient({
  protocol: 'https',
  host: 'policyengine.atlassian.net',
  username: process.env.JIRA_EMAIL,
  password: process.env.JIRA_API_TOKEN,
  apiVersion: '3',
  strictSSL: true
});

// Query upcoming deadlines
const upcomingDeadlines = await jira.searchJira(
  'duedate >= now() AND duedate <= endOfWeek() AND status not in (Done, Canceled) ORDER BY duedate ASC',
  {
    fields: ['summary', 'duedate', 'assignee', 'status', 'priority'],
    maxResults: 100
  }
);

// Response is deeply nested
const issues = upcomingDeadlines.issues.map(issue => ({
  title: issue.fields.summary,
  dueDate: issue.fields.duedate,
  assignee: issue.fields.assignee?.displayName,
  status: issue.fields.status.name,
  priority: issue.fields.priority?.name
}));

Characteristics:

  • ✅ Server-side filtering (JQL is powerful)
  • ⚠️ Requires JQL knowledge (learning curve)
  • ⚠️ Response needs transformation (deeply nested)
  • ✅ Very flexible for complex queries
  • ⚠️ More verbose than Linear

Linear API Additional Capabilities

1. Flexible Date Queries

# Issues due this week
dueDate: { gte: "2025-10-02", lte: "2025-10-09" }

# Overdue issues
dueDate: { lt: "2025-10-02" }

# Issues without due dates
dueDate: { null: true }

# Due in next 30 days
dueDate: { lte: "2025-11-01" }

2. Complex Filtering

filter: {
  dueDate: { lte: "2025-10-09" }
  priority: { gte: 2 }  # High priority only
  team: { key: { eq: "APP" } }  # Specific team
  assignee: { isMe: { eq: true } }  # My issues
  state: { type: { eq: "started" } }  # In progress
}

3. Aggregations

# Count overdue issues by team
query {
  teams {
    nodes {
      name
      issues(filter: { 
        dueDate: { lt: "2025-10-02" }
        state: { type: { nin: ["completed"] }}
      }) {
        nodes { id }
      }
    }
  }
}

4. Subscriptions (Real-time)

# Get notified when issues are due soon
subscription {
  issueUpdates(filter: { dueDate: { lte: "2025-10-09" } }) {
    node {
      title
      dueDate
      assignee { name }
    }
  }
}

Jira API Additional Capabilities

1. JQL Date Functions

# Issues due this week
duedate >= startOfWeek() AND duedate <= endOfWeek()

# Overdue issues
duedate < now()

# Issues without due dates
duedate is EMPTY

# Due in next 30 days
duedate <= 30d

# Due between specific dates
duedate >= "2025-10-02" AND duedate <= "2025-11-01"

2. Complex JQL Queries

# High priority, overdue, assigned to me
duedate < now()
AND priority in (High, Highest)
AND assignee = currentUser()
AND status != Done
ORDER BY duedate ASC

# Issues due this sprint, by team
duedate <= endOfWeek()
AND project = "PE"
AND component = "US Model"
AND status in ("In Progress", "To Do")

3. Aggregations & Reporting

// Count issues by due date ranges using JQL
const overdueCount = await jira.searchJira(
  'duedate < now() AND status != Done',
  { maxResults: 0 }
);
// overdueCount.total gives the count

// Group by assignee (requires fetching and processing)
const byAssignee = await jira.searchJira(
  'duedate <= endOfWeek() AND status != Done',
  { fields: ['assignee', 'duedate'] }
);

4. Webhooks (Event-based)

// Jira webhooks for issue updates
// Configure in Jira UI: Settings → System → Webhooks

// Webhook payload when issue due date changes:
{
  "webhookEvent": "jira:issue_updated",
  "issue": {
    "fields": {
      "summary": "...",
      "duedate": "2025-10-09",
      "status": { "name": "In Progress" }
    }
  },
  "changelog": {
    "items": [{
      "field": "duedate",
      "fromString": "2025-10-15",
      "toString": "2025-10-09"
    }]
  }
}

GitHub Projects Alternative Workarounds (All Bad)

Option 1: Use GitHub CLI with Custom Script

# Still requires fetching everything then filtering
gh project item-list 1 --owner PolicyEngine --format json \
  | jq 'complicated filter logic here'
  • Still downloads everything
  • Fragile to schema changes
  • Requires jq/custom scripting

Option 2: Use GitHub Issues API (Not Projects)

gh issue list --search "is:open" --json title,dueDate
  • ❌ GitHub Issues don't have native due dates
  • ❌ Would need to use labels or milestones (hacky)
  • ❌ Loses Projects v2 structure

Option 3: Sync to External Database

  • Maintain your own database with webhook sync
  • Massive overhead just to filter by date
  • Defeats the purpose of using a tool

Side-by-Side API Comparison

Feature GitHub Projects v2 Linear Jira
Filter by due date in query ❌ No ✅ Yes ✅ Yes (JQL)
Date range queries ❌ Client-side only ✅ Built-in ✅ Built-in (JQL)
Sort by due date ❌ Client-side only ✅ Built-in ✅ Built-in
Combine date + other filters ❌ Very complex ✅ Easy ✅ Powerful (JQL)
Pagination with filters ⚠️ Awkward ✅ Smooth ✅ Good
Real-time subscriptions ❌ No ✅ Yes (GraphQL) ⚠️ Webhooks only
Type safety (SDK) ⚠️ Limited ✅ Full TypeScript ⚠️ Community libs
API documentation ⚠️ Complex ✅ Excellent ✅ Comprehensive
Learning curve Medium Low High (JQL)
Query language GraphQL GraphQL JQL (custom)
Response structure Nested Flat Deeply nested
Official SDK ❌ No ✅ Yes (@linear/sdk) ⚠️ Community only

Practical Examples

What You Want to Do

Query: "Show me all issues due in the next 7 days, ordered by priority"

GitHub Projects v2:

// 50+ lines of complex code
// Fetch all, filter client-side, sort manually

Linear:

const issues = await linear.issues({
  filter: {
    dueDate: {
      gte: today,
      lte: addDays(today, 7)
    },
    state: { type: { eq: "started" } }
  },
  orderBy: 'priority'
});

Jira:

const issues = await jira.searchJira(
  'duedate >= now() AND duedate <= 7d AND status = "In Progress" ORDER BY priority DESC',
  { fields: ['summary', 'duedate', 'priority', 'assignee'] }
);

Query: "Which team has the most overdue issues?"

GitHub Projects v2:

// Fetch all projects
// Extract all items
// Parse custom field values
// Group by team field
// Count where date < today
// 100+ lines of code

Linear:

query {
  teams {
    nodes {
      name
      issues(filter: {
        dueDate: { lt: "2025-10-02" }
        state: { type: { nin: ["completed", "canceled"] }}
      }) {
        nodes { id }
      }
    }
  }
}

Jira:

// Query each project/component separately
const projects = ['US', 'UK', 'APP', 'API'];
const results = await Promise.all(
  projects.map(async (proj) => {
    const issues = await jira.searchJira(
      `project = ${proj} AND duedate < now() AND status != Done`,
      { maxResults: 0 }
    );
    return { project: proj, count: issues.total };
  })
);
// Sort by count to find team with most overdue
results.sort((a, b) => b.count - a.count);

The Automation You Want

Example: Daily Deadline Reminder Bot

With Linear's API:

// Send Slack message with upcoming deadlines
async function sendDeadlineReminder() {
  const issues = await linear.issues({
    filter: {
      dueDate: { 
        gte: today,
        lte: addDays(today, 3) // Next 3 days
      },
      state: { type: { nin: ['completed', 'canceled'] } }
    },
    orderBy: 'dueDate'
  });

  const message = issues.nodes.map(issue => 
    `• ${issue.title} - Due ${issue.dueDate} (@${issue.assignee.name})`
  ).join('\n');

  await slack.postMessage({
    channel: '#deadlines',
    text: `🚨 Upcoming deadlines:\n${message}`
  });
}

// Run daily via cron

With GitHub Projects API:

  • Would require fetching all issues across all projects
  • Complex field value parsing
  • Client-side date filtering
  • 5x more code, 10x slower

With Jira API:

// Send Slack message with upcoming deadlines
async function sendDeadlineReminder() {
  const issues = await jira.searchJira(
    'duedate >= now() AND duedate <= 3d AND status not in (Done, Canceled) ORDER BY duedate ASC',
    {
      fields: ['summary', 'duedate', 'assignee'],
      maxResults: 50
    }
  );

  const message = issues.issues.map(issue =>
    `• ${issue.fields.summary} - Due ${issue.fields.duedate} (@${issue.fields.assignee?.displayName || 'Unassigned'})`
  ).join('\n');

  await slack.postMessage({
    channel: '#deadlines',
    text: `🚨 Upcoming deadlines:\n${message}`
  });
}

Characteristics:

  • ✅ Server-side filtering works well
  • ⚠️ Need to know JQL syntax
  • ⚠️ Response requires transformation
  • ✅ Flexible and powerful

Linear API Official SDK

Linear provides official SDKs that make this even easier:

import { LinearClient } from '@linear/sdk';

const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });

// TypeScript autocomplete for everything
const upcomingIssues = await client.issues({
  filter: {
    dueDate: { 
      lte: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    }
  }
});

// Full type safety
upcomingIssues.nodes.forEach(issue => {
  console.log(issue.title); // TypeScript knows this exists
  console.log(issue.dueDate); // And this
});

GitHub Projects has no official SDK - you use Octokit and raw GraphQL.

Jira API Community SDKs

Jira doesn't have an official SDK, but has mature community libraries:

// Popular Node.js library: jira-client
const JiraClient = require('jira-client');

const jira = new JiraClient({
  protocol: 'https',
  host: 'policyengine.atlassian.net',
  username: process.env.JIRA_EMAIL,
  password: process.env.JIRA_API_TOKEN,
  apiVersion: '3',
  strictSSL: true
});

// Type safety with TypeScript definitions
const upcomingIssues = await jira.searchJira(
  'duedate <= 7d AND status != Done',
  { fields: ['summary', 'duedate'] }
);

// Or use the newer jira.js library with better TypeScript support
import { Version3Client } from 'jira.js';

const client = new Version3Client({
  host: 'https://policyengine.atlassian.net',
  authentication: {
    basic: {
      email: process.env.JIRA_EMAIL,
      apiToken: process.env.JIRA_API_TOKEN,
    },
  },
});

const issues = await client.issueSearch.searchForIssuesUsingJql({
  jql: 'duedate <= 7d AND status != Done',
});

Popular Libraries:

  • jira-client - Most widely used, mature
  • jira.js - Better TypeScript support, modern API
  • jira-connector - Alternative option
  • All community-maintained (no official SDK)

Verdict: Which API is Best for Deadline Automation?

API Quality Ranking

For deadline querying and automation:

  1. Linear ⭐⭐⭐⭐⭐

    • Cleanest API, easiest to use
    • Official SDK with full TypeScript support
    • GraphQL with powerful filtering
    • Best developer experience
    • Best for: Speed and simplicity
  2. Jira ⭐⭐⭐⭐

    • Powerful JQL for complex queries
    • Mature ecosystem
    • Good server-side filtering
    • Best for: Complex workflows, if free via OSS license
  3. GitHub Projects v2 ⭐⭐

    • Cannot filter by due date server-side
    • Complex nested structure
    • No official SDK
    • Best for: Staying with existing setup (but limited)

Key Differences

Aspect Linear Jira GitHub Projects
Ease of use Excellent Moderate (JQL) Poor
Query power Great Excellent Limited
Developer UX Best Good Mediocre
Learning curve Minimal Medium (JQL) High (GraphQL)
Documentation Excellent Excellent Limited
Automation ready Yes Yes No

Recommendation for PolicyEngine

If you're hitting GitHub Projects API limits:

Option 1: Linear (Recommended for Developer Experience)

  • Cost: $120-150/month
  • Benefit: Best API, fastest to implement, cleanest code
  • Choose if: You value developer time and want the best experience

Option 2: Jira (Recommended if Free)

  • Cost: $0 (if OSS license approved)
  • Benefit: Powerful API, saves money
  • Choose if: You get the free OSS license and can tolerate JQL

Option 3: Stay with GitHub Projects

  • Cost: $0
  • Benefit: No migration
  • Choose if: You can work around the limitations or don't need deadline automation

Code Complexity Comparison

For "Show upcoming deadlines" automation:

Linear:     5-10 lines of clean code
Jira:       15-20 lines (JQL + transformation)
GitHub:     50+ lines (fetch all, parse, filter client-side)

For "Daily deadline reminder bot":

Linear:     ~20 lines total
Jira:       ~30 lines total
GitHub:     ~100+ lines (complex, fragile)

Cost-Benefit Analysis for PolicyEngine

Linear Option

Cost: $120-150/month for 15 users

Benefits:

  • ✅ Build deadline automation in hours (not days)
  • ✅ AI agents can easily query deadlines
  • ✅ Save 2-3 hours/month on manual deadline checking
  • ✅ Cleaner codebase for integrations
  • ✅ Better developer experience = faster feature development

ROI: If team saves 2-3 hours/month, Linear pays for itself at typical engineering hourly rates.

Jira Option (if OSS approved)

Cost: $0 (free for open source)

Benefits:

  • ✅ Powerful API for deadline automation
  • ✅ More complex queries possible
  • ✅ Free Confluence for documentation
  • ✅ Good for long-term policy research tracking

Trade-offs:

  • ⚠️ JQL learning curve (1-2 weeks)
  • ⚠️ More verbose code
  • ⚠️ Slower UI for daily use

ROI: If approved, hard to beat free. Worth the JQL learning curve.

GitHub Projects Option

Cost: $0 (already using)

Benefits:

  • ✅ No migration needed
  • ✅ Native GitHub integration

Limitations:

  • ❌ Cannot efficiently query deadlines via API
  • ❌ Complex workarounds required
  • ❌ Limits automation potential
  • ❌ Frustrating for AI agents

ROI: Free, but limits your automation goals.


Final Recommendation

Given your specific needs (AI agents querying deadlines):

  1. Apply for Jira OSS license (10 minutes, potentially free)

    • If approved: Use Jira, accept JQL learning curve
    • If denied: Continue to step 2
  2. Trial Linear (14 days free)

    • Test deadline automation
    • Evaluate developer experience
    • If it saves 2-3 hours/month: Worth $120-150
  3. If budget constrained:

    • Stay with GitHub Projects
    • Build complex client-side filtering
    • Accept limitations

Most likely outcome for PolicyEngine: Linear is worth it for the API quality and developer experience, especially given your use of AI agents and automation goals.

The ability to ask "what deadlines are coming up?" and get an instant, accurate answer is exactly what Linear's API enables that GitHub Projects cannot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment