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.
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 LinearLinear 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-runshows 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.mdandindex.jsonaccumulate across runs. - Zero runtime dependencies — Node 20+ and a Linear personal API key. Nothing else to install or audit.
- 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_). - Make it available to the tool, either exported in your shell or in a
.envfile in the directory you run from:echo 'LINEAR_API_KEY=lin_api_...' > .env
- 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
npm install -g linear-archive
# or run without installing
npx linear-archive --helpCreate 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.
See what would be exported without changing anything:
linear-archive --dry-runExport every completed issue across all teams into ./linear-archive/:
linear-archiveExport the Done column of one team, then archive those issues in Linear:
linear-archive --team ENG --state Done --after archiveExport 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 deleteExport by label instead of state, with attachments downloaded locally:
linear-archive --state completed --label archive-me --download-attachmentsOn a shared team, export only the issues you created:
linear-archive --team ENG --state Done --creator me --after archiveNot sure what your states are called?
linear-archive --list-states --team ENG| 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.
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.
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.
--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.
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-project → LINEAR_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 planCLI 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.
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
...--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.
- 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-runperforms the same reads as a real run but writes nothing and calls no mutations.- The tool never permanently deletes anything.
--after deleteuses 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-trashedexports 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.
- Reactions, subscribers, and inline "anchored" description comment positions (the comment text itself is exported with its quoted snippet).
- Files uploaded to Linear, unless
--download-attachmentsis set. Without it, theuploads.linear.applinks remain and require a Linear login to open. - Project or initiative documents. This tool is scoped to issues.
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.
npm install
npm test # vitest
npm run typecheck
npm run build # emits dist/
npm run dev -- --dry-run --limit 3MIT