Skip to main content

Migrate projects between Plane instances or workspaces

Project description

plane-migrate

A CLI tool to migrate projects between Plane instances or between workspaces on the same instance — with full fidelity, resumable runs, and delta re-runs.

Works with: Plane Cloud (api.plane.so), Plane Commercial (self-hosted), and Plane OSS (open-source) — in any combination.


What gets migrated

Entity Details
Work Item Types Custom types defined in the source project
States All workflow states; matched by name to avoid duplicates
Labels All labels; matched by name
Members Matched by email across instances; unmatched members flagged in plan
Estimates Estimate schema + all points (Points and Categories types)
Work Items Title, description, state, priority, assignee, labels, type, dates, estimate point, parent — roots first, then children
Relations blocked_by, blocking, duplicate, relates_to
Links External URLs attached to work items
Comments Full comment body with author attribution byline
Modules With work item membership
Cycles With work item membership
Intake Intake work items
Pages Requires --src-session-cookie (see Pages)

Known limitations

Gap Reason
Activity history Plane API has no write endpoint for activity entries
File attachments Plane API has no attachment upload endpoint
Page listing via API Pages require a browser session cookie to list and fetch content

Cloud / Commercial vs OSS

Plane OSS exposes some entities at a different API path (/api/ instead of /api/v1/). The migrator auto-detects which type each side is and uses the right path automatically.

Feature Cloud / Commercial OSS
Work items, states, labels, modules, cycles /api/v1/ SDK /api/v1/ SDK
Relations /api/v1/ SDK /api/ app API ¹
Estimates /api/v1/ SDK /api/ app API ¹
Pages (read) /api/ + src session cookie /api/ + src session cookie
Pages (write) /api/v1/ SDK /api/ + dst session cookie ¹

¹ Requires --src-session-cookie (for OSS source) or --dst-session-cookie (for OSS destination).

Session cookies — when are they needed?

Scenario --src-session-cookie --dst-session-cookie
Cloud → Cloud pages only
Cloud → OSS pages only relations + estimates + pages
OSS → Cloud relations + pages
OSS → OSS relations + pages relations + estimates + pages

If a required cookie is missing, the affected entity type is skipped with a warning.


Installation

Requires Python 3.11+

Recommended — pipx (for CLI use)

pipx installs CLI tools in isolated environments and makes them available globally.

# macOS
brew install pipx && pipx ensurepath

# Debian / Ubuntu
sudo apt install pipx && pipx ensurepath

# Install plane-migrate
pipx install git+https://github.com/mguptahub/plane-migrate.git

pip (inside a virtualenv)

python -m venv venv && source venv/bin/activate
pip install git+https://github.com/mguptahub/plane-migrate.git

Local development

git clone https://github.com/mguptahub/plane-migrate.git
cd plane-migrate
python -m venv venv && source venv/bin/activate
pip install -e ".[dev]"

Verify:

plane-migrate --help

Quick Start

Step 1 — Dry run

Always start with a dry run. It fetches everything from source, previews what will be created, and writes a plan YAML — without touching the destination.

plane-migrate project \
  --src-url        https://api.plane.so \
  --src-token      <source-api-token> \
  --src-workspace  <source-workspace-slug> \
  --src-project    <source-project-id> \
  --dst-url        https://your-oss-instance.example.com \
  --dst-token      <destination-api-token> \
  --dst-workspace  <destination-workspace-slug> \
  --dst-project    <destination-project-id> \
  --dry-run

This writes a migration-plan-<timestamp>.yaml in the current directory.

Step 2 — Review the plan

members:
  mapped:
    - alice@old.com: alice@new.com    # auto-matched by email
  unmatched:
    bob@old.com: ""                   # ← fill in destination email
  _hint: Fill in destination emails for unmatched members, then pass this file via --members-map.

states:
  create: [Todo, In Progress, Done]
  skip:   [Backlog]                   # already exist on destination

