Linear API
The issue tracking tool you'll enjoy using
Linear helps streamline software projects, sprints, tasks, and
bug tracking. It's built for high-performance teams.
🦋 Your First Query
Connect to the Linear API and interact with your data in a few steps:
-
Install the Linear Client
Using npm:
npm install @linear/sdk
Or yarn:
yarn add @linear/sdk
-
Create a Linear API authentication token
Login or signup to Linear
Go to Settings > Api
Create a Personal API Key
-
Create a Linear Client
Using the API key created in step 2:
import { LinearClient } from '@linear/sdk'
const client = new LinearClient({
apiKey: YOUR_PERSONAL_API_KEY
})
-
Query for your issues
Using async await syntax:
async function getMyIssues() {
const me = await linearClient.viewer;
const myIssues = await me?.assignedIssues();
if (myIssues?.nodes?.length) {
myIssues?.nodes?.map(issue => console.log(`${me?.displayName} has issue: ${issue?.title}`));
} else {
console.log(`${me?.displayName} has no issues`);
}
}
getMyIssues();
Or promises:
linearClient.viewer.then(me => {
return me?.assignedIssues()?.then(myIssues => {
if (myIssues?.nodes?.length) {
myIssues?.nodes?.map(issue => console.log(`${me?.displayName} has issue: ${issue?.title}`));
} else {
console.log(`${me?.displayName} has no issues`);
}
});
});
🦄 Features
The Linear Client exposes the Linear GraphQL schema through strongly typed models and operations.
All operations return models, which can be used to perform operations for other models.
Typescript
All types are accessible through the Linear Client package. It is written in Typescript:
import { LinearClient, LinearFetch, User } from "@linear/sdk";
const linearClient = new LinearClient({ apiKey });
async function getCurrentUser(): LinearFetch<User> {
return linearClient.viewer;
}
Query
Some models can be fetched from the Linear Client without any arguments:
const me = await linearClient.viewer;
const org = await linearClient.organization;
Other models are exposed as connections, and return a list of nodes:
const issues = await linearClient.issues();
const firstIssue = issues?.nodes?.[0];
All required variables are passed as the first arguments:
const user = await linearClient.user("user-id");
const team = await linearClient.team("team-id");
Any optional variables are passed into the last argument as an object:
const fiftyProjects = await linearClient.projects({ first: 50 });
const allComments = await linearClient.comments({ includeArchived: true });
Most models expose operations to fetch other models:
const me = await linearClient.viewer;
const myIssues = await me?.assignedIssues();
const myFirstIssue = myIssues?.nodes?.[0];
const myFirstIssueComments = await myFirstIssue?.comments();
const myFirstIssueFirstComment = myFirstIssueComments?.nodes?.[0];
const myFirstIssueFirstCommentUser = await myFirstIssueFirstComment?.user;
NOTE: Parenthesis is required only if the operation takes an optional variables object.
Mutate
To create a model, call the Linear Client mutation and pass in the input object:
const teams = await linearClient.teams();
const team = teams?.nodes?.[0];
if (team?.id) {
await linearClient.issueCreate({ teamId: team.id, title: "My Created Issue" });
}
To update a model, call the Linear Client mutation and pass in the required variables and input object:
const me = await linearClient.viewer;
if (me?.id) {
await linearClient.userUpdate(me.id, { displayName: "My Updated Name" });
}
All mutations are exposed in the same way:
const projects = await linearClient.projects();
const project = projects?.nodes?.[0];
if (project?.id) {
await linearClient.projectArchive(project.id);
}
Mutations will often return a success boolean and the mutated entity:
const commentPayload = await linearClient.commentCreate({ issueId: "some-issue-id" });
if (commentPayload?.success) {
return commentPayload.comment;
} else {
return new Error("Failed to create comment");
}
Paginate
Connection models have helpers to fetch the next and previous pages of results:
const issues = await linearClient.issues({ after: "some-issue-cursor", first: 10 });
const nextIssues = await issues?.fetchNext();
const prevIssues = await issues?.fetchPrevious();
Pagination info is exposed and can be passed to the query operations. This uses the Relay Connection spec:
const issues = await linearClient.issues();
const hasMoreIssues = issues?.pageInfo?.hasNextPage;
const issuesEndCursor = issues?.pageInfo?.endCursor;
const moreIssues = await linearClient.issues({ after: issuesEndCursor, first: 10 });
Results can be ordered using the orderBy
optional variable:
import { LinearDocument } from "@linear/sdk";
const issues = await linearClient.issues({ orderBy: LinearDocument.PaginationOrderBy.UpdatedAt });
File Upload
Create a file upload URL, upload the file to external storage, and attach the file by asset URL:
import { Issue, LinearFetch } from "@linear/sdk";
async function createIssueWithFile(title: string, file: File, uploadData: RequestInit): LinearFetch<Issue> {
const uploadPayload = await linearClient.fileUpload(file.type, file.name, file.size);
const authHeader = uploadPayload?.uploadFile?.headers?.[0];
const uploadUrl = uploadPayload?.uploadFile?.uploadUrl;
if (uploadUrl && authHeader?.key && authHeader?.value) {
await fetch(uploadUrl, {
method: "PUT",
headers: {
[authHeader.key]: authHeader.value,
"cache-control": "max-age=31536000",
},
...uploadData,
});
const assetUrl = uploadPayload?.uploadFile?.assetUrl;
if (assetUrl) {
const issuePayload = await linearClient.issueCreate({
title,
description: `Attached file: ![${assetUrl}](${encodeURI(assetUrl)})`,
teamId: "team-id",
});
return issuePayload?.issue;
}
}
return undefined;
}
Error
Errors can be caught and interrogated by wrapping the operation in a try catch block:
async function createComment(input: LinearDocument.CommentCreateInput): LinearFetch<Comment | UserError> {
try {
const commentPayload = await linearClient.commentCreate(input);
return commentPayload?.comment;
} catch (error) {
throw error;
}
}
Or by catching the error thrown from a calling function:
async function archiveFirstIssue(): LinearFetch<ArchivePayload> {
const me = await linearClient.viewer;
const issues = await me?.assignedIssues();
const firstIssue = issues?.nodes?.[0];
if (firstIssue?.id) {
const payload = await linearClient.issueArchive(firstIssue.id);
return payload;
} else {
return undefined;
}
}
archiveFirstIssue().catch(error => {
throw error;
});
The parsed error type can be compared to determine the course of action:
import { InvalidInputLinearError, LinearError, LinearErrorType } from '@linear/sdk'
import { UserError } from './custom-errors'
const input = { name: "Happy Team" };
createTeam(input).catch(error => {
if (error instanceof InvalidInputLinearError) {
return new UserError(input, error);
} else {
throw error;
}
});
Information about the request
resulting in the error is attached if available:
run().catch(error => {
if (error instanceof LinearError) {
console.error("Failed query:", error.query);
console.error("With variables:", error.variables);
}
throw error;
});
Information about the response
is attached if available:
run().catch(error => {
if (error instanceof LinearError) {
console.error("Failed HTTP status:", error.status);
console.error("Failed response data:", error.data);
}
throw error;
});
Any GraphQL errors
are parsed and added to an array:
run().catch(error => {
if (error instanceof LinearError) {
error.errors?.map(graphqlError => {
console.log("Error message", graphqlError.message);
console.log("LinearErrorType of this GraphQL error", graphqlError.type);
console.log("Error due to user input", graphqlError.userError);
console.log("Path through the GraphQL schema", graphqlError.path);
});
}
throw error;
});
The raw
error returned by the graphql-request client is still available:
run().catch(error => {
if (error instanceof LinearError) {
console.log("The original error", error.raw);
}
throw error;
});
Limitations
This functionality is currently under development and must be performed by the consumer:
- Search and filtering of collections
☀️ API Reference
The Linear Client is generated from the Linear GraphQL schema.
You can use any GraphQL client to introspect and explore the schema. Such as Insomnia or GraphQL Playground.
Point the GraphQL client to the Linear production API endpoint:
https://api.linear.app/graphql
🌵 OAuth Authentication
Linear supports OAuth2 authentication, which is recommended if you're building applications to integrate with Linear.
NOTE: It is highly recommended you create a workspace for the purpose of managing the OAuth2 Application. As each admin user will have access.
-
Create an OAuth2 application in Linear
Create a new OAuth2 Application
Configure the redirect callback URLs to your application
-
Redirect user access requests to Linear
When authorizing a user to the Linear API, redirect to an authorization URL with correct parameters and scopes:
GET https://linear.app/oauth/authorize
Parameters
client_id
(required) - Client ID provided when you create the OAuth2 Applicationredirect_uri
(required) - Redirect URIresponse_type=code
(required) - Expected response typescope
(required) - Comma separated list of scopes (listed below)state
(optional) - Prevents CSRF attacks and should always be supplied. Read more about it here
Scopes
read
- (Default) Read access for the user's account. This scope will always be present.write
- Write access for the user's account.issues:create
- Special scope to only gain access in order to create new issues. If this is the main reason for your application, you should ask for this scope instead of write
admin
- Full access to admin level endpoints. You should never ask for this permission unless it's absolutely needed
Example
GET https://linear.app/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URL&state=SECURE_RANDOM&scope=read
GET https://linear.app/oauth/authorize?client_id=client1&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback&response_type=code&scope=read,write
-
Handle the redirect URLs you specified in the OAuth2 Application
Once the user approves your application they will be redirected back to your application, with the OAuth authorization code
in the URL params.
Any state
parameter you specified in step 2 will also be returned in the URL params and must match the value specified in step 2. If the values do not match, the request should not be trusted.
Example
GET https://example.com/oauth/callback?code=9a5190f637d8b1ad0ca92ab3ec4c0d033ad6c862&state=b1ad0ca92
-
Exchange code
for an access token
After receiving the code
, you can exchange it for a Linear API access token:
POST https://api.linear.app/oauth/token
Parameters
code
- (required) Authorization code from the previous stepredirect_uri
- (required) Same redirect URI which you used in the previous stepclient_id
- (required) Application's client IDclient_secret
- (required) Application's client secretgrant_type=authorization_code
(required)
Response
After a successful request, a valid access token will be returned in the response:
{
"access_token": "00a21d8b0c4e2375114e49c067dfb81eb0d2076f48354714cd5df984d87b67cc",
"token_type": "Bearer",
"expires_in": 315705599,
"scope": [
"read",
"write"
]
}
-
Make an API request
Initialize the Linear Client with the access token:
const client = new LinearClient({ accessToken: response.access_token })
const me = await client.viewer
-
Revoke an access token
To revoke a user's access to your application pass the access token as Bearer token in the authorization header (Authorization: Bearer <ACCESS_TOKEN>
) or as the access_token
form field:
POST https://api.linear.app/oauth/revoke
Response
Expected HTTP status:
200
- token was revoked400
- unable to revoke token (e.g. token was already revoked)401
- unable to authenticate with the token
🌈 Find Help
If you run into problems, have questions or suggestions:
Both options are available through the user menu in Linear.
🌊 Advanced
The Linear Client wraps the Linear SDK, provides a graphql-request client, and parses errors.
Request Configuration
The graphql-request client can be configured by passing the RequestInit
object to the Linear Client constructor:
const linearClient = new LinearClient({ apiKey, headers: { "my-header": "value" } });
Raw GraphQL Client
The graphql-request client is accessible through the Linear Client:
const graphQLClient = linearClient.client;
graphQLClient.setHeader("my-header", "value");
Raw GraphQL Queries
The Linear GraphQL API can be queried directly by passing a raw GraphQL query to the graphql-request client:
const graphQLClient = linearClient.client;
const cycle = await graphQLClient.rawRequest(
gql`
query cycle($id: String!) {
cycle(id: $id) {
id
name
completedAt
}
}
`,
{ id: "cycle-id" }
);
Custom GraphQL Client
In order to use a custom GraphQL Client, the Linear SDK must be extended and provided with a request function:
import { LinearError, LinearFetch, LinearRequest, LinearSdk, parseLinearError, UserConnection } from "@linear/sdk";
import { DocumentNode, GraphQLClient, print } from "graphql";
import { CustomGraphqlClient } from "./graphql-client";
const customGraphqlClient = new CustomGraphqlClient("https://api.linear.app/graphql", {
headers: { Authorization: apiKey },
});
const customLinearRequest: LinearRequest = <Response, Variables>(
document: DocumentNode,
variables?: Variables
) => {
return customGraphqlClient.request<Response>(print(document), variables).catch(error => {
throw parseLinearError(error);
});
};
class CustomLinearClient extends LinearSdk {
public constructor() {
super(customLinearRequest);
}
}
const customLinearClient = new CustomLinearClient();
async function getUsers(): LinearFetch<UserConnection> {
const users = await customLinearClient.users();
return users;
}
🔥 Contribute
The Linear Client uses custom GraphQL Code Generator plugins to produce a typed SDK for all operations and models exposed by the Linear production API.
Structure
This monorepo uses yarn workspaces
and lerna
to publish packages.
Generated code uses file prefix _generated
and should never be manually updated.
Open source packages:
- sdk - The Linear Client SDK for interacting with the Linear GraphQL API
- import - Import tooling for uploading from other systems
- codegen-doc - GraphQL codegen plugin to generate GraphQL fragments and documents
- codegen-sdk - GraphQL codegen plugin to generate Typescript SDK from fragments and documents
- codegen-test - GraphQL codegen plugin to generate a jest test for the Typescript SDK
Get Started
# install dependencies
yarn
# link package dependencies
yarn bootstrap
# build all packages
yarn build
# test all packages
yarn test
# update the schema from the production API
yarn schema
# create changeset for generating CHANGELOG.md
yarn changeset
Plugin Flow
- The @linear/codegen-doc plugin is used by codegen.doc.yml to generate fragments and documents
- The @linear/codegen-sdk plugin is used by codegen.sdk.yml to generate the typed Linear SDK
- The @linear/codegen-test plugin is used by codegen.test.yml to generate a typed jest test
License
Licensed under the MIT License.