# notion2plank One-shot Python script that imports tasks from a **Notion CSV export** into a **self-hosted Plane** instance via its REST API. Plane's free/community tier has no built-in import UI, but the API is fully open. This script bridges the gap. --- ## Requirements - Python 3.10+ - A Plane API key — _Settings → API Tokens_ - A Notion database exported as CSV — _··· → Export → Markdown & CSV → without subpages_ ``` pip -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` --- ## Setup ```bash cp config.yaml.example config.yaml ``` Edit `config.yaml`: ```yaml plane_url: https://your-plane-instance.com workspace_slug: your-workspace project_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx api_key: your-api-key-here # Optional — needed to import estimate points (вес column). # Get it from browser DevTools → Application → Cookies → session-id. # If omitted, the вес column is silently skipped. plane_session: your-session-id-cookie-value # Map Notion status values (exact text + emoji) → Plane state names. # Find your state names: GET /api/v1/workspaces//projects//states/ status_mapping: "в работе 🔨": "In Progress" "Готово ✅": "Done" "На проверке 👀": "Review" "Утверждённые задачи 📝": "Todo" "Общие идеи (задачи) 📋": "Backlog" ``` --- ## Usage **Dry run first** — parses every row and shows what would be created, no API calls: ```bash python import.py --dry-run ``` **Real import:** ```bash python import.py ``` **Custom paths:** ```bash python import.py --csv path/to/export.csv --config path/to/config.yaml ``` ### Output ``` Fetching project states… Found 6 states: ['Backlog', 'Todo', 'In Progress', 'Done', 'Cancelled', 'Review'] Fetching project labels… Found 7 labels: ['Sound', '2d', 'Game Design', 'Org', '3d', 'Level Design', 'Devel'] Fetching estimate points… Found 6 estimate points: ['1', '2', '3', '5', '8', '13'] Ensuring labels for disciplines: ['Game Design', 'Sound', ...] Processing 37 rows… [ 1] Created: #31 — 'Create Kanban Board' [ 2] Created: #32 — 'Add Milestones' ... Summary: 37 created, 0 failed. ``` --- ## Field Mapping | Notion column | Plane field | Notes | | ---------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- | | `Name` | `name` | Required | | `Status` | `state` (UUID) | Mapped via `status_mapping` in config | | `priority` | `priority` | Emoji stripped: 🔥 Critical→`urgent`, ⭐ High→`high`, 📌 Medium→`medium`, 💤 Low→`low`, empty→`none` | | `discipline` | `label_ids` | Auto-created as a Plane label if it doesn't exist yet | | `Date` (single) | `target_date` | Parsed with dateutil; output `YYYY-MM-DD` | | `Date` (range `A → B`) | `start_date` + `target_date` | Split on the `→` Unicode arrow | | `Description` | `description_html` | Wrapped in `

` | | `результат` | appended to `description_html` | Added as `

Result: N

` if non-zero | | `вес` | `estimate_point` (UUID) | Matched by value (e.g. `"5"`) then by key position; requires `plane_session` | | `исполнитель` | — | Skipped (no user ID mapping) | | `Автор задачи` | — | Skipped | | `Attachments` | — | Skipped | | `Person` | — | Skipped | --- ## Notes ### Estimate points (`вес`) Plane's public API (`/api/v1/`) does not expose an endpoint to list estimate point UUIDs. The script works around this by calling an internal frontend endpoint (`/api/workspaces/…/estimates/`) using your browser session cookie. Resolution order: 1. **Exact value match** — `вес="5"` maps to the point labeled "5" (Fibonacci-style: 1, 2, 3, 5, 8, 13) 2. **Ordinal key fallback** — `вес="4"` maps to the 4th point in the estimate scale If `plane_session` is not set or the cookie has expired, the `вес` column is skipped and everything else still imports normally. ### Custom fields (work item properties) Plane's custom property API (`/work-item-types/…/work-item-properties/`) requires the `is_issue_type_enabled` project flag, which is a paid-tier feature. It is not available in the community/self-hosted free edition. ### Discipline labels All unique `discipline` values in the CSV are pre-fetched and compared against existing project labels before the import starts. Missing labels are created automatically via `POST /labels/`. Existing labels are reused by UUID. --- ## Files ``` import.py — main script config.yaml.example — annotated config template config.yaml — your local config (do not commit) requirements.txt — pip dependencies ```