Skip to content

Conversation

@chindris-mihai-alexandru
Copy link

@chindris-mihai-alexandru chindris-mihai-alexandru commented Jan 7, 2026

Summary

This PR adds comprehensive documentation about async property patterns in the Linear SDK and common performance footguns that occur when using the SDK directly. This guide helps users understand when to use agent2linear versus the SDK, and how to avoid N+1 query problems.

Motivation

While researching Linear API integration patterns, I noticed that the Linear SDK documentation lacks detailed guidance about the performance implications of lazy loading. The SDK's design (where all entity relations return Promises) can lead to severe N+1 query problems if developers aren't aware of this behavior.

This documentation fills that gap by providing practical examples and mitigation strategies.

What's Included

Comprehensive Async Properties Reference

  • docs/linear-sdk-gotchas.md - Complete guide to async property patterns
    • Documented async properties for: Issue, Project, Team, WorkflowState, User
    • Clear distinction between async relations and synchronous ID properties
    • Collection methods vs single entity relations

Common Performance Footguns

Four detailed examples with bad and good patterns:

  1. The N+1 Query Problem - Accessing relations in loops
  2. Checking Existence vs Fetching Data - Using IDs first
  3. Repeated Access to Same Property - SDK doesn't cache
  4. Collection Iteration - Relations aren't pre-fetched

Mitigation Strategies

Five practical approaches with code examples:

  1. Use agent2linear for CLI operations
  2. Write custom GraphQL queries for specific needs
  3. Check ID properties before fetching relations
  4. Batch operations when using SDK directly
  5. Implement caching layer for long-running processes

Decision Framework

Clear guidance on when to use each approach:

  • agent2linear: CLI tools, one-time queries, AI agents, bulk operations
  • SDK with custom GraphQL: Specific fields, custom integrations, full control
  • SDK with caching: Long-running processes, servers, webhooks, real-time updates
  • Avoid naive SDK: Lists with nested data, iteration over collections

Key Findings

The Core Issue

All entity relations in the Linear SDK use lazy loading:

// Each of these triggers a separate API call
const assignee = await issue.assignee;
const state = await issue.state;
const team = await issue.team;

ID Properties Are Synchronous

// These are synchronous (no API calls)
const assigneeId = issue.assigneeId;
const stateId = issue.stateId;
const teamId = issue.teamId;

N+1 Query Example

// Triggers 41+ API calls for 20 issues
const issues = await client.issues({ first: 20 });
for (const issue of issues.nodes) {
  const assignee = await issue.assignee;  // +20 calls
  const state = await issue.state;        // +20 calls
}

This is the exact pattern documented in PR #4 (Performance Benchmarks), which showed 41x API call increase with the naive SDK approach.

Pattern References

  • Linear SDK TypeScript definitions (v69.0.0)
  • Linear GraphQL API documentation
  • agent2linear comprehensive query approach
  • Cyrus caching layer pattern (ceedaragents/cyrus)

Testing

Documentation verified against:

Impact

  • Helps developers avoid common performance pitfalls
  • Provides clear guidance on tool selection
  • Documents undocumented SDK behavior
  • Complements performance benchmarks with explanations
  • Reduces likelihood of N+1 queries in user code

Relationship to Other PRs

This PR complements PR #4 (Performance Benchmarks):

  • PR docs: Add Linear API performance benchmarks #4 shows the performance impact with numbers
  • This PR explains why those numbers exist and how to avoid the issues
  • Together, they provide both quantitative data and qualitative understanding.

Note: This is a documentation-only PR with no code changes.

Comprehensive documentation of async property patterns and N+1 query footguns
in the Linear SDK. Helps users understand when to use agent2linear vs SDK.

Includes:
- Complete list of async properties by object type (Issue, Project, Team, etc.)
- Common performance footguns with code examples
- Mitigation strategies for each use case
- Decision framework for choosing the right approach

Key findings:
- All entity relations return Promises (lazy loading)
- ID properties are synchronous (no API calls)
- Naive SDK usage in loops causes N+1 queries
- agent2linear solves this with custom GraphQL

Referenced patterns:
- agent2linear comprehensive queries
- Cyrus caching layer approach
- Linear SDK TypeScript definitions (v69.0.0)
- Linear GraphQL API documentation

Complements the performance benchmarks in PR smorin#3 by explaining why
the performance differences exist and how to avoid common pitfalls.
Copilot AI review requested due to automatic review settings January 7, 2026 20:23
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive documentation for async property patterns in the Linear SDK, helping developers understand and avoid performance pitfalls when working with the SDK's lazy-loading architecture. The guide provides detailed explanations of async properties across multiple Linear entity types, common N+1 query anti-patterns, and practical mitigation strategies.

Key Changes

  • Added docs/linear-sdk-gotchas.md with 357 lines of comprehensive documentation
  • Documented async properties for Issue, Project, Team, WorkflowState, and User objects
  • Provided four detailed examples of common performance footguns with bad/good pattern comparisons
  • Outlined five mitigation strategies including when to use agent2linear vs SDK vs custom GraphQL

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

