> ## Documentation Index
> Fetch the complete documentation index at: https://support.configview.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Linear setup

ConfigView pulls Linear data through **one Personal API key**. The key inherits the permissions of the user who created it, so the inventory follows that user's access — not the workspace as a whole.

You will end up with **1 secret** in ConfigView (`LINEAR_API_KEY`) when setup is complete.

> ## ⚠️ Required: the key must be created by a Workspace Admin
>
> Linear's Personal API key carries the creating user's permissions. **ConfigView requires the key to come from a Workspace Admin** — not a regular member, and not a guest.
>
> With a member-level key:
>
> * `OAuth Applications`, `Organization Invites` and `Audit Entries` return authorization errors and are recorded as empty
> * Private teams the member cannot see are missing from `Teams`, and every table derived from them is short by that team's rows
> * `Integrations` and `Webhooks` come back empty or partial
> * Seat and plan data on `Organization` is present but the surrounding governance picture is not
>
> None of this raises an error — ConfigView records a gated capability as an empty snapshot, exactly as it does for a workspace on a plan that never had the feature. That is the right behaviour for a mixed estate, but it means a member-tier key produces a quietly incomplete inventory. If no admin is available, ask the customer to (a) elevate an existing user temporarily, or (b) create a dedicated service user, promote it to Admin, then create the key from that account.

***

## Step 1: Create the Personal API key

