-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,357 @@ | ||||||
| # Linear SDK Async Properties Guide | ||||||
|
|
||||||
| This guide documents async properties in the Linear SDK and common performance footguns when using the SDK directly instead of agent2linear. | ||||||
|
|
||||||
| ## Overview | ||||||
|
|
||||||
| The Linear SDK uses lazy loading for all entity relationships. When you access a property that references another Linear entity (User, Team, Project, etc.), the SDK returns a Promise that triggers an API call. | ||||||
|
|
||||||
| This design follows GraphQL best practices for reducing over-fetching, but can lead to N+1 query problems if not handled carefully. | ||||||
|
|
||||||
| ## The Core Pattern | ||||||
|
|
||||||
| All async properties in the Linear SDK return `LinearFetch<T>`, which is an alias for `Promise<T>`. | ||||||
|
|
||||||
| ### Convention | ||||||
|
|
||||||
| - **Single entity relations**: Getter properties that return Promises | ||||||
| ```typescript | ||||||
| get assignee(): LinearFetch<User> | undefined | ||||||
| ``` | ||||||
|
|
||||||
| - **Collections**: Methods that return Promises | ||||||
| ```typescript | ||||||
| comments(variables?): LinearFetch<CommentConnection> | ||||||
| ``` | ||||||
|
|
||||||
| - **ID properties**: Always synchronous (no API calls) | ||||||
| ```typescript | ||||||
| get assigneeId(): string | undefined | ||||||
| ``` | ||||||
|
|
||||||
| ## 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 | ||||||
|
|
||||||
| ## Common Performance Footguns | ||||||
|
|
||||||
| ### 1. The N+1 Query Problem | ||||||
|
|
||||||
| The most common footgun is accessing async properties in loops: | ||||||
|
|
||||||
| ```typescript | ||||||
| // BAD: Triggers 1 + N API calls | ||||||
| const issues = await client.issues({ first: 50 }); | ||||||
| for (const issue of issues.nodes) { | ||||||
| const assignee = await issue.assignee; // +1 API call per issue | ||||||
| const state = await issue.state; // +1 API call per issue | ||||||
| console.log(`${issue.title}: ${assignee?.name} (${state?.name})`); | ||||||
| } | ||||||
| // Total: 1 (issues) + 50 (assignees) + 50 (states) = 101 API calls | ||||||
| ``` | ||||||
|
|
||||||
| ```typescript | ||||||
| // GOOD: Use agent2linear or custom GraphQL query | ||||||
| const result = await client.client.rawRequest(` | ||||||
| query { | ||||||
| issues(first: 50) { | ||||||
| nodes { | ||||||
| title | ||||||
| assignee { name } | ||||||
| state { name } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| `); | ||||||
| // Total: 1 API call | ||||||
| ``` | ||||||
|
|
||||||
| ### 2. Checking Existence vs Fetching Data | ||||||
|
|
||||||
| Always check ID properties first before fetching related entities: | ||||||
|
|
||||||
| ```typescript | ||||||
| // BAD: Always triggers API call, even if null | ||||||
| const assignee = await issue.assignee; | ||||||
| if (assignee) { | ||||||
| console.log(assignee.name); | ||||||
|
||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ```typescript | ||||||
| // GOOD: No API call if not assigned | ||||||
| if (issue.assigneeId) { | ||||||
| const assignee = await issue.assignee; | ||||||
| console.log(assignee.name); | ||||||
|
||||||
| 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.
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.
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.
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.