Skip to content

Repository files navigation

linear-archive

Export Linear issues to Markdown files, back them up locally, and clean up your Linear workspace by archiving or deleting what you have exported.

A zero-dependency command-line tool for Linear that turns issues into readable, greppable, git-friendly Markdown. Use it to back up completed work, declutter a crowded Done column, keep an offline record of tickets before deleting them, or migrate issue history out of Linear.

npm version license: MIT node >= 20

export LINEAR_API_KEY=lin_api_...
npx linear-archive --team ENG --state Done --dry-run      # preview
npx linear-archive --team ENG --state Done --after archive # export, then archive in Linear

Why

Linear gets slower and noisier as finished issues pile up, but deleting them means losing the discussion, decisions, and links that live in each ticket. linear-archive gives you a complete local copy first, then optionally archives or deletes the issue in Linear, so cleanup never costs you the record.

  • One Markdown file per issue — metadata table, full description, relations (blocks / blocked by / parent / children), attachment links, state history, and every comment in chronological order.
  • Pick exactly what to export — by workflow state (Done, completed, canceled, …), team, project, label, creator, assignee, or "completed more than N days ago".
  • Organise the output your way — flat, or grouped into folders by project, state, team, workspace, or year with a template such as {project}/{state|lower}.
  • Decide what happens in Linear afterwards — nothing (default), archive (reversible), or delete (moves to Linear's trash).
  • Multiple workspaces — a shared team workspace where you only want your own tickets, and a personal one where you want everything, each with its own API key and rules.
  • Safe by design — an issue is only archived or deleted after its file is written. --dry-run shows the full plan without touching anything. Already-trashed issues are never touched again.
  • Re-runnable — exporting the same issue twice overwrites its file and updates the index. INDEX.md and index.json accumulate across runs.
  • Zero runtime dependencies — Node 20+ and a Linear personal API key. Nothing else to install or audit.

Quick start

  1. Create a Linear API key. In Linear go to Settings → Security & access → Personal API keys → Create key. Copy the key (it starts with lin_api_).
  2. Make it available to the tool, either exported in your shell or in a .env file in the directory you run from:
    echo 'LINEAR_API_KEY=lin_api_...' > .env
  3. Preview, then run.
    npx linear-archive --dry-run                 # what would be exported, nothing written
    npx linear-archive                           # export every completed issue to ./linear-archive/
    npx linear-archive --after archive           # same, then archive those issues in Linear

Install

npm install -g linear-archive
# or run without installing
npx linear-archive --help

Setup

Create a personal API key in Linear under Settings → Security & access → Personal API keys, then either export it or put it in a .env file in the directory you run the command from:

export LINEAR_API_KEY=lin_api_...

The key needs write access only if you use --after archive or --after delete.

Usage

See what would be exported without changing anything:

linear-archive --dry-run

Export every completed issue across all teams into ./linear-archive/:

linear-archive

Export the Done column of one team, then archive those issues in Linear:

linear-archive --team ENG --state Done --after archive

Export issues finished more than 90 days ago and delete them from Linear (prompts for confirmation; add --yes to skip):

linear-archive --state completed --completed-before 90d --after delete

Export by label instead of state, with attachments downloaded locally:

linear-archive --state completed --label archive-me --download-attachments

On a shared team, export only the issues you created:

linear-archive --team ENG --state Done --creator me --after archive

Not sure what your states are called?

linear-archive --list-states --team ENG

Options

Flag Description Default
--team KEY Team key, repeatable all teams
--state NAME|TYPE Workflow state name (Done) or type (completed, canceled, started, unstarted, backlog, triage), repeatable completed
--label NAME Only issues carrying this label, repeatable
--project NAME Only issues in this project, repeatable
--creator WHO Only issues created by WHO: me, an email, a user ID, or a display name. Repeatable anyone
--assignee WHO Only issues assigned to WHO, same forms. Repeatable anyone
--completed-before X ISO date, or duration (30d, 2w, 6m, 1y) meaning "completed at least this long ago"
--include-archived Also export issues Linear has already archived off
--include-trashed Also export issues in Linear's trash (implies --include-archived). Never deleted or archived again off
--limit N Stop after N issues; handy for a first test
--workspace NAME Use a named profile from the config file's workspaces map
--all-workspaces Run every profile in the config file, one after another off
--out DIR Output directory ./linear-archive
--layout TEMPLATE Sub-folders under --out, e.g. {project}/{state|lower}. See Folder layout flat
--download-attachments Download files uploaded to Linear and rewrite links to local copies off
--json Also write the raw API payload next to each .md off
--after ACTION none, archive, or delete none
--yes Skip the confirmation prompt for --after delete off
--dry-run Print the plan; write nothing, change nothing off
--config FILE Config file path ./linear-archive.config.json
--verbose Log every API request off

State types match every team's workflow regardless of what the state is called, so --state completed is the portable choice. State names are matched exactly (case-insensitive) and are useful when a team has more than one completed-type state, such as Done and Released.

Config file

Anything you would pass as a flag can live in linear-archive.config.json in the working directory (or wherever --config points). Flags override the file; the file overrides the defaults. The API key is never read from the config file, only from the environment or .env.

{
  "outDir": "./linear-archive",
  "layout": "{project}/{state|lower}",
  "teams": ["ENG"],
  "states": ["completed"],
  "labels": [],
  "projects": [],
  "includeArchived": false,
  "completedBefore": "30d",
  "after": "archive",
  "downloadAttachments": false,
  "json": false
}

A copy is in linear-archive.config.example.json.

Folder layout

By default every file lands directly in the output directory. --layout (or layout in the config) adds sub-folders from a template:

linear-archive --layout "{project}/{state|lower}"
linear-archive/
├── INDEX.md
├── index.json
├── LinkForty Cloud/
│   └── done/
│       ├── ENG-42-fix-the-thing.md
│       └── ENG-41-prior-work.md
└── Platform/
    ├── done/
    └── canceled/
Placeholder Value
{project} Project name, or _no-project
{state} Workflow state name, e.g. Done
{stateType} State type, e.g. completed
{team} Team key, e.g. ENG
{teamName} Team name, e.g. Engineering
{workspace} Workspace profile name, or _default
{year} Year completed (or created, if never completed)

Add |lower, |upper, or |slug to change the case or make a URL-style slug: {state|lower}done, {teamName|slug}engineering. Names keep their spaces and capitals so folders read like Linear does; only characters that are illegal in paths are replaced. The index files always stay at the top level and link into the sub-folders.

Filtering by person

--creator and --assignee (or creators / assignees in the config) accept any mix of:

  • me — resolved to whoever owns the API key, so the same config works for each teammate.
  • an email address, e.g. sam@example.com
  • a Linear user ID (UUID)
  • a display name, matched case-insensitively, e.g. "Sam Jones"

Several values are OR-ed together. On a shared team where everyone files tickets, --creator me keeps the export to your own work.

Multiple workspaces

A Linear personal API key belongs to exactly one workspace. To handle more than one, define profiles under workspaces in the config file. Each profile inherits the top-level settings, overrides whatever it sets, and gets its own key:

{
  "states": ["completed"],
  "outDir": "./linear-archive",
  "workspaces": {
    "work": {
      "teams": ["ENG"],
      "creators": ["me"],
      "after": "archive"
    },
    "side-project": {
      "apiKeyEnv": "LINEAR_API_KEY_SIDE",
      "outDir": "./linear-archive/side-project",
      "after": "delete"
    }
  }
}

The key for a profile is looked up in this order: the env var named in its apiKeyEnv, then LINEAR_API_KEY_<NAME> (name uppercased, non-alphanumerics replaced with _, so side-projectLINEAR_API_KEY_SIDE_PROJECT), then plain LINEAR_API_KEY.

linear-archive --workspace work            # just the shared team, only my tickets, archived after
linear-archive --workspace side-project    # the workspace I own, everything, deleted after
linear-archive --all-workspaces --dry-run  # both, in order, showing the plan

CLI flags still override the profile, so --workspace work --after none exports without archiving. Running with no --workspace flag ignores the profiles and uses the top-level settings with LINEAR_API_KEY.

What the output looks like

linear-archive/
├── INDEX.md                      # table of every exported issue, newest first
├── index.json                    # the same data, machine-readable
├── ENG-42-fix-the-thing.md
├── ENG-41-prior-work.md
└── assets/                       # only with --download-attachments
    └── ENG-42/
        └── screenshot.png

Each issue file:

# ENG-42: Fix the thing

| Field | Value |
|---|---|
| Status | Done (completed) |
| Priority | High |
| Team | Engineering (ENG) |
| Project | Platform |
| Labels | bug, backend |
| Assignee | Sam <sam@example.com> |
| Completed | 2026-01-05 |
| Linear URL | https://linear.app/acme/issue/ENG-42/fix-the-thing |
| ... | ... |

## Relations
- **Blocks:** ENG-43 — Next
- **Blocked by:** ENG-41 — Prior

## Attachments
- [PR #12](https://github.com/acme/repo/pull/12)

## State history
| When | From | To | By |
|---|---|---|---|
| 2026-01-03 09:00 UTC | Todo | In Progress | Sam |
| 2026-01-05 10:00 UTC | In Progress | Done ||

## Description
...the issue body, verbatim...

## Comments
### 2026-01-04 11:00 UTC — Alex
...

Archive vs delete

--after archive --after delete
Reversible Yes, indefinitely, from Linear's archive From Linear's trash, for a limited time
Still searchable in Linear Yes (with "include archived") No
Frees up the active issue list Yes Yes
Requires confirmation No Yes, unless --yes

If you only want to declutter, archive. Delete is for when you have decided the local copy is the record.

Safety notes

  • The Linear mutation runs only after the Markdown file (and JSON, if requested) has been written to disk. If writing fails, the issue is left untouched in Linear and the run exits with code 2.
  • --dry-run performs the same reads as a real run but writes nothing and calls no mutations.
  • The tool never permanently deletes anything. --after delete uses Linear's standard trash.
  • Issues already in the trash are skipped by default, so re-running after a delete is a no-op for them. --include-trashed exports them anyway, which is the way to grab a copy before Linear purges the trash.
  • Attachment downloads are best-effort. A failed download is logged and the original Linear URL is kept in the file.

What is not exported

  • Reactions, subscribers, and inline "anchored" description comment positions (the comment text itself is exported with its quoted snippet).
  • Files uploaded to Linear, unless --download-attachments is set. Without it, the uploads.linear.app links remain and require a Linear login to open.
  • Project or initiative documents. This tool is scoped to issues.

FAQ

Does this delete anything permanently? No. --after delete uses Linear's own trash, which Linear keeps for a while and lets you restore from. --after archive is fully reversible. The default is to change nothing.

Can I run it more than once? Yes. Every run is idempotent: existing files are overwritten with fresh content, the index is updated in place, and issues already in Linear's trash are skipped.

What is the difference between a state name and a state type? Every team in Linear has its own workflow with its own state names (Done, Released, Shipped). Each state also has a fixed type (completed, canceled, started, …). --state completed matches the completed state of every team; --state Done matches only states literally named Done.

How do I export only my own issues on a shared team? --creator me. It resolves to whoever owns the API key, so a committed config works for every teammate.

Can I use it in CI or a cron job? Yes. Set LINEAR_API_KEY in the environment and pass --yes if you use --after delete, since there is no terminal to confirm on. The exit code is 0 on success, 1 on a configuration or authentication error, and 2 when some issues failed to export (those are left untouched in Linear).

Where do I find my team key? It is the prefix on your issue identifiers: for ENG-123 the team key is ENG. linear-archive --list-states prints every team and its states.

Development

npm install
npm test          # vitest
npm run typecheck
npm run build     # emits dist/
npm run dev -- --dry-run --limit 3

License

MIT

About

Export Linear issues to Markdown and back them up locally, then archive or delete them in Linear. Zero-dependency CLI with filters by state, team, project, label, creator, and multi-workspace support.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages