---
title: Linear
description: Read explicit issue relationships with workspace-scoped identity and visible coverage limits.
url: "https://issue-graph.dev/docs/linear"
docs_index: /llms.txt
canonical_url: "https://issue-graph.dev/docs/linear"
---

# Linear

> For an index of all documentation, see [/llms.txt](/llms.txt).

Start from one Linear issue to see its blockers, related work, parent and sub-issues, and attachment links.

## Connect Linear

Sign in once in the browser. You don't need an API key or workspace admin rights:

```bash
issue-graph linear login
```

`login` approves read-only access through Linear's MCP server using OAuth with PKCE. On macOS it saves the session in the Keychain, and elsewhere in a `0600` file under `~/.issue-graph/credentials`. `issue-graph linear status` and `issue-graph auth status` show what's connected, and `issue-graph linear logout` removes the session.

If your workspace allows personal API keys, you can set `LINEAR_API_KEY` with Read access instead, or `LINEAR_ACCESS_TOKEN` from an OAuth app. Environment keys take precedence over the session. Either way, the command sends only read queries and produces the same report with the same checks, and the report records which credential source it used.

```bash
issue-graph linear https://linear.app/your-workspace/issue/ENG-123/example --depth 1 --max-nodes 20
```

You can also pass an issue identifier or native UUID. Add `--workspace` to require a particular workspace:

```bash
issue-graph linear ENG-123 --workspace your-workspace --json
```

The workspace in an issue URL and the `--workspace` value must both match the authenticated organization. Each identity includes the organization ID and native issue ID, so identifiers from different workspaces can't collide.

## What the graph includes

- Explicit issue relations in both directions, with their native type and direction.
- Parent and sub-issue relationships.
- Attachment URLs and titles, including linked GitHub pull requests.
- Native workflow status, archive state, and update time.

By default the command lists attachment targets without fetching them, and it doesn't collect text mentions in descriptions or comments. A linked pull request doesn't show that an issue is resolved.

## Linked GitHub work

Add `--github` to read the GitHub issues and pull requests attached to each Linear issue. The command reads each target once through your authenticated `gh` and records its kind, title, state, and draft status:

```bash
issue-graph linear ENG-123 --depth 2 --github
```

When an issue is blocked by another issue, the blocker's line shows the state of its linked work, such as `← blocks: ENG-120 · vercel/app#412 PR open, draft`. The attachment rows in the dashboard show the same state.

GitHub targets are one hop. The command doesn't follow their references, and other attachment URLs stay unfetched. A target that can't be read stays visible as unavailable and marks its Linear issue as partial, so the command exits with `1`. Without `--github`, the command makes no GitHub requests.

Output is Markdown by default. `--json` prints the versioned report on stdout. Neither writes local files.

## Dashboard

Generate an HTML explorer and save the latest run for the issue's project:

```bash
issue-graph linear ENG-123 --workspace your-workspace --html linear.html --open
issue-graph dashboard --open
```

For a project dashboard, capture the project's full list of non-archived issues and read every selected issue's connections:

```bash
issue-graph linear --project <project-uuid> --workspace your-workspace --max-nodes 1000 --max-pages 100 --html linear.html --open
```

Project mode includes disconnected issues and empty projects. It paginates the issue inventory, then reads relations in both directions, parents, children, and attachments with bounded concurrency. Connections to issues outside the project stay visible, but those issues aren't fetched. Depth and hub expansion limits apply only to issue neighborhoods. `--max-pages` also bounds the project inventory, and reaching either limit marks the capture as incomplete. Rerun the command to refresh a saved capture.

Each dashboard entry shows one project. In an issue neighborhood, related issues from other projects count toward the coverage notice and stay in the `--json` output, but the dashboard doesn't draw them. Their edges stay on the issues that point to them.

The dashboard lists successful reads under **Snapshot details** and **Connection details**. An empty connection is a valid result. Scope exclusions, such as archived issues or text mentions, don't flag individual issues. Failed, truncated, or inconsistent reads keep their warnings.

The dashboard switches between saved Linear projects and GitHub repositories. Each Linear entry is labeled `Workspace / Project` and keyed by workspace and project UUID. Its view excludes issues from other projects but keeps boundary relationships visible. Issues without a project are scoped to their team. Older captures without project metadata are labeled `Unscoped capture`.

Linear rate-limits an API key to 2,500 requests and 3,000,000 complexity points per hour, and all of a user's keys share that quota. A full project capture can use a large part of it, because each issue takes at least one request and every extra page of a connection takes another. When the limit is reached, the command stops and prints when the window resets.

