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 - 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 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.)
// 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 deadlinesProblems:
- Downloads all issues (slow, wasteful)
- Complex client-side filtering
- Requires custom date logic
- Breaks with large projects (100+ items pagination)
// 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-sortedBenefits:
- Server does the filtering (fast)
- Only transfers relevant data
- Works at any scale
- Clean, readable code
// 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
# 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" }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
}# Count overdue issues by team
query {
teams {
nodes {
name
issues(filter: {
dueDate: { lt: "2025-10-02" }
state: { type: { nin: ["completed"] }}
}) {
nodes { id }
}
}
}
}# Get notified when issues are due soon
subscription {
issueUpdates(filter: { dueDate: { lte: "2025-10-09" } }) {
node {
title
dueDate
assignee { name }
}
}
}# 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"
# 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")
// 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'] }
);// 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"
}]
}
}# 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
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
- Maintain your own database with webhook sync
- Massive overhead just to filter by date
- Defeats the purpose of using a tool
| 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 | ✅ Smooth | ✅ Good | |
| Real-time subscriptions | ❌ No | ✅ Yes (GraphQL) | |
| Type safety (SDK) | ✅ Full TypeScript | ||
| API documentation | ✅ 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) |
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 manuallyLinear:
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 codeLinear:
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);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 cronWith 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 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 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, maturejira.js- Better TypeScript support, modern APIjira-connector- Alternative option- All community-maintained (no official SDK)
For deadline querying and automation:
-
Linear ⭐⭐⭐⭐⭐
- Cleanest API, easiest to use
- Official SDK with full TypeScript support
- GraphQL with powerful filtering
- Best developer experience
- Best for: Speed and simplicity
-
Jira ⭐⭐⭐⭐
- Powerful JQL for complex queries
- Mature ecosystem
- Good server-side filtering
- Best for: Complex workflows, if free via OSS license
-
GitHub Projects v2 ⭐⭐
- Cannot filter by due date server-side
- Complex nested structure
- No official SDK
- Best for: Staying with existing setup (but limited)
| 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 |
If you're hitting GitHub Projects API limits:
- Cost: $120-150/month
- Benefit: Best API, fastest to implement, cleanest code
- Choose if: You value developer time and want the best experience
- Cost: $0 (if OSS license approved)
- Benefit: Powerful API, saves money
- Choose if: You get the free OSS license and can tolerate JQL
- Cost: $0
- Benefit: No migration
- Choose if: You can work around the limitations or don't need deadline automation
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: $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.
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.
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.
Given your specific needs (AI agents querying deadlines):
-
Apply for Jira OSS license (10 minutes, potentially free)
- If approved: Use Jira, accept JQL learning curve
- If denied: Continue to step 2
-
Trial Linear (14 days free)
- Test deadline automation
- Evaluate developer experience
- If it saves 2-3 hours/month: Worth $120-150
-
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.