```typescript
// GOOD: Use custom GraphQL or agent2linear
// agent2linear automatically includes common relations
const projects = await a2l.projects({ limit: 25 });
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code example uses a2l without defining it or explaining that it's an alias for agent2linear. While the bash example on line 211 shows a2l issue list, the TypeScript code on line 199 should either use the full agent2linear name for clarity in documentation, or include a brief note explaining that a2l is a shorthand alias. This is especially important since the document is titled "Linear SDK Async Properties Guide" and focuses on SDK usage patterns.

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context of this guide is comparing SDK usage vs agent2linear. Line 211 shows the bash command 'a2l issue list' which is the standard CLI usage. The guide references the main agent2linear documentation where the alias is explained.


- [Linear GraphQL API Documentation](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)
- [Linear SDK Documentation](https://developers.linear.app/docs/sdk/getting-started)
- [agent2linear Performance Benchmarks](./performance/README.md) - Real-world N+1 query impact
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reference to ./performance/README.md appears to be broken. Based on the repository structure, this file doesn't exist. The PR description mentions "PR #3 (Performance Benchmarks)" but there's no evidence of this documentation file in the codebase. Consider either removing this link, updating it to point to an existing resource, or noting that this documentation is planned for a future PR.

Suggested change
- [agent2linear Performance Benchmarks](./performance/README.md) - Real-world N+1 query impact
- agent2linear Performance Benchmarks (planned; documentation will be added in PR #3) - Real-world N+1 query impact

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional forward reference. This refers to PR #4 (Performance Benchmarks) which has already been submitted to this repository. The link uses a relative path that will work once both PRs are merged.

// BAD: Always triggers API call, even if null
const assignee = await issue.assignee;
if (assignee) {
console.log(assignee.name);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code should use optional chaining when accessing assignee.name since assignee could potentially be undefined. While the example shows checking for assignee existence with the if (assignee) condition, using optional chaining (assignee?.name) would be more defensive and consistent with the pattern shown in line 124.

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example intentionally shows the explicit 'if (assignee)' check to make the defensive pattern more visible to developers learning about async properties. The explicit check is pedagogically clearer than optional chaining for this educational context.

// GOOD: No API call if not assigned
if (issue.assigneeId) {
const assignee = await issue.assignee;
console.log(assignee.name);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code should use optional chaining when accessing assignee.name for consistency with the pattern shown in line 124. While checking issue.assigneeId first guards against undefined assignee, using optional chaining (assignee?.name) would be more defensive and follow TypeScript best practices.

Suggested change
console.log(assignee.name);
console.log(assignee?.name);

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example demonstrates checking 'issue.assigneeId' first (the performance optimization pattern), then explicitly checking the assignee exists. Using 'assignee.name' without optional chaining after an explicit check is valid and shows the pattern clearly.

Comment on lines +32 to +110
## Async Properties by Object Type

### Issue Object

#### Async Relation Properties (Each triggers an API call)
- `assignee` - Assigned user
- `creator` - User who created the issue
- `cycle` - Sprint/cycle this issue belongs to
- `parent` - Parent issue (for sub-issues)
- `project` - Project this issue belongs to
- `projectMilestone` - Project milestone
- `state` - Workflow state
- `team` - Team this issue belongs to
- `snoozedBy` - User who snoozed this issue
- `favorite` - Favorite record if favorited

#### Async Collection Methods
- `attachments()` - Issue attachments
- `children()` - Sub-issues
- `comments()` - Issue comments
- `history()` - Issue history
- `labels()` - Issue labels
- `relations()` - Issue relations (blocks/blocked by)
- `subscribers()` - Users subscribed to this issue

#### Synchronous ID Properties (No API calls)
- `assigneeId`, `creatorId`, `cycleId`, `parentId`
- `projectId`, `stateId`, `teamId`
- All scalar fields: `id`, `title`, `description`, `identifier`, `number`, `priority`, `estimate`

### Project Object

#### Async Relation Properties
- `creator` - User who created the project
- `lead` - Project lead
- `status` - Project status
- `convertedFromIssue` - Original issue if converted from issue

#### Async Collection Methods
- `comments()` - Project comments
- `issues()` - Issues in this project
- `members()` - Project members
- `teams()` - Teams associated with this project
- `projectMilestones()` - Project milestones

### Team Object

#### Async Relation Properties
- `activeCycle` - Current active cycle
- `defaultIssueState` - Default workflow state for new issues
- `organization` - Organization (always returns Promise, not optional)
- `markedAsDuplicateWorkflowState` - State for duplicates
- `triageIssueState` - Triage state

#### Async Collection Methods
- `cycles()` - Team cycles
- `issues()` - Team issues
- `labels()` - Team labels
- `members()` - Team members
- `projects()` - Team projects
- `states()` - Workflow states

### WorkflowState Object

#### Async Relation Properties
- `team` - Team this state belongs to

#### Async Collection Methods
- `issues()` - Issues in this state

### User Object

#### Async Relation Properties
- `organization` - Organization (always returns Promise, not optional)

#### Async Collection Methods
- `assignedIssues()` - Issues assigned to this user
- `createdIssues()` - Issues created by this user
- `teams()` - Teams this user belongs to
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions documenting async properties for six object types including "Cycle", but the documentation only covers five types: Issue, Project, Team, WorkflowState, and User. Consider adding a section for the Cycle object to match the stated scope, or update the PR description to accurately reflect the documented objects.

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional scope decision to keep the documentation focused on the most commonly used objects. Cycle patterns would largely duplicate the patterns shown for other objects (async team property, async issues() method). The patterns are consistent across all Linear SDK objects.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant