How to duplicate a project

Last updated: July 14, 2026

A step-by-step guide to cloning an existing Plane project (including all work items, relations and dependencies) into a brand new project.

Duplicating a project is not a built-in feature of the Plane product. This guide uses Plane Compose, Plane's official command-line companion tool, to achieve it. If you'd like to learn more about what Plane Compose can do, see the Plane Compose documentation.

What you'll accomplish

You'll take an existing Plane project and create a complete copy of it under a new project key, preserving all work items, parent/child hierarchies, module assignments, and inter-item relations.

This requires two separate push operations. The first creates all the work items in your new project. The second links the relations; this two-step approach is necessary because relation targets need to exist before they can be linked.


Part 1: One-time setup

If you've already used Plane Compose, skip ahead to Part 2. Otherwise, work through this section once.

1.1 Open a terminal

All the commands in this guide are typed into a terminal.

  • macOS: Press Cmd + Space, type Terminal, and press Enter.

  • Windows: Open the Start menu, type PowerShell, and press Enter.

1.2 Install the tools

Follow the block for your operating system, running the commands one at a time, in order.

macOS

First install Homebrew (a tool that installs other tools). This command will ask for your Mac login password. The screen shows nothing while you type it; that's normal, just type it and press Enter:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

When it finishes, it may print a couple of commands under "Next steps". Copy, paste, and run those too. Then:

brew install pipx
pipx ensurepath

Windows

First install Python: go to python.org/downloads, download the installer, and run it. Important: on the first screen of the installer, tick the checkbox that says "Add Python to PATH" before clicking Install.

Then, in PowerShell:

pip install --user pipx
pipx ensurepath

Windows note: anywhere this guide says python3, type python instead; that's the command name on Windows.

Linux

pip install --user pipx
pipx ensurepath

