Packages

Elixir client for the Linear GraphQL API.

Current section

Files

Jump to
linear_client lib linear.ex
Raw

lib/linear.ex

defmodule Linear do
@moduledoc """
Elixir client for the [Linear](https://linear.app) GraphQL API.
## Quick start
client = Linear.client(api_key: "lin_api_...")
{:ok, viewer} = Linear.viewer(client)
IO.inspect(viewer["name"])
{:ok, issues} = Linear.list_issues(client, team_id: "TEAM-UUID")
Enum.each(issues, &IO.puts(&1["title"]))
## Raw GraphQL
{:ok, data} = Linear.query(client, "query { viewer { id name email } }")
{:ok, data} = Linear.query(client,
"query($id: String!) { issue(id: $id) { title state { name } } }",
variables: %{"id" => "ABC-123"}
)
## Authentication
Two auth modes are supported:
- **API key**: `Linear.client(api_key: "lin_api_...")` -- sends `Authorization: <key>`
- **OAuth bearer**: `Linear.client(access_token: "oauth_token")` -- sends `Authorization: Bearer <token>`
"""
alias Linear.Client
@type client :: Client.t()
@doc """
Create a new Linear API client.
## Options
* `:api_key` - Linear personal API key
* `:access_token` - OAuth2 access token (bearer auth)
* `:endpoint` - API endpoint (default `"https://api.linear.app/graphql"`)
* `:timeout` - HTTP request timeout in ms (default `30_000`)
"""
@spec client(keyword()) :: client()
def client(opts \\ []) do
Client.new(opts)
end
@doc """
Execute a raw GraphQL query or mutation.
## Options
* `:variables` - GraphQL variables map (default `%{}`)
* `:operation_name` - optional GraphQL operation name
"""
@spec query(client(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def query(client, query_string, opts \\ []) do
Client.query(client, query_string, opts)
end
@doc """
Execute a GraphQL mutation. Alias for `query/3` with clearer intent.
"""
@spec mutate(client(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def mutate(client, mutation, opts \\ []) do
Client.query(client, mutation, opts)
end
@doc """
Fetch the authenticated user.
Returns the `viewer` object with `id`, `name`, `email`, and `displayName`.
"""
@spec viewer(client(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate viewer(client, opts \\ []), to: Linear.Queries
@doc """
List teams in the workspace.
Returns a list of team nodes.
"""
@spec list_teams(client(), keyword()) :: {:ok, [map()]} | {:error, term()}
defdelegate list_teams(client, opts \\ []), to: Linear.Queries
@doc """
List issues, optionally filtered by team or project.
## Options
* `:team_id` - filter by team UUID
* `:project_id` - filter by project UUID
* `:state_names` - filter by workflow state names (list of strings)
* `:assignee_id` - filter by assignee UUID
* `:first` - page size (default `50`)
* `:after` - pagination cursor
* `:include_archived` - include archived issues (default `false`)
"""
@spec list_issues(client(), keyword()) :: {:ok, [map()]} | {:error, term()}
defdelegate list_issues(client, opts \\ []), to: Linear.Queries
@doc """
Fetch a single issue by ID (UUID or shorthand like `"ABC-123"`).
"""
@spec get_issue(client(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate get_issue(client, issue_id, opts \\ []), to: Linear.Queries
@doc """
Create an issue.
## Required fields in `attrs`
* `"teamId"` - team UUID
* `"title"` - issue title
## Optional fields
* `"description"` - markdown body
* `"assigneeId"` - assignee UUID
* `"stateId"` - workflow state UUID
* `"priority"` - integer priority (0=none, 1=urgent, 4=low)
* `"labelIds"` - list of label UUIDs
* `"projectId"` - project UUID
"""
@spec create_issue(client(), map(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate create_issue(client, attrs, opts \\ []), to: Linear.Mutations
@doc """
Update an existing issue.
`issue_id` can be a UUID or shorthand like `"ABC-123"`.
`attrs` is a map of fields to update (same keys as `create_issue/2`).
"""
@spec update_issue(client(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate update_issue(client, issue_id, attrs, opts \\ []), to: Linear.Mutations
@doc """
Create a comment on an issue.
"""
@spec create_comment(client(), String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate create_comment(client, issue_id, body, opts \\ []), to: Linear.Mutations
@doc """
Update an issue's workflow state by state name.
Resolves the state name to a state ID via the issue's team, then applies the update.
"""
@spec transition_issue(client(), String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate transition_issue(client, issue_id, state_name, opts \\ []), to: Linear.Mutations
@doc """
List workflow states, optionally filtered by team.
## Options
* `:team_id` - filter by team UUID
"""
@spec list_workflow_states(client(), keyword()) :: {:ok, [map()]} | {:error, term()}
defdelegate list_workflow_states(client, opts \\ []), to: Linear.Queries
@doc """
List projects in the workspace.
## Options
* `:first` - page size (default `50`)
* `:after` - pagination cursor
* `:include_archived` - include archived (default `false`)
"""
@spec list_projects(client(), keyword()) :: {:ok, [map()]} | {:error, term()}
defdelegate list_projects(client, opts \\ []), to: Linear.Queries
@doc """
Auto-paginate a query that returns a connection with `nodes` and `pageInfo`.
Calls `query_fn` repeatedly, following `endCursor`, and accumulates all nodes.
`query_fn` receives an `after` cursor (or `nil` for the first page) and must
return `{:ok, %{"nodes" => [...], "pageInfo" => %{...}}}` or `{:error, reason}`.
"""
@spec paginate((String.t() | nil -> {:ok, map()} | {:error, term()})) :: {:ok, [map()]} | {:error, term()}
defdelegate paginate(query_fn), to: Linear.Pagination
end