work_items:
  total:    142
  roots:    98
  children: 44

Fill in any blank destination emails under members.unmatched.

Step 3 — Real run

plane-migrate project \
  --src-url        https://api.plane.so \
  --src-token      <source-api-token> \
  --src-workspace  <source-workspace-slug> \
  --src-project    <source-project-id> \
  --dst-url        https://your-oss-instance.example.com \
  --dst-token      <destination-api-token> \
  --dst-workspace  <destination-workspace-slug> \
  --dst-project    <destination-project-id> \
  --members-map    migration-plan-<timestamp>.yaml \
  --src-session-cookie "csrftoken=abc; sessionid=xyz" \
  --dst-session-cookie "csrftoken=def; sessionid=uvw"

Environment Variables

All flags can be set via environment variables — useful for CI or repeated runs:

Variable Flag equivalent
PLANE_SRC_URL --src-url
PLANE_SRC_TOKEN --src-token
PLANE_SRC_WORKSPACE --src-workspace
PLANE_DST_URL --dst-url
PLANE_DST_TOKEN --dst-token
PLANE_DST_WORKSPACE --dst-workspace
PLANE_SRC_SESSION_COOKIE --src-session-cookie
PLANE_DST_SESSION_COOKIE --dst-session-cookie
PLANE_MIGRATE_LOG --log-file

PLANE_SESSION_COOKIE is still accepted as a fallback for --src-session-cookie for backward compatibility.

export PLANE_SRC_URL=https://api.plane.so
export PLANE_SRC_TOKEN=plane_api_...
export PLANE_SRC_WORKSPACE=my-workspace
export PLANE_DST_URL=https://oss.example.com
export PLANE_DST_TOKEN=plane_api_...
export PLANE_DST_WORKSPACE=my-workspace
export PLANE_SRC_SESSION_COOKIE="csrftoken=abc; sessionid=xyz"
export PLANE_DST_SESSION_COOKIE="csrftoken=def; sessionid=uvw"

plane-migrate project \
  --src-project <src-project-id> \
  --dst-project <dst-project-id>

All CLI Flags

plane-migrate project [OPTIONS]

Source:
  --src-url TEXT               Source Plane instance URL
  --src-token TEXT             Source API token
  --src-workspace TEXT         Source workspace slug
  --src-project TEXT           Source project UUID or key
  --src-session-cookie TEXT    Browser session cookie for source instance
                               (OSS relations + pages)  [env: PLANE_SRC_SESSION_COOKIE]

Destination:
  --dst-url TEXT               Destination Plane instance URL
  --dst-token TEXT             Destination API token
  --dst-workspace TEXT         Destination workspace slug
  --dst-project TEXT           Destination project UUID or key
  --dst-session-cookie TEXT    Browser session cookie for destination instance
                               (OSS relations + estimates + pages)  [env: PLANE_DST_SESSION_COOKIE]

Options:
  --dry-run / -n               Preview what would be migrated — writes a plan YAML
  --only TEXT                  Migrate only specific entities (repeatable)
  --members-map TEXT           YAML file mapping src emails → dst emails
  --plan-file TEXT             Where to write the dry-run plan (default: migration-plan-<ts>.yaml)
  --state-file TEXT            State file path for resumable runs (default: <src>-<dst>.json)
  --log-file TEXT              Log file path  [env: PLANE_MIGRATE_LOG]

Selective Migration — --only

Migrate only specific entity types. Can be repeated.

# Re-run only links and comments (fill gaps)
plane-migrate project ... --only links --only comments

# Migrate only work items and relations
plane-migrate project ... --only work-items --only relations

# Migrate pages from Cloud source to OSS destination
plane-migrate project ... --only pages \
  --src-session-cookie "csrftoken=abc; sessionid=xyz" \
  --dst-session-cookie "csrftoken=def; sessionid=uvw"

# Migrate relations to an OSS destination
plane-migrate project ... --only relations \
  --dst-session-cookie "csrftoken=def; sessionid=uvw"

Valid --only values:

Value What it migrates
types Work item types
states Workflow states
labels Labels
members Member matching (no destination writes)
estimates Estimate schema + points
work-items All work items (roots + children)
relations Work item relations
links External URL links on work items
comments Work item comments
modules Modules + work item membership
cycles Cycles + work item membership
intake Intake work items
pages Pages (requires --src-session-cookie)

Resumable Runs & Gap Filling

Every successful write is tracked in a state file (<src-project-id>-<dst-project-id>.json). If a run is interrupted — network error, rate limit, crash — just rerun the same command. The migrator will:

  • Skip entities already created (tracked by source ID → destination ID mapping)
  • Fill gaps for links and comments — only creates the ones missing per work item
  • Save state incrementally — progress is never lost mid-run
# Interrupted run — just rerun, it picks up where it left off
plane-migrate project ... --state-file my-migration.json

Keep the state file between runs. Deleting it causes the migrator to treat everything as new — resulting in duplicates on the destination.


Members Mapping

Members are matched by email between source and destination. When migrating within the same instance, members are automatically matched.

When migrating across instances, emails may differ. Unmatched members appear in the dry-run plan:

members:
  unmatched:
    old-email@company.com: ""    # ← fill in new email

Pass the edited plan file (or any YAML with src_email: dst_email pairs) via --members-map:

plane-migrate project ... --members-map migration-plan.yaml

Work items assigned to unmatched members will still be migrated — but without an assignee.


Pages Migration

Pages require a browser session cookie because the public API does not expose a page listing endpoint.

  • --src-session-cookie — used to list and fetch page content from the source
  • --dst-session-cookie — used to create pages on an OSS destination (not needed for Cloud/Commercial destinations)

Getting the session cookie

  1. Open your Plane instance in a browser and log in
  2. Open DevTools → Application → Cookies
  3. Copy the values of csrftoken and sessionid
  4. Pass them as a single string:
# Cloud source → Cloud destination (only src cookie needed)
plane-migrate project ... --only pages \
  --src-session-cookie "csrftoken=abc123; sessionid=xyz789"

# Cloud source → OSS destination (both cookies needed)
plane-migrate project ... --only pages \
  --src-session-cookie "csrftoken=abc123; sessionid=xyz789" \
  --dst-session-cookie "csrftoken=def456; sessionid=uvw012"

If --src-session-cookie is not provided, pages are skipped with a note in the summary.


Logging

Every run writes a structured JSON log to plane-migrate-<timestamp>.log (or the path from --log-file / PLANE_MIGRATE_LOG).

Each line is a JSON event:

{"ts": "2025-04-01T10:23:45Z", "kind": "created", "entity": "work_item", "name": "Fix login bug", "src_id": "abc", "dst_id": "xyz"}
{"ts": "2025-04-01T10:23:46Z", "kind": "skipped", "entity": "label",     "name": "Bug", "detail": "already exists"}
{"ts": "2025-04-01T10:23:47Z", "kind": "failed",  "entity": "comment",   "name": "on abc", "detail": "429 Too Many Requests"}

Tail in real time:

