Mirror a Linear team's issues onto a Nave
dashboard. Pulls every issue for a team from the Linear GraphQL API — with each
issue's full state-transition history — translates it into the JSON shape of
a Jira Agile board export
(/rest/agile/1.0/board/<id>/issue?expand=changelog), and uploads the result to
a Nave dashboard.
Nave does the placement, not this tool. Because we hand Nave the raw
changelog with full transition timestamps, Nave reconstructs each card's current
column and time-in-column itself — with hour/minute precision — exactly as it
does for a live Jira integration. There is no stage-date computation here: the
script is a faithful Linear → Jira-export translator. (An earlier version emitted
a dd/MM/yyyy CSV and computed placement locally; that path is gone because the
CSV format is day-granular only, which mis-sorted cards that changed state more
than once on the same day.)
- Python 3.8+ — standard library only, no
pip installneeded. - A Linear API key and a Nave API token + dashboard id (see below).
cp .env.example .env
# edit .env with your keys, then:
set -a && source .env && set +a| Variable | Required | Where to get it |
|---|---|---|
LINEAR_API_KEY |
yes | Linear → Settings → Security & access → API. Used verbatim (no Bearer prefix). |
LINEAR_TEAM_KEY |
yes | The team key — the issue-id prefix, e.g. WOS for WOS-1234. |
NAVE_TOKEN |
yes | In Nave: dashboard → API → "Get Access Token". Used verbatim (no Bearer prefix). |
NAVE_DASHBOARD_ID |
yes* | The target Nave dashboard id. *Optional if you pass --dashboard-id. |
One command — pull from Linear, push to Nave, watch the job finish:
python3 sync.pyBy default this is a full replace: each run mirrors Linear onto the dashboard
(cumulative=false). Nave never deletes completed items.
python3 sync.py --wipe-out # wipe ALL dashboard data first
python3 sync.py --cumulative # merge into existing data instead of replacing
python3 sync.py --keep-json out.json # keep the generated snapshot at a path
python3 sync.py --dashboard-id ID # override $NAVE_DASHBOARD_ID
python3 sync.py --no-poll # fire the upload and exit without pollingsync.py is a thin orchestrator over two single-purpose scripts:
python3 backfill.py snapshot.json # Linear -> Nave-ready Jira-export JSON
python3 upload.py snapshot.json # push that JSON to Nave + poll the jobupload.py supports the same --wipe-out / --cumulative / --no-poll /
--dashboard-id flags and sends .json files as application/json.
backfill.py emits one Jira-style issue object per Linear issue:
key← Linear identifier,fields.summary← title,fields.created← creation time,fields.labels,fields.issuetype(Bug / Sub-task / Task), optionalfields.duedate.fields.status← the issue's current Linear state, tagged with a JirastatusCategoryderived from the state's type (triage/backlog/unstarted→ To Do,started→ In Progress,completed/canceled/duplicate→ Done).changelog.histories[]← every real state transition (no-op same-state touches dropped), each carrying its full timestamp and astatusitem withfromString/toString.
Nave reads the changelog and reconstructs the board from it. Column order is
arranged once in the Nave dashboard settings — a board-issue export carries no
column configuration, so Nave groups statuses by statusCategory and you order
them in the UI. Every status that appears in the data is available there to map.
- Generated JSON/CSV snapshots and any Linear exports are git-ignored; this repo holds only code.
- Large pulls page through the Linear API (issues and per-issue history) and back off on rate limits automatically.
- Run
python3 test_backfill.pyto exercise the Linear → Jira-export translation.