The project selector counts every non-archived open issue in the project, regardless of graph depth and node caps. Open means triage, backlog, unstarted, or started. A separate query reads up to 100 pages of 250 issue IDs across all teams in the project. `≥` marks a partial count, and `—` means the count is unavailable, which includes captures without a project. The tooltip records when the count was read. Saved dashboards don't refresh automatically. GitHub entries count open issues plus pull requests and show the breakdown in the tooltip.

For Linear, the dashboard shows issue identifiers, native workflow states, archived issues, relationship direction, attachment links, and read coverage. Explore and Swarm support relationships and depth. Impact and discussion ranking are GitHub-only, so they don't appear for Linear.

`--open` creates a temporary HTML file when you omit `--html`. HTML runs save the latest project model under `~/.issue-graph/dashboard`. Add `--no-save` to write or open the HTML without saving a dashboard entry. You can combine `--json` with HTML output, and file notices then go to stderr.

### Thematic clusters

By default, groups are connected components. To group by topic, print a task for the calling agent, then pass its reviewed JSON back:

```bash
issue-graph linear ENG-123 --cluster
issue-graph linear ENG-123 --clusters themes.json --html linear.html --open
```

The task includes native issue descriptions and explicit relationships from this project's capture. The command doesn't call a model itself. `--cluster` can't be combined with `--json`, and `--clusters` requires `--html` or `--open`. The file uses the same cluster shape as GitHub:

```json
{"clusters":[{"label":"Keyboard input","root_cause":"Proposed theme: negotiated input handling","members":[{"key":"<exact captured node key>"}]}]}
```

For Linear, `root_cause` holds a grouping rationale, not a verified cause, and Explore and Swarm label these groups as proposed themes. The command rejects keys from other projects, duplicate assignments, malformed input, verdicts, and cleanup actions. Issues without an assignment stay visible under Ungrouped. Clustering changes only the local grouping and leaves issue states, relationships, and project open totals intact.

A partial run still generates HTML and exits with `1`. Inspect the coverage notice and each issue's read details. The dashboard is a captured view with no live connection, and opening it sends nothing to Linear.

## Limits and coverage

| Option            | Default | Range                             |
| ----------------- | ------- | --------------------------------- |
| `--depth`         | 1       | 0 to 8                            |
| `--max-nodes`     | 80      | 1 to 1000                         |
| `--max-pages`     | 5       | 1 to 100 per connection per issue |
| `--hub-threshold` | 12      | 1 to 1000                         |
| `--concurrency`   | 4       | 1 to 32                           |

Each connection has its own cursor, and each page holds up to 50 entries. Archived issues are included, while archived relations and attachments are excluded. Hubs other than the seed are fetched but not expanded.

`coverageComplete` describes the requested depth and relationship sources. It's `false` when a node can't be read, a connection is incomplete, an issue changes while its pages are being read, or a node cap or hub stops traversal. Depth boundaries are listed separately. Reads across pages don't form an atomic snapshot.

The command exits with `0` for complete coverage within scope, `1` for partial coverage or a runtime failure, and `2` for invalid arguments. JSON includes per-node connection coverage and the known nodes that the node cap omitted.

## Library

The reader is a separate entry point, which keeps transport details out of the graph contract:

```ts
import { buildLinearGraph } from "issue-graph";
import { linearReader } from "issue-graph/transport/linear";

const reader = linearReader({ apiKey: process.env.LINEAR_API_KEY });
const report = await buildLinearGraph(reader, "ENG-123", {
  workspace: "your-workspace",
  maxDepth: 1,
  maxNodes: 20,
  maxPages: 3,
  hubThreshold: 12,
  concurrency: 4,
});
```

Use `buildLinearProject(reader, projectUuid, options)` for a project capture. Pass `linearDashboardModel(report)` to `renderDashboard()` to combine Linear and GitHub models in one HTML page. `issue-graph` exports all of these functions.

An authenticated Linear MCP session is separate from these credentials. An MCP capture should record its actual source and the relationship categories it supports in the report scope. A returned empty array is a successful read, and a category that wasn't requested or isn't exposed belongs in the scope notes. Never record a failed requested read as a scope exclusion.

The reader accepts a per-request timeout and bounded retry options. It follows Linear's rate-limit reset headers: it waits for a reset that falls within the retry budget, and fails with the reset time when the reset is later. Reports never include credentials or raw upstream error messages.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)