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) and self-hosted Plane instances (v0.23+)
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 |
| 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 --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 |
Installation
Requires Python 3.11+
Recommended — pipx (for CLI use)
pipx installs CLI tools in isolated environments and makes them available globally. This is the recommended approach on any system.
# Install pipx if you don't have it
# 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 \
--src-url https://api.plane.so \
--src-token <source-api-token> \
--src-workspace <source-workspace-slug> \
--src-project <source-project-id-or-key> \
--dst-url https://api.plane.so \
--dst-token <destination-api-token> \
--dst-workspace <destination-workspace-slug> \
--dst-project <destination-project-id-or-key> \
--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
modules: [Sprint 1, Sprint 2, Backlog]
cycles: [Q1 2025, Q2 2025]
Fill in any blank destination emails under members.unmatched.
Step 3 — Real run
plane-migrate \
--src-url https://api.plane.so \
--src-token <source-api-token> \
--src-workspace <source-workspace-slug> \
--src-project <source-project-id-or-key> \
--dst-url https://api.plane.so \
--dst-token <destination-api-token> \
--dst-workspace <destination-workspace-slug> \
--dst-project <destination-project-id-or-key> \
--members-map migration-plan-<timestamp>.yaml
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_SESSION_COOKIE |
--session-cookie |
PLANE_MIGRATE_LOG |
--log-file |
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://self-hosted.example.com
export PLANE_DST_TOKEN=plane_api_...
export PLANE_DST_WORKSPACE=my-workspace
plane-migrate \
--src-project <src-project-id> \
--dst-project <dst-project-id>
All CLI Flags
plane-migrate [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
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
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)
--session-cookie TEXT Browser session cookie for page migration
--log-file TEXT Log file path
Selective Migration — --only
Migrate only specific entity types. Can be repeated.
# Re-run only links and comments (fill gaps)
plane-migrate ... --only links --only comments
# Migrate only work items and relations
plane-migrate ... --only work-items --only relations
# Migrate pages (requires session cookie)
plane-migrate ... --only pages --session-cookie "..."
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 --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 ... --state-file my-migration.json
State file structure
{
"src_project": "...",
"dst_project": "...",
"states": { "<src-state-id>": "<dst-state-id>", ... },
"labels": { "<src-label-id>": "<dst-label-id>", ... },
"work_items": { "<src-item-id>": "<dst-item-id>", ... },
"modules": [{ "src": "...", "dst": "...", "workitems": [...] }],
"cycles": [{ "src": "...", "dst": "...", "workitems": [...] }],
"links_done": { "<src-item-id>": ["<src-link-id>", ...] },
"comments_done": { "<src-item-id>": ["<src-comment-id>", ...] },
"relations": ["<src-item-id>:relation_type:<src-related-id>", ...]
}
Keep the state file between runs. Deleting it causes the migrator to treat everything as new — resulting in duplicates in the destination.
Members Mapping
Members are matched by email between source and destination. When the source and destination share the same Plane instance (workspace-to-workspace migration), 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 ... --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. The session cookie is used only to fetch page content — no writes happen with it.
Getting the session cookie
- Open your Plane instance in a browser and log in
- Open DevTools → Application → Cookies
- Copy the values of
csrftokenandsession-id - Pass them as a single string:
plane-migrate ... \
--session-cookie "csrftoken=abc123; session-id=xyz789"
If --session-cookie is not provided, pages are skipped and noted 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:
- 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 fixing rate limit issues.
Cross-Instance Migration
Migrating from one Plane deployment to another (e.g. Plane Cloud → self-hosted):
plane-migrate \
--src-url https://api.plane.so \
--src-token plane_api_<cloud-token> \
--src-workspace my-cloud-workspace \
--src-project <project-id> \
--dst-url https://plane.mycompany.com \
--dst-token plane_api_<selfhosted-token> \
--dst-workspace my-internal-workspace \
--dst-project <project-id> \
--members-map members.yaml
Same-Instance Workspace Migration
Migrating between two workspaces on the same Plane instance (same token works for both):
plane-migrate \
--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 | ✗ | ✗ | ✓ |
| Re-importable | ✓ | ✗ | ✓ |
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)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file plane_migrate-0.1.0.tar.gz.
File metadata
- Download URL: plane_migrate-0.1.0.tar.gz
- Upload date:
- Size: 29.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.11.5 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0dc7d5507b62632ab789616f2208f4103b2bec00d06cad5e6648828aac21e88b
|
|
| MD5 |
842dbbdb6050eafbd634e998cbd7fac1
|
|
| BLAKE2b-256 |
1f328ed867ae32a6b95992a970f063089152ae9f7acd039ba7bd2f2b892693e4
|
File details
Details for the file plane_migrate-0.1.0-py3-none-any.whl.
File metadata
- Download URL: plane_migrate-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.11.5 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b547cf57164764558958e19a34ae6de4032a736d68ae35d6b58cc72a17bb8770
|
|
| MD5 |
966cc88f5d1974c2859f657a3dc4c658
|
|
| BLAKE2b-256 |
02b1a7472af1f123be439b95cf28334ce039c5943122b5cb7270daab86a3bf65
|