-
Notifications
You must be signed in to change notification settings - Fork 2
docs: Add Linear SDK async properties guide #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
docs: Add Linear SDK async properties guide #5
Conversation
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.
There was a problem hiding this 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.mdwith 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 }); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
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.
| - [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 |
There was a problem hiding this comment.
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); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
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.
| console.log(assignee.name); | |
| console.log(assignee?.name); |
There was a problem hiding this comment.
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.
| ## 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 |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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
Common Performance Footguns
Four detailed examples with bad and good patterns:
Mitigation Strategies
Five practical approaches with code examples:
Decision Framework
Clear guidance on when to use each approach:
Key Findings
The Core Issue
All entity relations in the Linear SDK use lazy loading:
ID Properties Are Synchronous
N+1 Query Example
This is the exact pattern documented in PR #4 (Performance Benchmarks), which showed 41x API call increase with the naive SDK approach.
Pattern References
Testing
Documentation verified against:
Impact
Relationship to Other PRs
This PR complements PR #4 (Performance Benchmarks):
Note: This is a documentation-only PR with no code changes.