1. Sign in to Linear **as a Workspace Admin** (see the callout above — this is not optional)
2. Verify your role: **Settings** → **Members**. Your row must read **Admin**. If it reads **Member** or **Guest**, stop and use the workaround above.
3. Go to **Settings** → **Security & access** → **Personal API keys** (or open [linear.app/settings/account/security](https://linear.app/settings/account/security))
4. Click **New API key**, give it a label such as `ConfigView`
5. Copy the key (starts with `lin_api_`). **Linear shows it once** — if you lose it you must create a new one.

> **Store it bare.** Copy the key on its own, with no `Bearer ` prefix. Linear's personal API keys go in the `Authorization` header *without* `Bearer` — only OAuth access tokens use that prefix. A key stored as `Bearer lin_api_...` fails with an HTTP 400 that reads like a malformed query. The health check detects this specific mistake and names it.

> **OAuth alternative.** Linear also supports OAuth 2.0 for multi-tenant apps. ConfigView is single-tenant per satellite, so the Personal API key is the right choice — simpler, and it gets the larger complexity budget (3,000,000 points/hour against an OAuth app's 2,000,000).

***

## Step 2: Add the secret to ConfigView

1. Go to your ConfigView dashboard: `https://{companyname}.configview.com/admin/secret/`
2. Click **Add Secret**
3. **Secret name:** `LINEAR_API_KEY`
4. **Secret value:** Paste the `lin_api_...` key from Step 1
5. Click **Save**

***

## Step 3: Enable the Linear scripts

1. Go to: `https://{companyname}.configview.com/admin/cron/`
2. You should see **linear** in the list of available apps
3. Enable the scripts you want. **No script depends on another** — Linear serves one GraphQL endpoint and every collector queries it directly, so they can be enabled in any order and run in parallel.

### Identity & licensing

| Script                   | Notes                                                                                                                                                                                         |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Organization**         | The workspace record: plan, seat count, SSO/SCIM posture, trial dates. `seats` vs `userCount` is the licence reconciliation. `subscription` is null on Free — expect NULL seat columns there. |
| **Users**                | Every member, **including deactivated and archived accounts**. Filter on `active` in your query, not at ingestion — a disabled user still holding a seat is the point.                        |
| **Teams**                | Visibility (private/public/restricted), SCIM management, cycle and triage settings.                                                                                                           |
| **Team Memberships**     | The user × team join, with per-team owner flags. Denormalised with email and account status.                                                                                                  |
| **Organization Invites** | Pending and historical invitations. Each unaccepted invite becomes a billable seat on acceptance.                                                                                             |
| **External Users**       | Customers and collaborators with no paid seat. They are **not** in `linear_users`.                                                                                                            |
| **Project Members**      | Who can see each project. See the nested-page caveat below.                                                                                                                                   |

### Security & governance

| Script                 | Notes                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **OAuth Applications** | Third-party apps holding delegated access, with redirect URIs and grant types. **Admin key required.**                       |
| **Integrations**       | Connected services (Slack, GitHub, Figma…) and who connected each.                                                           |
| **Webhooks**           | Outbound event feeds. Records **whether** each is signed, never the signing secret itself.                                   |
| **IP Restrictions**    | Configured IP/CIDR restrictions. **Enterprise-only** — empty elsewhere.                                                      |
| **Audit Entries**      | Actor, IP and country per action. **Enterprise-only.** Capped at the 10,000 most recent entries; see the caps section below. |

### Work structure

| Script                  | Notes                                                                                     |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| **Projects**            | Lead, status, health, dates, progress. `health` + `last_update_at` is the staleness lens. |
| **Project Milestones**  | Target dates and overdue status.                                                          |
| **Project Updates**     | The written updates behind each health rating, with Linear's own `is_stale` flag.         |
| **Project Statuses**    | Status definitions — what makes `linear_projects.status_name` meaningful.                 |
| **Project Labels**      | Project label taxonomy with retirement and last-applied dates.                            |
| **Initiatives**         | The layer above projects: owner, status, health, visibility.                              |
| **Initiative Projects** | The initiative × project join (a project can sit under several).                          |

### Workflow configuration

| Script                      | Notes                                                                |
| --------------------------- | -------------------------------------------------------------------- |
| **Cycles**                  | Sprints per team, with active/past/future flags.                     |
| **Workflow States**         | Every team's board columns. Group by `state_type`, not `state_name`. |
| **Issue Labels**            | Label taxonomy, workspace-wide and team-scoped.                      |
| **Templates**               | Issue, project and document templates with last-applied dates.       |
| **Custom Views**            | Saved views and their filters. `shared = 1` means workspace-visible. |
| **Triage Responsibilities** | Who is on triage duty per team.                                      |

### Work items

| Script              | Notes                                                                                    |
| ------------------- | ---------------------------------------------------------------------------------------- |
| **Issues**          | The main work table. Archived issues are **excluded**; capped at 20,000. See caps below. |
| **Issue Relations** | Blocks / duplicate / related edges.                                                      |
| **Comments**        | Across issues, projects, updates and initiatives. Capped at 20,000.                      |
| **Attachments**     | Links out to GitHub, Slack, Figma, Zendesk and uploaded files. Capped at 20,000.         |
| **Documents**       | Linear documents and what each is attached to.                                           |

***

## Rate limits and collection caps

Linear meters **two** budgets per API key, and either can throttle a run:

| Budget       | Per personal API key  | Per OAuth app         |
| ------------ | --------------------- | --------------------- |
| Requests     | 5,000/hour (\~83/min) | 5,000/hour            |
| Complexity   | 3,000,000 points/hour | 2,000,000 points/hour |
| Single query | 10,000 points maximum | 10,000 points maximum |

**Complexity is the limit that actually bites.** It scales with page size × fields × nesting, so the collectors use different page sizes rather than one folder-wide constant — 50 for wide types like issues, 100–250 for narrow configuration tables. ConfigView paces requests well under the ceiling and reads the live `X-RateLimit-Complexity-Remaining` header on every response, pausing until the window resets when the workspace is close to exhausting it.

Three collectors are **deliberately capped**, because the underlying data is unbounded:

| Script            | Cap                | Why                                                                                                                        |
| ----------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| **Issues**        | 20,000 most recent | Archived issues excluded. A mature workspace's full history would page for hours and exhaust the hourly complexity budget. |
| **Comments**      | 20,000 most recent | Same reasoning.                                                                                                            |
| **Attachments**   | 20,000 most recent | Same reasoning.                                                                                                            |
| **Audit Entries** | 10,000 most recent | An audit log grows without limit.                                                                                          |

When a cap is reached the run log prints an explicit `TRUNCATED` warning naming the collector — a partial snapshot is never reported as if it were the whole workspace. Raise the limit in `tools/linear_specs.py` (`max_pages`) and regenerate if a customer needs the full history.

**Project Members** has a different kind of cap. Linear serves a project's member list as a nested connection, which has no cursor of its own, so only the first **100 members per project** are returned. The collector prints a warning naming any project that hits that ceiling.

***

## What gets collected

30 collectors, one table each. Every table is a **per-run snapshot** stamped with `run_at`, and each collector prunes older snapshots after a successful insert — so query the latest with:

```sql theme={null}
WHERE run_at = (SELECT MAX(run_at) FROM <table>)
```

| Table                            | Script                  | Key columns                                                                                                    |
| -------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------- |
| `linear_organization`            | Organization            | org\_id, name, url\_key, user\_count, seats, subscription\_type, saml\_enabled, scim\_enabled, trial\_ends\_at |
| `linear_ip_restrictions`         | IP Restrictions         | org\_id, restriction\_type, ip\_range, enabled                                                                 |
| `linear_users`                   | Users                   | user\_id, email, name, active, admin, owner, guest, is\_app, last\_seen, identity\_provider\_id                |
| `linear_teams`                   | Teams                   | team\_id, team\_key, name, visibility, scim\_managed, issue\_count, cycles\_enabled, triage\_enabled           |
| `linear_team_memberships`        | Team Memberships        | membership\_id, team\_id, team\_key, user\_id, user\_email, is\_owner                                          |
| `linear_organization_invites`    | Organization Invites    | invite\_id, email, invite\_role, is\_external, accepted\_at, expires\_at, inviter\_email                       |
| `linear_external_users`          | External Users          | external\_user\_id, email, name, last\_seen                                                                    |
| `linear_integrations`            | Integrations            | integration\_id, service, creator\_email, team\_key                                                            |
| `linear_oauth_applications`      | OAuth Applications      | application\_id, client\_id, name, developer, distribution, redirect\_uris, webhook\_url                       |
| `linear_webhooks`                | Webhooks                | webhook\_id, label, url, enabled, resource\_types, has\_secret, creator\_email                                 |
| `linear_audit_entries`           | Audit Entries           | entry\_id, entry\_type, actor\_email, ip, country\_code, created\_at                                           |
| `linear_projects`                | Projects                | project\_id, name, status\_type, health, lead\_email, target\_date, progress, last\_update\_at                 |
| `linear_project_members`         | Project Members         | project\_id, project\_name, user\_id, user\_email, user\_active                                                |
| `linear_project_milestones`      | Project Milestones      | milestone\_id, project\_id, name, milestone\_status, target\_date                                              |
| `linear_project_updates`         | Project Updates         | update\_id, project\_id, health, is\_stale, user\_email, body\_excerpt                                         |
| `linear_project_statuses`        | Project Statuses        | status\_id, name, status\_type, position                                                                       |
| `linear_project_labels`          | Project Labels          | label\_id, name, retired\_at, last\_applied\_at, team\_key                                                     |
| `linear_initiatives`             | Initiatives             | initiative\_id, name, initiative\_status, health, owner\_email, visibility, target\_date                       |
| `linear_initiative_projects`     | Initiative Projects     | link\_id, initiative\_id, project\_id, project\_status\_type                                                   |
| `linear_cycles`                  | Cycles                  | cycle\_id, team\_key, cycle\_number, starts\_at, ends\_at, is\_active, progress                                |
| `linear_workflow_states`         | Workflow States         | state\_id, team\_key, name, state\_type, position                                                              |
| `linear_labels`                  | Issue Labels            | label\_id, name, team\_key, retired\_at, last\_applied\_at                                                     |
| `linear_templates`               | Templates               | template\_id, name, template\_type, team\_key, last\_applied\_at                                               |
| `linear_custom_views`            | Custom Views            | view\_id, name, model\_name, shared, owner\_email, filter\_data                                                |
| `linear_triage_responsibilities` | Triage Responsibilities | responsibility\_id, team\_key, action, current\_user\_email                                                    |
| `linear_issues`                  | Issues                  | issue\_id, identifier, title, state\_type, assignee\_email, team\_key, project\_id, cycle\_id, due\_date       |
| `linear_issue_relations`         | Issue Relations         | relation\_id, relation\_type, issue\_identifier, related\_identifier                                           |
| `linear_comments`                | Comments                | comment\_id, issue\_id, project\_id, user\_email, external\_user\_email, body\_excerpt                         |
| `linear_attachments`             | Attachments             | attachment\_id, issue\_identifier, title, url, source\_type, creator\_email                                    |
| `linear_documents`               | Documents               | document\_id, title, project\_id, initiative\_id, team\_key, creator\_email                                    |

***

## What is deliberately **not** collected

ConfigView reads inventory, not credentials. These fields exist in Linear's API and are left out on purpose:

| Field                                                                                | Why                                                                                                                                                                                                   |
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Webhook.secret`                                                                     | The live signing secret. Anyone holding it can forge events into the workspace. ConfigView records **whether** one is set (`has_secret`) and strips the value before writing.                         |
| `Organization.samlSettings` / `scimSettings` / `authSettings` / `securitySettings`   | JSON blobs that can carry IdP certificates and SCIM bearer tokens. The `saml_enabled` / `scim_enabled` booleans carry the posture signal without copying the credential into your reporting database. |
| `Organization.aiProviderConfiguration`, `codingAgentSettings`, `linearAgentSettings` | Can hold third-party provider API keys.                                                                                                                                                               |
| `IdentityProvider.ssoSigningCert`                                                    | The workspace SSO certificate. Only the IdP's `id` and `type` are stored.                                                                                                                             |
| `User.calendarHash`                                                                  | A bearer token in a URL — anyone with it can read that user's calendar feed.                                                                                                                          |
| `Document.content`, `Template.templateData`                                          | Long free-text bodies that would dominate storage and that nothing queries. `title` and `summary` are collected.                                                                                      |

Attachment URLs **are** collected, and for uploaded files these can be signed object-store URLs. Treat `linear_attachments` with the same care as a storage-bucket listing.

***

## Verifying the setup

Run the health check from `/admin/cron/`. It probes, in order:

1. `LINEAR_API_KEY` retrievable from Secret Manager — and **not** stored with a `Bearer ` prefix
2. `viewer` — names the identity behind the key and whether it is an admin
3. `organization` — workspace name, user count, plan and SSO posture
4. `users` — the core read path
5. Teams, Projects, Issues, Integrations, Webhooks, OAuth applications, Organization invites, Audit log, Initiatives — each reported as **ok** or **skip**, never as a failure when the plan or key scope simply does not grant it
6. Rate-limit headroom — remaining requests **and** complexity points for the hour

A workspace on the Free plan with a member-level key should still show **overall: ok**, with several checks marked `skip`. That is the design: a skip means "not granted here", a fail means "broken".

***

## Troubleshooting

| Issue                                                             | Fix                                                                                                                                                                                                  |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Health check fails with HTTP 400 on every query                   | The key is stored with a `Bearer ` prefix. Personal API keys go in bare — re-save `LINEAR_API_KEY` with just the `lin_api_...` value.                                                                |
| Health check fails on "Linear API auth (viewer)"                  | The key is wrong, revoked, or belongs to a deactivated user. Create a new one under an admin account.                                                                                                |
| `linear_oauth_applications` / `linear_organization_invites` empty | The key is not from a Workspace Admin. These need admin scope.                                                                                                                                       |
| `linear_audit_entries` / `linear_ip_restrictions` always empty    | Both are Enterprise-only features. Empty is correct on other plans.                                                                                                                                  |
| `linear_organization` seat columns are NULL                       | The workspace is on the Free plan and has no `subscription` object. Expected.                                                                                                                        |
| A private team's issues are missing                               | The key's user is not a member of that private team. Linear scopes reads to the user; add them to the team or use a broader admin account.                                                           |
| Runs slow down or pause mid-collection                            | The workspace is near its hourly complexity budget. The collectors wait for the window to reset by design. Stagger the schedules of Issues, Comments and Attachments if it happens every run.        |
| `TRUNCATED` in the run log                                        | A collection cap was reached — the snapshot is partial by design. Raise `max_pages` in `tools/linear_specs.py` and regenerate if the full history is needed.                                         |
| A project's member list looks short                               | More than 100 members on that project; Linear's nested connection stops there. The run log names the affected project.                                                                               |
| Many columns NULL after the first run                             | NULL-audit before assuming a bug: Linear's schema declares fields that a given workspace may never populate (SLA timers, GitHub links, initiative fields on workspaces that do not use initiatives). |