tail -f plane-migrate-*.log | python3 -c "
import sys, json
for line in sys.stdin:
    e = json.loads(line)
    print(f\"{e['ts'][11:19]} [{e['kind']:8}] {e['entity']:12} {e['name']}\")
"

Rate Limiting

The migrator writes at ~3 requests/second (300ms delay between writes). On HTTP 429 Too Many Requests, it backs off automatically and shows a live countdown in the terminal:

⏳ Rate limited — retrying in 28s (attempt 1/3)...
  • Wait 30s → retry
  • Wait 45s → retry
  • Wait 60s → retry
  • Fail and log the error

Failures are logged and counted but do not abort the run. Use --only to re-run specific phases after hitting rate limits.


Cross-Instance Examples

Cloud → OSS

plane-migrate project \
  --src-url        https://api.plane.so \
  --src-token      plane_api_<cloud-token> \
  --src-workspace  my-cloud-workspace \
  --src-project    <project-id> \
  --dst-url        https://oss.mycompany.com \
  --dst-token      plane_api_<oss-token> \
  --dst-workspace  my-oss-workspace \
  --dst-project    <project-id> \
  --members-map    members.yaml \
  --src-session-cookie "csrftoken=abc; sessionid=xyz" \
  --dst-session-cookie "csrftoken=def; sessionid=uvw"

Cloud → Cloud (or Commercial → Commercial)

plane-migrate project \
  --src-url        https://api.plane.so \
  --src-token      plane_api_<src-token> \
  --src-workspace  source-workspace \
  --src-project    <project-id> \
  --dst-url        https://api.plane.so \
  --dst-token      plane_api_<dst-token> \
  --dst-workspace  destination-workspace \
  --dst-project    <project-id> \
  --members-map    members.yaml \
  --src-session-cookie "csrftoken=abc; sessionid=xyz"

Same-Instance Workspace Migration

plane-migrate project \
  --src-url        https://api.plane.so \
  --src-token      plane_api_<token> \
  --src-workspace  old-workspace \
  --src-project    <src-project-id> \
  --dst-url        https://api.plane.so \
  --dst-token      plane_api_<token> \
  --dst-workspace  new-workspace \
  --dst-project    <dst-project-id>

Members are auto-matched by email — no --members-map needed.


Comparison with Plane's Native Export

Capability CSV Export/Import JSON Export plane-migrate
Work items ✓ (flat)
States / Labels names only names only ✓ full remap
Comments ✓ with attribution
Relations
Links
Modules / Cycles names only ✓ with membership
Estimates
Pages ✓ (session cookie)
Intake
Member remapping
Resumable
OSS compatible

Contributing

Pull requests are welcome. When contributing:

  • Keep the migration sequence order (types → states → labels → members → estimates → work-items → relations → links → comments → modules → cycles → intake → pages)
  • All new entity types should follow the same pattern: fetch from source → remap IDs → create on destination → track in state file
  • State must be saved incrementally (per entity, not at the end of the run)
  • For OSS compatibility: if an entity's /api/v1/ endpoint returns 404, check whether /api/ has the equivalent and add a fallback (see oss_relations.py and _oss_app_post() as reference)

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plane_migrate-0.2.0.tar.gz (80.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plane_migrate-0.2.0-py3-none-any.whl (34.1 kB view details)

Uploaded Python 3

File details

Details for the file plane_migrate-0.2.0.tar.gz.

File metadata

  • Download URL: plane_migrate-0.2.0.tar.gz
  • Upload date:
  • Size: 80.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.16.5 cpython/3.14.2 HTTPX/0.28.1

File hashes

Hashes for plane_migrate-0.2.0.tar.gz
Algorithm Hash digest
SHA256 cadeb33d3cf1baf16237d70c2a3c5fb122d990e95cbe23eb159b6b9b9a4ab916
MD5 a92ca2b0dbae2a3a6941f1a831b2fd02
BLAKE2b-256 5af31d3a2beb34301653da27dd9f99a1943336f81694140ddfb3592ab15cb495

See more details on using hashes here.

File details

Details for the file plane_migrate-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: plane_migrate-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 34.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Hatch/1.16.5 cpython/3.14.2 HTTPX/0.28.1

File hashes

Hashes for plane_migrate-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5c41a0ca57a47830f8399034bdc9dbfdc680ae32943c77ae8188d6e58543894a
MD5 59ffbf7f0e6645c08d96fc37cde6a269
BLAKE2b-256 f55940ed40d0e8bcbd29cc672d3445cf645db8f1da05f5f129ed9064547dda81

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page