(If pip isn't found, install it first, e.g. sudo apt install python3-pip on Ubuntu.)

1.3 Install Plane Compose

Close your terminal window and open a new one. This makes the changes from pipx ensurepath take effect; skipping this causes "command not found" errors. Then run:

pipx install plane-compose

Verify it worked:

plane --version

You should see a version number. If you see command not found: plane, close and reopen the terminal once more and try again.

1.4 Install PyYAML

Steps 3 and 7 of this guide use a small Python helper that needs one extra piece installed:

python3 -m pip install --user pyyaml

If this fails with an error mentioning "externally-managed-environment", run this instead:

python3 -m pip install --user pyyaml --break-system-packages

1.5 Connect to your Plane account

You need an API token, a secret key that lets Plane Compose act on your behalf:

  1. Open Plane in your browser and log in.

  2. Go to your avatar > Settings > Personal Access Tokens

  3. Create a new token and copy it right away; it's only shown once.

Now, back in the terminal:

plane auth login

It will ask a few questions: accept the default server URL by pressing Enter (unless your company runs its own Plane server), paste your token when asked, and enter your workspace slug (explained in Part 2 below if you're unsure).

Verify you're connected:

plane auth list-connections

You should see your connection listed. Setup is done; you never need to repeat Part 1.


Part 2: Find your workspace and project details

You need three pieces of information. All of them are visible in your browser's address bar when you're looking at the project in Plane:

https://app.plane.so/my-workspace/projects/...
                     ^^^^^^^^^^^^
                     workspace slug
  • Workspace slug: the first part of the URL after app.plane.so/ (e.g. my-workspace).

  • Source project key: the short prefix on the project's work items (e.g. MOBILE in MOBILE-42). It's also shown next to the project name in Plane.

Throughout the steps below, replace MOBILE, MOBILE2, and my-workspace with your own values.


Part 3: Duplicate the project

Step 1 below moves your terminal into the project's folder, and every later step assumes you're still there. If you close the terminal partway through, open a new one and run cd mobile (using your project key, in lowercase) before continuing.

Step 1: Clone the source project

This downloads the project's schema, settings, and all work items into a local folder on your computer.

plane clone MOBILE --workspace my-workspace

MOBILE is the id of your project.

Compose creates a folder named after the project key in lowercase. Move into it:

cd mobile

Inside this folder you'll find plane.yaml (project settings), a schema/ directory (states, labels, types), and work/workitems.yaml with all the work items already inside.

Step 2: Back up the work items file

You're about to temporarily strip relations from the file. Keep a copy so you can restore it later.

cp work/workitems.yaml work/workitems_with_relations.yaml

Step 3: Strip relations for the first push

Relations in the file reference items by their source project identifiers (e.g. MOBILE-5). Those identifiers don't exist yet in the new project, so a first push with relations present would silently skip them. This command removes them for now; you'll restore them in Step 7.

Paste the entire block below as one command (including the quote marks on the first and last lines) and press Enter:

python3 -c "
import yaml

with open('work/workitems.yaml') as f:
    data = yaml.safe_load(f)

def strip(item):
    if isinstance(item, dict):
        item.pop('dependencies', None)
        item.pop('relations', None)
        for child in item.get('children', []):
            strip(child)
    return item

if isinstance(data, dict) and 'workitems' in data:
    data['workitems'] = [strip(i) for i in data['workitems']]
elif isinstance(data, list):
    data = [strip(i) for i in data]

with open('work/workitems.yaml', 'w') as f:
    yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
"

The command prints nothing when it succeeds; no news is good news.

Why strip first? Relations are stored as MOBILE-N identifiers. On a first push to a brand new project, those identifiers aren't in Compose's lookup map yet, so any relation link would fail. After items are created in the new project, the map is populated and the second push can resolve them correctly.

Step 4: Configure plane.yaml for the new project

Now edit one file. Open plane.yaml in a text editor:

  • macOS: run open -e plane.yaml in the terminal; it opens in TextEdit.

  • Windows: run notepad plane.yaml in PowerShell.

  • If you have an editor like VS Code, feel free to use that instead.

Update the marked fields so they point to your new (not yet created) project:

IMPORTANT

Clear the uuid field in the plane.yaml file, so that it does not overwrite the original project.

type: project
workspace: my-workspace
project:
  key: MOBILE2         # change to your new project key
  uuid: ""             # clear this; it will be filled in after push
  name: Mobile App v2  # any name you like
  description: ""
  network: public
  timezone: UTC

Then save the file and close the editor.

Careful with spacing: this file format is indentation-sensitive. Only change the text after the colons; don't add or remove spaces at the start of any line, and don't let the editor change straight quotes " into curly ones.

Tip: The project key (e.g. MOBILE2) becomes the prefix for all new items: MOBILE2-1, MOBILE2-2, etc. Pick something meaningful.

Step 5: Push the schema to create the new project

This creates the project in Plane and pushes all schema settings: states, labels, types, workflows, and features. The new project will be an exact copy of the source schema.

plane schema push --force

Work Item Types error? If you see 400: Work item type not found in this project, go to your new project in Plane > Settings > Features > enable Work Item Types, then re-run this command.

Step 6: First push, create all work items

This creates every work item in the new project. Since relations were stripped in Step 3, nothing will fail. After this completes, Compose's state file records the mapping from each source identifier to its new UUID in the new project.

plane push --work-only --force

You should see all items created with a success summary. Module assignments, parent/child hierarchies, assignees, labels, and custom properties are all applied in this pass.

Step 7: Restore the file with relations

Bring back the backup you made in Step 2. This reintroduces the dependencies and relations fields into the file:

cp work/workitems_with_relations.yaml work/workitems.yaml

The restored file still carries leftover id labels from the old project. Remove them so they can't conflict with the new project's numbering. Paste this entire block as one command, just like in Step 3:

python3 -c "
import yaml

with open('work/workitems.yaml') as f:
    data = yaml.safe_load(f)

def strip(item):
    if isinstance(item, dict):
        item.pop('id', None)
        for child in item.get('children', []):
            strip(child)
    return item

if isinstance(data, dict) and 'workitems' in data:
    data['workitems'] = [strip(i) for i in data['workitems']]
elif isinstance(data, list):
    data = [strip(i) for i in data]

with open('work/workitems.yaml', 'w') as f:
    yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
"

Compose now detects a content change (the restored relations produce a different file than what was recorded after Step 6). This is what causes the next push to run updates rather than skip everything.

Step 8: Second push, link all relations

Now that the state file has the full identifier map (each MOBILE-N mapped to its UUID in the new project), Compose can resolve every relation target. Each item gets updated with its dependencies and relations.

plane push --work-only --force

Done. Your duplicate project now has all work items with their relations fully linked. Verify in Plane by opening any item that had a dependency; the linked items should show the new project's identifiers (e.g. MOBILE2-5).


Why two pushes?

When Compose pushes work items, it resolves relation targets (like MOBILE-5) by looking them up in an identifier map, a table that records which local item ID corresponds to which UUID in Plane. On a brand-new project, this map starts empty.

As items are created during the first push, each one gets registered in the map under the new project's identifiers (MOBILE2-1, MOBILE2-2, etc.), not the source identifiers. Since items are created in parallel, there's no guarantee item B is registered before item A tries to link to it. Even if it were, the lookup key is MOBILE-5 but the registered key is MOBILE2-5; they don't match.

After the first push, the state file has a complete picture: the new UUID for every MOBILE-N item. On the second push, that map is pre-loaded before any work begins, so when Compose processes blocked_by: [MOBILE-5], it finds the UUID immediately and creates the link correctly.

Clone > Back up > Strip relations > Push 1: create items > Restore relations > Push 2: link relations

Troubleshooting

command not found: plane The terminal hasn't picked up the new installation yet. Close the terminal window entirely, open a new one, and try again. If it persists, re-run pipx ensurepath, then restart the terminal once more.

ModuleNotFoundError: No module named 'yaml' (Step 3 or 7) PyYAML isn't installed. Run python3 -m pip install --user pyyaml (see setup step 1.4), then re-run the command that failed.

No such file or directory: work/workitems.yaml Your terminal isn't inside the project folder. Run cd mobile (your project key, in lowercase) and try again.

Schema push fails with "Work item type not found in this project" The Work Item Types feature is disabled for the new project. Go to Plane > your new project > Settings > Features > toggle Work Item Types on. Then re-run plane schema push --force.

Second push says "Nothing to push" and relations aren't created The restore in Step 7 didn't change the file; the backup may not have contained any relations. Verify with:

grep -c "blocked_by\|relates_to\|duplicate" work/workitems_with_relations.yaml

If the count is 0, the source project had no relations to begin with.

Some relations are missing after the second push Run plane push --work-only --force a third time; it's safe to repeat and will resolve any stragglers from parallel processing races.

Items appear duplicated in the new project This happens if plane state clear-items was run between pushes. Do not clear state; Compose uses it to track which items already exist. Delete the duplicates from the Plane UI, then run plane pull --work-only --force to re-sync state before pushing again.

The clone command can't find the project Try using the full project UUID instead of the key:

plane clone <uuid> --workspace=my-workspace

You can find the UUID in the Plane project URL. Or run plane clone --workspace=my-workspace without a project argument for an interactive picker.