diff --git a/docs/dev-tools/plane-compose.md b/docs/dev-tools/plane-compose.md index 609b425..6355b65 100644 --- a/docs/dev-tools/plane-compose.md +++ b/docs/dev-tools/plane-compose.md @@ -6,34 +6,9 @@ keywords: plane compose, plane yaml, infrastructure as code, plane cli, plane pr # Plane Compose -Plane Compose is a command-line tool that lets you define and manage Plane projects using YAML configuration files instead of the web interface. Think of it as "infrastructure as code" for project management — you write your project structure in files, version control them with Git, and sync them with Plane. - -## Features - -- **Local-first workflow** - Define everything in YAML and version control with Git. Your project structure, work item types, workflows, and tasks all live in your repository. -- **Bidirectional sync** - Push local changes to Plane or pull remote changes down. Keep your local files and Plane in sync however you prefer to work. -- **Auto-create projects** - Projects are created automatically in Plane when you push a schema. No need to set things up manually first. -- **Rich schema management** - Define work item types with custom fields, create workflow state machines, and organize labels into groups. -- **Two sync modes** - Use collaborative mode (plane push) for team-friendly additive syncing, or declarative mode (plane apply) when YAML should be the single source of truth. -- **Intelligent change detection** - Content-based diffing means only actual changes get pushed. Stable IDs (user-defined or content-hashed) prevent duplicate work items. -- **State tracking** - Terraform-style .plane/state.json tracks what's been synced, so Plane Compose knows what changed since the last push. -- **Built-in rate limiting** - Respects Plane API limits with configurable throttling (50 requests/minute by default). Monitor usage with plane rate stats. -- **Project cloning** - Clone an existing Plane project by UUID to start working with it locally. -- **Debug mode** - Comprehensive logging and error handling when you need to troubleshoot. - -## Installation - -Install Plane Compose globally using pipx (recommended): +Plane Compose is a command-line tool that lets you define and manage Plane projects using YAML configuration files. Think of it as "project as code", you write your project structure, schema, and work items in files, version control them with Git, and sync them with Plane. + +## Install Plane Compose ```bash pipx install plane-compose @@ -53,635 +28,1365 @@ To upgrade to the latest version: pipx upgrade plane-compose ``` -## Getting started +--- + +## Authenticate + +```bash +plane auth login +``` -This tutorial walks you through creating your first Plane Compose project and syncing it with Plane. +You will be prompted for: -### Initialize a project +- **Server URL** - leave blank for `https://api.plane.so`; enter your instance URL if self-hosted +- **Auth type** - `pat` for a Personal Access Token, `workspace` for a workspace-scoped token +- **Token** - your API key, generated at `https://app.plane.so//settings/account/api-tokens/` +- **Workspace** - your workspace slug (the URL segment after `app.plane.so/`) -Create a new project directory with the default structure: +Verify it worked: ```bash -plane init my-project -cd my-project +plane auth list-connections ``` -This creates: +--- + +## Start a new project +```bash +plane init my-project --workspace myteam ``` -my-project/ -├── plane.yaml # Project configuration -├── schema/ -│ ├── types.yaml # Work item types (task, bug, etc.) -│ ├── workflows.yaml # State machines -│ └── labels.yaml # Label definitions -├── work/ -│ └── inbox.yaml # Work items to create -└── .plane/ - └── state.json # Sync state (auto-managed) + +Then push the schema to create the project in Plane: + +```bash +cd my-project +plane schema push ``` -::: tip -You can set the workspace and project key directly from the command line: +`plane.yaml` is updated with the project UUID after this runs. + +To start from a template instead of the default schema: ```bash -plane init my-project --workspace myteam --project API +plane init my-project --workspace myteam --template default +# or a Git URL: +plane init my-project --workspace myteam \ + --template https://github.com/myorg/templates/engineering ``` -This saves you from having to edit plane.yaml afterward. -::: +--- -### Authenticate with Plane +## Clone an existing project -Log in using your API key: +Use this when the project already exists in Plane and you want to manage it locally. ```bash -plane auth login +plane clone myteam/API ``` -When prompted, enter your API key. You can generate one at `https://app.plane.so//settings/account/api-tokens/`. - -To verify your authentication: +Or using the project UUID: ```bash -plane auth whoami +plane clone abc-123-def-456 --workspace myteam ``` -### Configure your workspace +This downloads `plane.yaml`, all schema files, and all work files into a new local directory. -Open `plane.yaml` and set your workspace name: +--- -```yaml -workspace: your-workspace-slug -project: - key: PROJ - name: My Project +## Push changes to Plane -defaults: - type: task - workflow: standard +From inside the project directory: + +```bash +plane push ``` -:::tip -The default schema includes common work item types, states, and labels. You can customize `schema/types.yaml`, `schema/workflows.yaml`, and `schema/labels.yaml` before pushing, or use the defaults and adjust later. -::: +Preview what will change before pushing: -### Push your schema +```bash +plane push --dry-run +``` -Create the project in Plane along with its work item types, states, and labels: +If you have only changed schema files: ```bash -plane schema push +plane push --schema-only ``` -After this runs, `plane.yaml` is automatically updated with the project UUID. +If you have only changed work items: + +```bash +plane push --work-only +``` -### Add work items +--- -Edit `work/inbox.yaml` to define your work items: +## Pull remote changes from Plane -```yaml -- id: "auth-oauth" - title: Implement user authentication - type: task - priority: high - labels: [backend, feature] - state: todo - description: Add OAuth2 authentication - assignee: dev@example.com - watchers: - - pm@example.com - - qa@example.com +Use this when changes have been made in Plane (via the UI or by other users) and you want to bring them into your local files. -- id: "bug-login-css" - title: Fix login button CSS - type: bug - priority: medium - labels: [frontend, bug] - state: backlog +```bash +plane pull ``` -### Push work items +To keep local additions and apply remote changes without losing local-only items: + +```bash +plane pull --merge +``` -Preview what will be pushed: +To overwrite local files entirely with what is in Plane: ```bash -plane push --dry-run +plane pull --force ``` -When you're ready, push to Plane: +--- + +## Import schema changes made in Plane + +Use this when someone has modified work item types, states, labels, or workflows directly in the Plane UI and your local schema files are now out of sync. + +To reconnect local names to their remote IDs without changing any YAML (safe, no file changes): ```bash -plane push +plane schema import ``` -## Cloning an existing project +To add items that exist in Plane but are absent from your local files, without touching what you already have: -If you want to work with a project that already exists in Plane, clone it by UUID: +```bash +plane schema import --merge +``` + +To replace your local schema files entirely with whatever is in Plane: ```bash -plane clone abc-123-def-456 --workspace my-workspace -cd +plane schema import --force ``` -The remote work items are pulled to `.plane/remote/items.yaml`. You can review them, make changes in `work/inbox.yaml`, and push updates back to Plane. +--- -## Project structure +## Upgrade a project to a new template version -When you initialize a project, Plane Compose creates the following structure: +Use this when your team has updated the standard template and you want to bring an existing project in line with it. +Preview the changes first: + +```bash +plane upgrade --template https://github.com/myorg/templates/engineering --dry-run ``` -my-project/ -├── plane.yaml # Project configuration -├── schema/ -│ ├── types.yaml # Work item types (task, bug, etc.) -│ ├── workflows.yaml # State machines -│ └── labels.yaml # Label definitions -├── work/ -│ └── inbox.yaml # Work items to create -└── .plane/ - └── state.json # Sync state (auto-managed) + +Apply the upgrade: + +```bash +plane upgrade --template https://github.com/myorg/templates/engineering ``` -### plane.yaml +The template merges over your local schema. Items unique to your project are preserved; conflicts are resolved in favour of the template. -The main configuration file for your project: +--- -```yaml -workspace: myteam -project: - key: API # Short key or UUID - name: API Project +## Run Plane Compose in CI/CD -defaults: - type: task - workflow: standard +Authenticate non-interactively using flags: + +```bash +plane auth login \ + --server-url https://api.plane.so \ + --auth-type pat \ + --token "$PLANE_TOKEN" \ + --workspace myteam +``` + +Push without prompts and with a machine-readable exit code: + +```bash +plane push --force --no-conflict-check --exit-code ``` -### schema/types.yaml +Exit code values: `0` - no changes needed, `1` - error, `2` - changes were applied. -Define work item types and their fields: +--- -```yaml -task: - description: A single unit of work - workflow: standard - fields: - - name: title - type: string - required: true - - name: priority - type: enum - options: [none, low, medium, high, urgent] +## Manage workspace configuration + +Clone workspace-level configuration (work item types, members, releases) to a local directory: + +```bash +plane ws clone myteam ``` -### schema/workflows.yaml +After making changes locally, push them to Plane: -Define state machines with states and transitions: +```bash +plane ws push +``` -```yaml -standard: - states: - - name: backlog - group: unstarted - color: "#858585" - - name: in_progress - group: started - color: "#f59e0b" - - name: done - group: completed - color: "#22c55e" - initial: backlog - terminal: [done] +To pull the latest workspace state from Plane: + +```bash +plane ws pull +``` + +To pull and preserve local additions: + +```bash +plane ws pull --merge ``` -### schema/labels.yaml +--- -Organize labels into groups: +## Work with multiple projects -```yaml -groups: - area: - color: "#3b82f6" - labels: - - name: frontend - - name: backend +From a directory containing multiple project subdirectories: + +```bash +plane push --all +plane pull --all +plane status --all ``` -### work/inbox.yaml +To limit to a specific workspace: -Define work items to sync with Plane: +```bash +plane push --all --workspace myteam +``` -```yaml -- id: "auth-oauth" - title: Implement user authentication - type: task - priority: high - labels: [backend, feature] - state: todo - description: Add OAuth2 authentication - assignee: dev@example.com - watchers: - - pm@example.com - - qa@example.com +To filter by project name: -- id: "bug-login-css" - title: Fix login button CSS - type: bug - priority: medium - labels: [frontend, bug] - state: backlog +```bash +plane push --all --filter "api-*" ``` -## Understanding sync modes +--- + +## Recover from sync problems + +**State file deleted or corrupted:** -Plane Compose offers two sync modes for different workflows. +```bash +plane schema import # reconnects schema names to remote IDs +plane pull # restores work item entries +``` -### Collaborative mode (plane push) +**A specific item is stuck or needs to be re-created:** -Use `plane push` when working with a team. This mode is additive-only — it creates and updates work items but never deletes them. Team members can safely push their changes without accidentally removing someone else's work. +Remove its state entry so the next push treats it as new: ```bash -plane push # Push new and updated items -plane push --dry-run # Preview changes first -plane push --force # Skip confirmation prompt +plane state remove types.Story # for a schema item +plane state remove work_items.AUTH-1 # for a work item ``` -### Declarative mode (plane apply) +**All work items need to be re-pushed:** -Use `plane apply` when you want your YAML files to be the single source of truth. This mode creates, updates, and deletes work items to match exactly what's in your files. +```bash +plane state clear-items +plane push +``` -To prevent accidental deletions, declarative mode only affects work items within a defined scope. Configure the scope in `plane.yaml`: +**A push was interrupted and some items failed:** -```yaml -apply_scope: - labels: ["automated"] - assignee: "bot@example.com" - id_prefix: "AUTO-" +```bash +plane push --resume ``` -With this configuration, `plane apply` only manages work items that match this scope—everything else is left untouched. +**Diagnosing what went wrong:** ```bash -plane apply # Sync with create/update/delete -plane apply --dry-run # Preview all changes including deletions -plane apply --force # Skip confirmation prompt +plane --debug push +tail -f ~/.config/plane-compose/plane.log ``` -## Working with schemas +## Reference -Schemas define the structure of your project: work item types, workflows, and labels. +### CLI commands -### Work item types +#### `plane init` -Define types in `schema/types.yaml`: +Initialises a new project directory with the standard file structure: `plane.yaml`, `schema/`, `work/`, and `.plane/state.json`. If a template is specified, schema and work files are pre-populated from the template source. If called without arguments, the command runs interactively and prompts for workspace and project values. -```yaml -task: - description: A single unit of work - workflow: standard - fields: - - name: title - type: string - required: true - - name: priority - type: enum - options: [none, low, medium, high, urgent] - -bug: - description: A defect to fix - workflow: bug-workflow - fields: - - name: title - type: string - required: true - - name: severity - type: enum - options: [minor, major, critical] -``` - -### Workflows - -Define state machines in `schema/workflows.yaml`: +``` +plane init [PROJECT] [--workspace WS] [--connection CONN] + [--path PATH] [--template TEMPLATE] +``` -```yaml -standard: - states: - - name: backlog - group: unstarted - color: "#858585" - - name: in_progress - group: started - color: "#f59e0b" - - name: done - group: completed - color: "#22c55e" - initial: backlog - terminal: [done] - -bug-workflow: - states: - - name: reported - group: unstarted - color: "#ef4444" - - name: investigating - group: started - color: "#f59e0b" - - name: fixed - group: completed - color: "#22c55e" - initial: reported - terminal: [fixed] -``` - -### Labels - -Organize labels into groups in `schema/labels.yaml`: +| Option | Description | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PROJECT` | Name of the directory to create. Also used as the project key if `--project` is not set separately. | +| `--workspace WS` | Slug of the Plane workspace this project belongs to. Written into `plane.yaml`. | +| `--connection CONN` | ID of the connection to use for API calls during initialisation. Defaults to the connection linked to the workspace. | +| `--path PATH` | Parent directory in which to create the project folder. Defaults to the current working directory. | +| `--template TEMPLATE` | Source for pre-populating the schema and work files. Accepts a built-in name (e.g. `default`), a local filesystem path, a Git HTTPS URL, or a Git SSH URL. The resolved value is written to `plane.yaml` under `template` so that `plane upgrade` can reference it later. | -```yaml -groups: - area: - color: "#3b82f6" - labels: - - name: frontend - - name: backend - - name: infrastructure +--- + +#### `plane auth login` + +Stores a new set of credentials as a connection in `~/.config/plane-compose/config.json` and links it to a workspace. When all flags are provided, the command runs non-interactively, making it suitable for CI/CD pipelines. On success, prints the generated connection ID. - priority: - color: "#ef4444" - labels: - - name: urgent - - name: high - - name: low +``` +plane auth login [--server-url URL] [--auth-type TYPE] + [--token TOKEN] [--workspace WS] [--connection CONN] ``` -### Validating and pushing schemas +| Option | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--server-url URL` | Base URL of the Plane API. Defaults to `https://api.plane.so`. Set this to your instance URL when using a self-hosted deployment. | +| `--auth-type TYPE` | Token type. `pat` for a Personal Access Token scoped to a user. `workspace` for a workspace-scoped token. | +| `--token TOKEN` | The API token value. | +| `--workspace WS` | Workspace slug to associate with this connection. The association is stored so that commands can resolve credentials from `plane.yaml` automatically. | +| `--connection CONN` | Optional explicit ID to assign to this connection. If omitted, an ID is generated automatically. | -Before pushing, validate your schema files: +--- + +#### `plane auth logout` + +Removes a stored connection and all its associated workspace links from `~/.config/plane-compose/config.json`. This operation is irreversible without re-authenticating. -```bash -plane schema validate +``` +plane auth logout CONNECTION_ID [--force] ``` -Push the schema to Plane: +| Option | Description | +| --------------- | -------------------------------------------------------------------------- | +| `CONNECTION_ID` | ID of the connection to remove, as shown by `plane auth list-connections`. | +| `--force` | Skips the confirmation prompt before deletion. | + +--- + +#### `plane auth list-connections` + +Prints all stored connections with their server URL, auth type, and linked workspaces. The default workspace is marked with \*. Aliases: `whoami`, `connections`, `list`. -```bash -plane schema push -plane schema push --dry-run # Preview first +``` +plane auth list-connections ``` -## Working with work items +--- -### Work item fields +#### `plane auth connect-workspace` -Each work item in your YAML files can include these fields: +Associates a workspace slug with an existing connection. After this, any command targeting that workspace will use the specified connection's credentials without requiring an explicit `--connection` flag. -| Field | Description | -| ------------- | -------------------------------------------------- | -| `id` | Stable identifier for tracking (recommended) | -| `title` | Work item title (required) | -| `type` | Work item type (defaults to value in `plane.yaml`) | -| `priority` | `none`, `low`, `medium`, `high`, or `urgent` | -| `state` | Current state from the workflow | -| `labels` | List of label names | -| `description` | Detailed description | -| `assignee` | Email of the assignee | -| `watchers` | List of watcher emails | +``` +plane auth connect-workspace WORKSPACE_SLUG --connection CONN_ID +``` -### Using stable IDs +| Option | Description | +| ---------------------- | --------------------------------------------- | +| `WORKSPACE_SLUG` | The Plane workspace slug to link. | +| `--connection CONN_ID` | ID of the connection to link it to. Required. | -Always use stable IDs to prevent duplicate work items: +--- -```yaml -- id: "feature-user-auth" - title: Implement user authentication - type: task +#### `plane auth disconnect-workspace` + +Removes the association between a workspace slug and a connection. After this, commands targeting that workspace will require an explicit `--connection` flag or re-authentication. + +``` +plane auth disconnect-workspace WORKSPACE_SLUG [--connection CONN_ID] ``` -The ID can be any string that uniquely identifies the work item. Without an ID, Plane Compose uses content hashing, which can create duplicates if you change the title or description. +| Option | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `WORKSPACE_SLUG` | The workspace slug to unlink. | +| `--connection CONN_ID` | ID of the connection to unlink from. If omitted and the workspace has only one associated connection, that connection is unlinked automatically. | -### Pulling remote changes +--- -To see what's currently in Plane: +#### `plane schema validate` -```bash -plane pull +Checks all schema files in the project for structural errors, unknown field types, missing required fields, and invalid references. Runs entirely offline - no API connection is made. Exits with a non-zero code if any errors are found. + +``` +plane schema validate [PROJECT] [--path PATH] ``` -This downloads work items to `.plane/remote/items.yaml`. You can compare this with your local files to see what's different. +| Option | Description | +| ------------- | ------------------------------------------------------------ | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | -### Combined sync +--- -Run schema push and work item push together: +#### `plane schema push` -```bash -plane sync +Pushes local schema files to Plane. Creates the project in Plane if it does not already exist. Applies changes to work item types, states, workflows, and labels in the order required by the Plane API. On first push, writes the project UUID back into `plane.yaml`. Updates `.plane/state.json` with remote ID mappings for all pushed schema items. + +``` +plane schema push [PROJECT] [--path PATH] [--dry-run] [--force] ``` -## Monitoring and rate limits +| Option | Description | +| ------------- | ----------------------------------------------------------------------------------------------- | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--dry-run` | Computes and displays the full change plan without making any API calls or modifying any files. | +| `--force` | Skips the interactive confirmation prompt before applying changes. | -### Check sync status +--- -View the current state of your local project: +#### `plane schema import` + +Reads the current schema from the Plane remote and reconciles it with local files. The behaviour depends on which flag is used. Without flags, only `.plane/state.json` is updated - no YAML files are modified. This is the safe mode for reconnecting local names to remote IDs after out-of-band changes. -```bash -plane status +``` +plane schema import [PROJECT] [--path PATH] [--merge] [--force] ``` -### Rate limit management +| Option | Description | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--merge` | Writes remote schema items that are absent from local files into the appropriate YAML files. Existing local items are not modified. Additive only. | +| `--force` | Replaces the contents of local schema files entirely with what is returned from the Plane API. Any local-only items are lost. | -Plane Compose includes built-in rate limiting (50 requests per minute by default). To check your current rate limit stats: +--- -```bash -plane rate stats +#### `plane schema diff` + +Fetches the current schema from Plane and compares it to local schema files. Prints a structured diff showing which types, states, workflows, and labels differ. Makes no changes to local files or the remote. + +``` +plane schema diff [PROJECT] [--path PATH] ``` -To reset the statistics: +| Option | Description | +| ------------- | ------------------------------------------------------------ | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | -```bash -plane rate reset +--- + +#### `plane push` + +Pushes schema and work data to Plane in dependency order: schema first (types, states, workflows, labels), then work items, then cycles and modules, then milestones. Skips items whose content hash matches the hash stored in `.plane/state.json`. Updates state after each successful push. If the schema push fails, work data push does not proceed. + +``` +plane push [PROJECT] [--path PATH] [--connection CONN] + [--dry-run] [--force] [--schema-only] [--work-only] + [--skip SECTION] [--all] [--workspace WS] [--filter PATTERN] + [--no-conflict-check] [--exit-code] [--resume] ``` -If you hit rate limits, you can reduce the request rate: +| Option | Description | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--connection CONN` | Overrides the connection resolved from `plane.yaml` for this invocation only. | +| `--dry-run` | Computes and displays the full change plan without making any API calls or writing to state. | +| `--force` | Skips the interactive confirmation prompt before applying changes. | +| `--schema-only` | Pushes only schema files. Equivalent to running `plane schema push`. Skips `workitems`, `cycles`, `modules`, and `milestones`. | +| `--work-only` | Skips the schema push phase and pushes only work files. Requires schema to already be in sync. | +| `--skip SECTION` | Excludes a specific section from the push. Repeatable. Valid values: `workitems`, `cycles`, `modules`, `milestones`. | +| `--all` | Discovers all project directories under the current directory and pushes each one. | +| `--workspace WS` | When used with `--all`, restricts discovery to projects belonging to this workspace. | +| `--filter PATTERN` | When used with `--all`, applies a glob pattern to filter project directory names. | +| `--no-conflict-check` | Skips the pre-push API call that detects remote conflicts. Reduces API usage. Recommended for CI/CD pipelines where conflicts are not expected. | +| `--exit-code` | Returns a differentiated exit code: `0` if no changes were needed, `1` on error, `2` if changes were successfully applied. Useful for scripting and CI gate logic. | +| `--resume` | Reads the failure log from the previous push and retries only the items that failed. Items that succeeded in the previous run are not re-pushed. | -```bash -export PLANE_RATE_LIMIT_PER_MINUTE=30 -plane push +--- + +#### `plane pull` + +Fetches schema and work data from Plane and writes it to local files. Without flags, overwrites local files with remote content after prompting for confirmation. Updates `.plane/state.json` with the latest remote IDs and content hashes. + +``` +plane pull [PROJECT] [--path PATH] [--connection CONN] + [--merge] [--force] [--schema-only] [--work-only] + [--skip SECTION] [--with-properties] [--no-properties] + [--all] [--workspace WS] [--filter PATTERN] ``` -## Configuration reference +| Option | Description | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--connection CONN` | Overrides the connection resolved from `plane.yaml` for this invocation only. | +| `--merge` | Applies remote changes to local files while preserving items that exist locally but not remotely. Local-only items are not deleted. | +| `--force` | Overwrites local files with remote content without prompting. Local-only items are lost. | +| `--schema-only` | Pulls only schema files. Skips `workitems`, `cycles`, `modules`, and `milestones`. | +| `--work-only` | Pulls only work files. Skips schema. | +| `--skip SECTION` | Excludes a specific section from the pull. Repeatable. Valid values: `workitems`, `cycles`, `modules`, `milestones`. | +| `--with-properties` | Includes custom property values in the pulled work items. Enabled by default. | +| `--no-properties` | Excludes custom property values from pulled work items. The `properties` map is omitted from each work item in the output file. | +| `--all` | Discovers all project directories under the current directory and pulls each one. | +| `--workspace WS` | When used with `--all`, restricts discovery to projects belonging to this workspace. | +| `--filter PATTERN` | When used with `--all`, applies a glob pattern to filter project directory names. | -### plane.yaml +--- -```yaml -workspace: my-workspace -project: - key: PROJ # User-defined short key - uuid: abc-123 # Auto-added after schema push - name: My Project +#### `plane clone` -defaults: - type: task - workflow: standard +Downloads a complete Plane project - including `plane.yaml`, all schema files, and all work files - into a new local directory. Initialises `.plane/state.json` with the remote IDs of all cloned items. The project must already exist in Plane. -# Optional: Scope for declarative mode -apply_scope: - labels: ["automated"] - assignee: "bot@example.com" - id_prefix: "AUTO-" +``` +plane clone PROJECT [--directory DIR] [--path PATH] + [--workspace WS] [--connection CONN] + [--schema-only] [--skip SECTION] [--with-properties] ``` -### Environment variables +| Option | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PROJECT` | Project identifier. Accepts three formats: `workspace/KEY` shorthand (e.g. `myteam/API`), a project key with `--workspace` flag, or a UUID with `--workspace` flag. | +| `--directory DIR` | Name of the local directory to create. Defaults to the project key. | +| `--path PATH` | Parent directory in which to create the project folder. Defaults to the current working directory. | +| `--workspace WS` | Workspace slug. Required when `PROJECT` is a key or UUID rather than a shorthand. | +| `--connection CONN` | Overrides the connection resolved from the workspace for this invocation only. | +| `--schema-only` | Downloads schema files only. Skips `workitems`, `cycles`, `modules`, and `milestones`. | +| `--skip SECTION` | Excludes a specific section from the clone. Repeatable. Valid values: `workitems`, `cycles`, `modules`, `milestones`. | +| `--with-properties` | Includes custom property values in the cloned work items. Enabled by default. | -| Variable | Default | Description | -| ----------------------------- | ---------------------- | --------------------------- | -| `PLANE_API_URL` | `https://api.plane.so` | API endpoint | -| `PLANE_API_TIMEOUT` | `30` | Request timeout in seconds | -| `PLANE_RATE_LIMIT_PER_MINUTE` | `50` | Maximum requests per minute | -| `PLANE_DEBUG` | `false` | Enable debug mode | -| `PLANE_VERBOSE` | `false` | Enable verbose output | -| `PLANE_LOG_TO_FILE` | `false` | Write logs to file | +--- -### Credentials storage +#### `plane diff` -API keys are stored securely at `~/.config/plane-compose/credentials`. +Fetches work items from Plane and compares them to local work files. Classifies each item into one of six categories and prints a structured report. Makes no changes to local files or the remote. -Logs are written to `~/.config/plane-compose/plane.log` when debug mode is enabled. +``` +plane diff [PROJECT] [--path PATH] [--connection CONN] +``` -## Command reference +| Option | Description | +| ------------------- | ----------------------------------------------------------------------------- | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--connection CONN` | Overrides the connection resolved from `plane.yaml` for this invocation only. | -### Project management +**Output categories:** -| Command | Description | -| -------------------- | --------------------------------- | -| `plane init [path]` | Initialize a new project | -| `plane status` | Show sync status | -| `plane clone ` | Clone an existing project by UUID | +| Category | Meaning | +| ----------------- | ------------------------------------------------------------------------------------------------ | +| `new_local` | Item exists in local files but has no corresponding remote record. | +| `modified_local` | Item exists both locally and remotely, and the local version differs from the remote. | +| `modified_remote` | Item exists both locally and remotely, and the remote version differs from what was last synced. | +| `conflicts` | Item has been modified both locally and remotely since the last sync. | +| `in_sync` | Local and remote versions are identical. | +| `deleted_remote` | Item has been deleted from Plane but still exists in local files. | -### Authentication +--- -| Command | Description | -| ------------------- | ------------------------- | -| `plane auth login` | Authenticate with API key | -| `plane auth whoami` | Show current user | -| `plane auth logout` | Remove stored credentials | +#### `plane validate` -### Schema management +Validates work item files against the project schema. Checks for unknown type names, unknown state names, unknown label names, invalid priority values, duplicate `id` values, malformed dates, and missing required fields. By default, fetches the current schema from Plane to validate against. Exits with a non-zero code if any errors are found. -| Command | Description | -| ----------------------------- | --------------------------- | -| `plane schema validate` | Validate local schema files | -| `plane schema push` | Push schema to Plane | -| `plane schema push --dry-run` | Preview schema changes | +``` +plane validate [PATH] [--offline] [--json] +``` -### Work items (collaborative mode) +| Option | Description | +| ----------- | ----------------------------------------------------------------------------------------------------------- | +| `PATH` | Filesystem path to the project root. Defaults to the current directory. | +| `--offline` | Skips the API call to fetch the remote schema. Validates only against local schema files. | +| `--json` | Outputs validation errors as a JSON array instead of formatted text. Useful for scripting and CI pipelines. | -| Command | Description | -| ---------------------- | ------------------------------- | -| `plane push` | Push new/updated work items | -| `plane push --dry-run` | Preview changes | -| `plane push --force` | Push without confirmation | -| `plane pull` | Pull work items from Plane | -| `plane sync` | Run schema push + push together | +--- -### Work items (declarative mode) +#### `plane status` -| Command | Description | -| ----------------------- | ------------------------------------ | -| `plane apply` | Declarative sync with delete support | -| `plane apply --dry-run` | Preview creates/updates/deletes | -| `plane apply --force` | Apply without confirmation | +Reads `.plane/state.json` and the local work files to produce a summary of the project's sync state: schema sync status, number of work items pending push, number of items in sync, and the timestamp of the last successful push. Does not make any API calls. -### Monitoring +``` +plane status [PATH] [--all] [--workspace WS] [--filter PATTERN] [--json] +``` -| Command | Description | -| ------------------ | --------------------------- | -| `plane rate stats` | Show rate limit statistics | -| `plane rate reset` | Reset rate limit statistics | +| Option | Description | +| ------------------ | ------------------------------------------------------------------------------------------ | +| `PATH` | Filesystem path to the project root. Defaults to the current directory. | +| `--all` | Discovers all project directories under the current directory and reports status for each. | +| `--workspace WS` | When used with `--all`, restricts discovery to projects belonging to this workspace. | +| `--filter PATTERN` | When used with `--all`, applies a glob pattern to filter project directory names. | +| `--json` | Outputs the status report as JSON. | -### Global options +--- -| Option | Description | -| ----------------- | --------------------- | -| `--verbose`, `-v` | Enable verbose output | -| `--debug` | Enable debug logging | +#### `plane upgrade` -## Troubleshooting +Applies a template to an existing project's schema. Pulls the latest schema from Plane first (unless `--skip-pull` is set), then computes a three-way merge between the current local schema, the current remote schema, and the template. Items present only in the template are added. Items in conflict between template and local are resolved in favour of the template. Items present only locally are preserved. Presents a plan before applying. -### Authentication failed (401) +``` +plane upgrade [PROJECT] --template TEMPLATE [--path PATH] + [--include-data] [--dry-run] [--force] + [--skip-pull] [--schema-only] +``` -Your API key may be invalid or expired. Log out and log in again: +| Option | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--template TEMPLATE` | Template source. Required. Accepts a built-in name, a local path, a Git HTTPS URL, or a Git SSH URL. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--include-data` | Also copies cycles, modules, and work items from the template into the local work files. | +| `--dry-run` | Computes and displays the upgrade plan without modifying any files or making any API calls. | +| `--force` | Skips the interactive confirmation prompt before applying the upgrade. | +| `--skip-pull` | Skips pulling the latest schema from Plane before computing the merge. Uses the current local schema as the base. | +| `--schema-only` | Applies only schema changes from the template. Does not copy cycles, modules, or work items even if `--include-data` is set. | -```bash -plane auth logout -plane auth login +--- + +#### `plane state show` + +Prints the contents of `.plane/state.json` as a structured report showing remote ID mappings and content hashes for schema items and work items. Makes no API calls. + +``` +plane state show [PROJECT] [--path PATH] [--json] ``` -### Permission denied (403) +| Option | Description | +| ------------- | ------------------------------------------------------------ | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--json` | Outputs the state as raw JSON. | -You may not have access to the workspace. Verify your membership: +--- -```bash -plane auth whoami +#### `plane state reset` + +Clears all entries from `.plane/state.json`. After a reset, the next `plane push` treats every local item as new and attempts to create it in Plane. Use with caution - this can result in duplicate remote items if the project already exists in Plane. + +``` +plane state reset [PROJECT] [--path PATH] [--force] ``` -Contact your workspace administrator if you need access. +| Option | Description | +| ------------- | ------------------------------------------------------------ | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--force` | Skips the confirmation prompt. | -### Project not found (404) +--- -The project UUID in `plane.yaml` may be stale. Remove it and recreate: +#### `plane state clear-items` -```bash -# Edit plane.yaml and delete the uuid line -plane schema push +Removes only the `work_items` section of `.plane/state.json`, leaving schema state intact. The next `plane push` re-pushes all work items as if they are new, but schema items are not affected. + +``` +plane state clear-items [PROJECT] [--path PATH] [--force] ``` -### Rate limit exceeded (429) +| Option | Description | +| ------------- | ------------------------------------------------------------ | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | +| `--force` | Skips the confirmation prompt. | -Check your rate limit status: +--- -```bash +#### `plane state remove` + +Removes a single entry from `.plane/state.json` identified by a dot-separated path. On the next push, the removed item is treated as new. + +``` +plane state remove PATH_STR [PROJECT] [--path PATH] +``` + +| Option | Description | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `PATH_STR` | Dot-separated path into the state object. For example: `types.Story` removes the Story type mapping; `states.Done` removes the Done state mapping; `work_items.AUTH-1` removes a specific work item mapping. | +| `PROJECT` | Project key or directory. Defaults to the current directory. | +| `--path PATH` | Explicit filesystem path to the project root. | + +--- + +#### `plane ws clone` + +Downloads workspace-level configuration - including `workspace.yaml`, workspace schema files (e.g. `workitem_types.yaml`), and member data - into a new local directory. Initialises `.plane/state.json` with remote ID mappings. + +``` +plane ws clone WORKSPACE [--directory DIR] [--path PATH] + [--connection CONN] [--force] [--skip SECTION] +``` + +| Option | Description | +| ------------------- | ------------------------------------------------------------------------------------- | +| `WORKSPACE` | Workspace slug to clone. | +| `--directory DIR` | Name of the local directory to create. Defaults to the workspace slug. | +| `--path PATH` | Parent directory for the workspace folder. Defaults to the current working directory. | +| `--connection CONN` | Overrides the connection resolved from the workspace slug for this invocation only. | +| `--force` | Overwrites an existing local directory without prompting. | +| `--skip SECTION` | Excludes a section from the clone. Valid values: `releases`. | + +--- + +#### `plane ws pull` + +Fetches workspace-level configuration from Plane and writes it to local workspace files. Behaviour with respect to local content is controlled by `--merge` and `--force`, identical in semantics to `plane pull`. + +``` +plane ws pull [WORKSPACE] [--path PATH] [--merge] [--force] [--skip SECTION] +``` + +| Option | Description | +| ---------------- | ---------------------------------------------------------------------------------------- | +| `WORKSPACE` | Workspace slug. Defaults to the value in `workspace.yaml`. | +| `--path PATH` | Explicit filesystem path to the workspace root. | +| `--merge` | Preserves local-only items while applying remote changes. | +| `--force` | Overwrites local files with remote content without prompting. Local-only items are lost. | +| `--skip SECTION` | Excludes a section from the pull. Valid values: `releases`. | + +--- + +#### `plane ws push` + +Pushes local workspace configuration files to Plane. Updates `.plane/state.json` with remote ID mappings for all pushed items. + +``` +plane ws push [WORKSPACE] [--path PATH] [--dry-run] [--force] +``` + +| Option | Description | +| ------------- | ------------------------------------------------------------------- | +| `WORKSPACE` | Workspace slug. Defaults to the value in `workspace.yaml`. | +| `--path PATH` | Explicit filesystem path to the workspace root. | +| `--dry-run` | Computes and displays the change plan without making any API calls. | +| `--force` | Skips the interactive confirmation prompt. | + +--- + +#### `plane ws diff` + +Fetches workspace configuration from Plane and compares it to local workspace files. Prints a structured diff. Makes no changes. + +``` +plane ws diff [WORKSPACE] [--path PATH] +``` + +| Option | Description | +| ------------- | ---------------------------------------------------------- | +| `WORKSPACE` | Workspace slug. Defaults to the value in `workspace.yaml`. | +| `--path PATH` | Explicit filesystem path to the workspace root. | + +--- + +#### `plane ws upgrade` + +Applies a template to an existing workspace's schema. Follows the same merge logic as `plane upgrade`. + +``` +plane ws upgrade [WORKSPACE] [--path PATH] [--template TEMPLATE] + [--dry-run] [--force] [--skip-pull] [--schema-only] +``` + +| Option | Description | +| --------------------- | ------------------------------------------------------------------------------------ | +| `WORKSPACE` | Workspace slug. Defaults to the value in `workspace.yaml`. | +| `--path PATH` | Explicit filesystem path to the workspace root. | +| `--template TEMPLATE` | Template source. Accepts a built-in name, local path, Git HTTPS URL, or Git SSH URL. | +| `--dry-run` | Displays the upgrade plan without applying it. | +| `--force` | Skips the confirmation prompt. | +| `--skip-pull` | Skips pulling the latest workspace schema from Plane before computing the merge. | +| `--schema-only` | Applies only schema changes. Does not copy data from the template. | + +--- + +#### `plane ws state show / reset / clear / remove` + +Manage workspace sync state in `.plane/state.json` within the workspace directory. Semantics are identical to their project-level equivalents (`plane state show`, `plane state reset`, etc.). + +``` +plane ws state show [WORKSPACE] [--path PATH] [--json] +plane ws state reset [WORKSPACE] [--path PATH] [--force] +plane ws state clear [WORKSPACE] [--path PATH] [--force] +plane ws state remove PATH_STR [WORKSPACE] [--path PATH] +``` + +`PATH_STR` examples for workspace state: `workitemtypes.types.Task`, `members.dev@example.com`, `releases.tags.v1.0`. + +--- + +#### `plane rate stats` + +Prints the current rate limit window statistics: total requests made, requests remaining, and the time until the window resets. Reads from local counters maintained by the token bucket; does not make an API call. + +``` plane rate stats ``` -Wait for the limit to reset, or reduce your request rate: +--- -```bash -export PLANE_RATE_LIMIT_PER_MINUTE=30 -plane push +#### `plane rate reset` + +Resets the local rate limit counters to zero. Does not affect Plane's server-side rate limiting. + +``` +plane rate reset ``` -### Duplicate work items +--- + +#### Global options + +These options are accepted by every `plane` command. -Always use stable IDs in your work items: +| Option | Description | +| ----------------- | -------------------------------------------------------------------------------------------- | +| `--version`, `-V` | Prints the installed version of Plane Compose and exits. | +| `--verbose`, `-v` | Enables verbose output. Prints additional detail about each operation as it runs. | +| `--debug` | Enables debug-level logging. Writes a structured log to `~/.config/plane-compose/plane.log`. | + +--- + +### Configuration files + +#### Project directory structure + +A project managed by Plane Compose has the following directory layout. All paths are relative to the project root. + +``` +/ +├── plane.yaml # project identity and sync settings +├── schema/ +│ ├── types.yaml # work item type definitions and custom properties +│ ├── states.yaml # state definitions and group assignments +│ ├── workflows.yaml # workflow definitions and transition rules +│ ├── labels.yaml # label definitions +│ ├── features.yaml # project feature flag configuration +│ ├── members.yaml # project membership (populated on pull; read-only) +│ ├── workitem_templates.yaml # project-level work item templates +│ └── page_templates.yaml # project-level page templates +├── work/ +│ ├── workitems.yaml # work item definitions +│ ├── cycles.yaml # cycle (sprint) definitions +│ ├── modules.yaml # module definitions +│ └── milestones.yaml # milestone definitions +├── .plane/ +│ ├── state.json # sync state: local name → remote UUID + content hash mappings +│ └── .state.lock # file lock held during active sync operations +└── .gitignore # generated on init; excludes .state.lock +``` + +A workspace directory created by `plane ws clone` has the following layout: + +``` +/ +├── workspace.yaml # workspace identity and connection settings +├── schema/ +│ └── workitem_types.yaml # workspace-level work item type definitions (Enterprise) +└── .plane/ + └── state.json +``` + +--- + +#### `plane.yaml` + +The primary configuration file for a project. Identifies the project, specifies the workspace and connection, and sets defaults used when work item fields are omitted. + +| Field | Type | Description | +| --------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | string | Always `project`. Identifies this directory as a project (as opposed to a workspace). | +| `workspace` | string | Slug of the Plane workspace this project belongs to. Used to resolve the correct connection from `~/.config/plane-compose/config.json`. | +| `connection` | string | Optional. ID of a specific connection to use for all operations on this project. Overrides the workspace-to-connection mapping. | +| `project.key` | string | Short project identifier, maximum 10 uppercase characters. Used as the prefix in work item sequence IDs (e.g. `API-42`). | +| `project.name` | string | Human-readable project name as displayed in Plane. | +| `project.uuid` | string | Remote UUID of the project. Populated automatically after the first `plane schema push`. | +| `project.description` | string | Optional. Project description as displayed in Plane. | +| `project.network` | string | Visibility setting. `public` makes the project visible to all workspace members. `private` restricts visibility. Defaults to `public`. | +| `project.timezone` | string | IANA timezone string (e.g. `UTC`, `America/New_York`). Affects due date display and cycle date calculations in Plane. | +| `defaults.type` | string | Default work item type applied when a work item in `work/workitems.yaml` does not specify a `type` field. Must match a key in `schema/types.yaml`. | +| `defaults.workflow` | string | Default workflow applied when a work item type does not specify one. Must match a key in `schema/workflows.yaml`. | +| `template` | string | Source of the template used during `plane init` or `plane upgrade`. Written automatically; used by `plane upgrade` to know where to pull the template from. | ```yaml -- id: "unique-identifier" - title: "My task" +type: project +workspace: myteam +connection: conn-1 +project: + key: API + name: API Project + uuid: abc-123-def-456 + description: "" + network: public + timezone: UTC +defaults: + type: Story + workflow: default +template: default ``` -Without IDs, content changes can create duplicates instead of updates. +--- -### State corruption +#### `schema/types.yaml` + +Defines the work item types available in the project. Each key is the type name. Types control which workflow applies, whether the type can act as an epic (parent), its icon in the Plane UI, and which custom properties are attached. + +| Field | Type | Description | +| ----------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | +| `` | map | Top-level key is the type name as referenced in work items and workflows. | +| `description` | string | Human-readable description of the type, displayed in the Plane UI. | +| `workflow` | string | Name of the workflow from `schema/workflows.yaml` that governs state transitions for this type. | +| `is_epic` | boolean | When `true`, work items of this type can act as parents for other work items, enabling hierarchy. Defaults to `false`. | +| `logo_props.icon` | string | Name of the icon displayed for this type in the Plane UI. | +| `logo_props.background_color` | string | Hex colour string for the icon background (e.g. `#6366f1`). | +| `properties` | list | List of custom property definitions attached to this type. See property field reference below. | +| `properties[].name` | string | Property name as displayed in the Plane UI and as the key in work item `properties` maps. | +| `properties[].type` | string | Data type of the property. See property type table below. | +| `properties[].required` | boolean | When `true`, the property must have a value before a work item of this type can be marked as done. | +| `properties[].options` | list | List of option strings. Required when `type` is `option`. | +| `properties[].is_multi` | boolean | When `true` and `type` is `option`, the property accepts multiple selected values. Defaults to `false`. | + +**Property types:** + +| Type | Description | Notes | +| ---------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `text` | Single or multi-line text input. | Alias: `string`. | +| `number` | Integer numeric value. | | +| `decimal` | Floating-point numeric value. | | +| `date` | Calendar date in `YYYY-MM-DD` format. | | +| `datetime` | Date and time in ISO 8601 format. | | +| `option` | Dropdown selector. Single or multi-select depending on `is_multi`. | Alias: `enum`. Requires `options` list. | +| `boolean` | True/false checkbox. | | +| `url` | URL string with validation. | | +| `email` | Email address string with validation. | | +| `member_picker` | Reference to one or more Plane workspace members. | | +| `relation` | Reference to another work item or user. Set `relation_type: user` or `relation_type: issue`. | | +| `release_picker` | Reference to a Plane release tag. | Populated on pull; push is blocked by the Plane API. Read-only in practice. | +| `file` | File attachment reference. | | +| `formula` | Computed value derived from other fields. | Push not yet supported. | -If your local state gets corrupted, back it up and reset: +```yaml +work_item_types: + Story: + description: A unit of user-facing work + workflow: default + is_epic: false + logo_props: + icon: bookmark + background_color: "#6366f1" + properties: + - name: Severity + type: option + required: false + options: + - Minor + - Major + - Critical + is_multi: false + Bug: + description: A defect requiring correction + workflow: default + properties: + - name: Reproducible + type: boolean + required: true +``` -```bash -cp .plane/state.json .plane/state.json.backup -rm .plane/state.json -plane pull +--- + +#### `schema/states.yaml` + +Defines the states available in the project. Each key is the state name as referenced in work items and workflows. States are grouped into one of five standard Plane groups that determine how Plane treats them in reporting and cycle calculations. + +| Field | Type | Description | +| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `` | map | Top-level key is the state name, referenced in `work/workitems.yaml` and in `schema/workflows.yaml`. | +| `group` | string | Functional category of the state. One of: `backlog`, `unstarted`, `started`, `completed`, `cancelled`. Determines how Plane aggregates and reports on work items in this state. | +| `color` | string | Hex colour string used to represent this state in the Plane UI (e.g. `#22c55e`). | +| `allow_issue_creation` | boolean | When `true`, new work items can be created directly in this state. Defaults to `true`. | +| `is_default` | boolean | When `true`, this state is assigned to new work items that do not specify a state. Only one state per project should have `is_default: true`. | + +```yaml +states: + Backlog: + group: backlog + color: "#858585" + allow_issue_creation: true + is_default: true + Todo: + group: unstarted + color: "#d1d5db" + In Progress: + group: started + color: "#f59e0b" + Done: + group: completed + color: "#22c55e" + Cancelled: + group: cancelled + color: "#ef4444" +``` + +--- + +#### `schema/workflows.yaml` + +Defines the workflows available in the project. Each workflow associates a set of states with a set of work item types and optionally restricts which state transitions are permitted. When no transitions are defined, any state change is allowed. + +| Field | Type | Description | +| ------------------------------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `` | map | Top-level key is the workflow name, referenced from `schema/types.yaml` and `plane.yaml`. | +| `description` | string | Human-readable description of the workflow. | +| `is_active` | boolean | When `false`, the workflow is defined but not enforced by Plane. | +| `work_item_types` | list | Names of work item types from `schema/types.yaml` that this workflow governs. | +| `states` | list | Names of states from `schema/states.yaml` that are valid within this workflow. | +| `transitions` | map | Optional. Defines which state transitions are permitted. Keys are source state names; values are lists of allowed target transitions. When absent, all transitions between the workflow's states are permitted. | +| `transitions.[].to` | string | Name of the target state for this transition. Must be in the workflow's `states` list. | +| `transitions.[].type` | string | `transition` for a direct state change. `approval` for a change that requires approval before completing. | +| `transitions.[].required_approvals` | integer | For `approval` type only. Number of approvers required. `null` means all listed approvers must approve. | +| `transitions.[].approvers` | list | For `approval` type only. Email addresses of Plane members who can approve the transition. | + +```yaml +workflows: + default: + description: Standard engineering workflow + is_active: true + work_item_types: + - Story + - Bug + states: + - Backlog + - Todo + - In Progress + - Done + transitions: + Todo: + - to: In Progress + type: transition + In Progress: + - to: Done + type: approval + required_approvals: 1 + approvers: + - lead@example.com + - to: Todo + type: transition +``` + +--- + +#### `schema/labels.yaml` + +Defines the labels available in the project. Labels are flat - there are no groups at the schema level. Each entry in the list defines one label. + +| Field | Type | Description | +| ------- | ------ | -------------------------------------------------------------------------------------------- | +| `name` | string | Label name as referenced in work item `labels` lists. | +| `color` | string | Hex colour string used to render the label chip in the Plane UI. | +| `id` | string | Remote UUID of the label. Populated automatically after the first push. Do not set manually. | + +```yaml +labels: + - name: backend + color: "#3b82f6" + - name: frontend + color: "#8b5cf6" + - name: infrastructure + color: "#10b981" ``` -### Debug mode +--- -Enable debug logging to troubleshoot issues: +#### `schema/features.yaml` + +Controls which Plane features are enabled for the project. Disabling a feature hides it from the Plane UI and prevents Plane Compose from pushing or pulling data for that section. For example, setting `cycles: false` causes `plane push` to skip `work/cycles.yaml`. + +| Field | Type | Description | +| ----------------- | ------- | ---------------------------------------------------------------------------------------------------- | +| `cycles` | boolean | Enables time-boxed sprint cycles. When `false`, `work/cycles.yaml` is ignored on push and pull. | +| `modules` | boolean | Enables modules for grouping work by feature area. When `false`, `work/modules.yaml` is ignored. | +| `pages` | boolean | Enables wiki-style pages within the project. | +| `views` | boolean | Enables saved filtered views. | +| `intakes` | boolean | Enables a public intake form for submitting work items from outside the workspace. | +| `epics` | boolean | Enables work item hierarchy. Requires at least one type with `is_epic: true` in `schema/types.yaml`. | +| `work_item_types` | boolean | Enables custom work item types. When `false`, Plane uses only the default type. | +| `workflows` | boolean | Enables custom workflow enforcement. When `false`, state transitions are unrestricted. | +| `parallel_cycles` | boolean | Allows multiple active cycles to run simultaneously. | +| `project_updates` | boolean | Enables the project updates feed. | + +```yaml +features: + cycles: true + modules: true + pages: true + views: true + intakes: false + epics: true + work_item_types: true + workflows: true + parallel_cycles: false + project_updates: false +``` + +--- + +#### `work/workitems.yaml` + +Defines the work items to be synced to Plane. The file contains a single top-level `workitems` key whose value is a list. Each list entry represents one work item. + +| Field | Type | Required | Description | +| -------------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `id` | string | Recommended | Stable local identifier chosen by the author. Used as the tracking key in `.plane/state.json`. If omitted, Plane Compose derives the key from a hash of the item's content - title or description changes will then generate a new key and cause a duplicate to be created instead of an update. | +| `title` | string | Yes | Work item title as displayed in Plane. | +| `type` | string | No | Name of the work item type from `schema/types.yaml`. Defaults to `defaults.type` in `plane.yaml`. | +| `state` | string | No | Name of the state from `schema/states.yaml`. Defaults to the state with `is_default: true`. | +| `priority` | string | No | Priority level. One of: `urgent`, `high`, `medium`, `low`, `none`. Defaults to `none`. | +| `labels` | list | No | List of label names from `schema/labels.yaml`. | +| `assignees` | list | No | List of email addresses of Plane workspace members to assign to this work item. | +| `watchers` | list | No | List of email addresses of Plane workspace members who receive notifications for this work item. | +| `start_date` | string | No | Planned start date in `YYYY-MM-DD` format. | +| `due_date` | string | No | Planned due date in `YYYY-MM-DD` format. | +| `description` | string | No | Full description in Markdown format. | +| `parent` | string | No | Sequence ID of the parent work item (e.g. `API-5`). Requires `epics: true` in `schema/features.yaml` and a type with `is_epic: true`. | +| `blocked_by` | list | No | List of sequence IDs of work items that must be completed before this one can begin. | +| `blocking` | list | No | List of sequence IDs of work items that cannot begin until this one is completed. | +| `duplicate_of` | string | No | Sequence ID of the work item this one duplicates. | +| `relates_to` | list | No | List of sequence IDs of work items related to this one without a specific dependency relationship. | +| `properties` | map | No | Custom property values. Keys are property names as defined in the type's `properties` list in `schema/types.yaml`. Values must match the property type. | + +```yaml +workitems: + - id: "auth-oauth" + title: Implement OAuth2 login + type: Story + state: Backlog + priority: high + labels: + - backend + assignees: + - dev@example.com + watchers: + - pm@example.com + start_date: "2026-06-01" + due_date: "2026-06-15" + description: | + Add OAuth2 authentication using the provider SDK. + parent: "API-5" + blocked_by: + - "API-3" + blocking: + - "API-9" + properties: + Severity: Major +``` + +--- + +#### `work/cycles.yaml` + +Defines time-boxed sprint cycles. Each cycle is identified by its name and date range. The `status` field is computed by Plane based on dates relative to the current time and is read-only - setting it locally has no effect. The `id` field is populated automatically after the first push. + +| Field | Type | Description | +| ------------- | ------ | ---------------------------------------------------------------------------------- | +| `name` | string | Cycle name as displayed in Plane. Used as the tracking key in state. | +| `description` | string | Optional description of the cycle's goal or scope. | +| `start_date` | string | Cycle start date in `YYYY-MM-DD` format. | +| `end_date` | string | Cycle end date in `YYYY-MM-DD` format. | +| `id` | string | Remote UUID of the cycle. Populated automatically after push. Do not set manually. | + +```yaml +cycles: + - name: Sprint 1 + description: Foundation sprint + start_date: "2026-06-01" + end_date: "2026-06-14" + id: abc-123 +``` + +--- + +#### `work/modules.yaml` + +Defines modules that group work by feature or initiative. The `status` field is computed by Plane and is read-only. The `id` field is populated automatically after the first push. + +| Field | Type | Description | +| ------------- | ------ | ----------------------------------------------------------------------------------- | +| `name` | string | Module name as displayed in Plane. Used as the tracking key in state. | +| `description` | string | Optional description of the module's scope. | +| `start_date` | string | Module start date in `YYYY-MM-DD` format. | +| `end_date` | string | Module end date in `YYYY-MM-DD` format. | +| `id` | string | Remote UUID of the module. Populated automatically after push. Do not set manually. | + +```yaml +modules: + - name: Authentication + description: All auth-related work items + start_date: "2026-06-01" + end_date: "2026-07-01" + id: abc-123 +``` + +--- + +#### `work/milestones.yaml` + +Defines milestones that mark significant points in the project timeline. Milestones can reference work items by sequence ID. The `id` field is populated automatically after the first push. + +| Field | Type | Description | +| ------------- | ------ | -------------------------------------------------------------------------------------------------------------------- | +| `name` | string | Milestone name as displayed in Plane. Used as the tracking key in state. Maps to the `title` field in the Plane API. | +| `target_date` | string | Target completion date in `YYYY-MM-DD` format. | +| `work_items` | list | List of work item sequence IDs (e.g. `API-1`) to associate with this milestone. | +| `id` | string | Remote UUID of the milestone. Populated automatically after push. Do not set manually. | + +```yaml +milestones: + - name: v1.0 Release + target_date: "2026-08-01" + work_items: + - "API-1" + - "API-5" + id: abc-123 +``` + +--- + +### State file + +`.plane/state.json` is written and read exclusively by Plane Compose. Do not edit it manually. + +The file maps every pushed local item to its Plane remote UUID and stores a content hash at the time of last sync. On each push, Plane Compose recomputes the hash of each local item and compares it to the stored value. Items whose hash is unchanged are skipped; items with a different hash are pushed as updates. + +The file contains two categories of entries: + +- **Schema entries** - keyed by type, state, label, or workflow name; value is the remote UUID. +- **Work item entries** - keyed by the item's stable `id` (or a content-derived hash if `id` is absent); value contains the remote UUID, content hash, source file path, and timestamp of last sync. + +If the file is deleted or corrupted, run `plane schema import` to reconnect schema entries to their remote IDs, then `plane pull` to restore work item entries. + +`.plane/.state.lock` is a file lock held for the duration of any write operation to prevent concurrent state corruption. It is created and deleted automatically. If a process is killed mid-operation, a stale lock file may remain - delete it manually to unblock subsequent commands. + +--- + +### Environment variables + +Environment variables override the values stored in `~/.config/plane-compose/config.json` and `plane.yaml`. They take precedence over file-based configuration but are overridden by explicit CLI flags. + +| Variable | Default | Description | +| ----------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `PLANE_CONFIG_DIR` | `~/.config/plane-compose` | Path to the directory where `config.json` and `plane.log` are stored. Override to use a non-default location, for example in containerised environments. | +| `PLANE_SERVER_URL` | `https://api.plane.so` | Base URL of the Plane API. Set this to your instance URL when using a self-hosted deployment. | +| `PLANE_AUTH_TYPE` | - | Default authentication type (`pat` or `workspace`) used when `plane auth login` is called non-interactively. | +| `PLANE_AUTH_TOKEN` | - | API token value. When set, overrides the token stored in the matched connection. Useful for ephemeral CI environments where credentials should not be persisted to disk. | +| `PLANE_WORKSPACE` | - | Default workspace slug. Used when a command cannot determine the workspace from `plane.yaml` or an explicit flag. | +| `PLANE_CONNECTION` | - | Default connection ID. Applied when no connection can be resolved from the workspace mapping. | +| `PLANE_RATE_LIMIT_PER_MINUTE` | `50` | Maximum number of API requests issued per minute. Plane Compose uses a token bucket algorithm; this value sets the bucket refill rate. Reduce this value if your Plane instance enforces a lower rate limit. | +| `PLANE_API_TIMEOUT` | `30` | Timeout in seconds for individual API requests. Requests that exceed this limit are retried once before failing. | +| `PLANE_DEBUG` | `false` | When `true`, enables debug-level log output to `~/.config/plane-compose/plane.log`. Equivalent to passing `--debug` on every command. | +| `PLANE_VERBOSE` | `false` | When `true`, prints additional operational detail to stdout. Equivalent to passing `--verbose` on every command. | +| `PLANE_LOG_TO_FILE` | `false` | When `true`, writes all log output to `~/.config/plane-compose/plane.log` regardless of log level. | + +--- + +### Credentials and logs + +**Credentials** are stored in `~/.config/plane-compose/config.json`. The file contains a list of connection objects (each with a token, server URL, and auth type) and a list of workspace-to-connection associations. This file is created on first login and updated by `plane auth` commands. It is not created or modified by any other command. + +**Debug logs** are written to `~/.config/plane-compose/plane.log` when `--debug` is active or `PLANE_DEBUG=true`. The log file is appended to on each run and is not automatically rotated. To stream the log during a command: `tail -f ~/.config/plane-compose/plane.log`. + +--- + +### Troubleshooting + +#### Authentication failed (401) + +The stored token is invalid, expired, or has been revoked. Remove the connection and re-authenticate: ```bash -plane --debug push +plane auth logout +plane auth login ``` -View the logs: +#### Permission denied (403) + +The authenticated user does not have access to the requested workspace or project. Verify workspace membership with `plane auth list-connections`. Contact the workspace administrator to request access. + +#### Project not found (404) + +The `project.uuid` in `plane.yaml` does not correspond to an existing project. This occurs when the project has been deleted from Plane or when `plane.yaml` from one environment is used in another. Remove the `uuid` field from `plane.yaml` and run `plane schema push` to create a new project and update the UUID. + +#### Rate limit exceeded (429) + +Plane Compose has exceeded the API request limit for the current time window. Run `plane rate stats` to see how many requests remain and when the window resets. To reduce the request rate: `PLANE_RATE_LIMIT_PER_MINUTE=30 plane push`. + +#### Duplicate work items + +Work items were pushed without a stable `id` field. When the title or description was subsequently changed, the content hash changed, causing Plane Compose to treat the item as new rather than an update. A second item was created in Plane. To fix: add a stable `id` to the item in the YAML file, remove the old state entry with `plane state remove work_items.`, delete the duplicate from Plane manually, then push. + +#### State file out of sync or deleted + +If `.plane/state.json` is missing or its contents no longer match the remote, run: ```bash -tail -f ~/.config/plane-compose/plane.log +plane schema import # rebuilds schema entries from remote IDs +plane pull # rebuilds work item entries from remote data ``` + +#### Push fails partway through + +If a push is interrupted before completion, the next `plane push --resume` reads the failure log written during the interrupted run and retries only the items that did not succeed. Items that pushed successfully are not re-pushed. + +## Explanation + +### Why project as code + +Project management tools like Plane are rich and powerful, but they have a structural problem: the configuration of a project - its work item types, its workflows, its state definitions - lives exclusively inside the tool. There is no file you can open, no diff you can review, no commit history you can trace. When a workflow changes, you cannot see who changed it, when, or why. When a project template drifts across teams, you have no way to detect it. When you want to spin up a new project that mirrors an existing one, you configure it manually from memory. + +Plane Compose addresses this by treating the project as an artifact that lives in your repository. The schema and work items are YAML files. Changes go through pull requests. History is in Git. + +--- + +### The local-first model + +In a bidirectional sync tool, neither side is fully in control - changes can originate anywhere and the tool tries to merge them. This works for some use cases but creates ambiguity: if a state is renamed both locally and in the UI at the same time, which one wins? Who is responsible for the project structure? + +Plane Compose takes a deliberate position: local files are the source of truth. The Plane remote is the target. You declare what you want; Plane Compose makes it so. Remote changes do not flow back automatically - you pull them deliberately when you choose to accept them, review the diff, and commit. + +This asymmetry is a feature, not a limitation. It means the project schema has a single authoritative home: your repository. It means changes are proposed through pull requests, reviewed by teammates, and tracked in Git history. It means a new team member can understand the entire project structure by reading YAML files rather than navigating a UI. + +The tradeoff is that Plane Compose requires discipline. If your team routinely reconfigures projects through the Plane UI and rarely pulls those changes back into local files, the local files drift out of date and lose their value as the source of truth. The model works best when local files are treated as the real project definition and the UI is used for day-to-day work on individual items, not for structural changes. + +--- + +### State and identity + +When Plane Compose pushes a work item, Plane assigns it a UUID. On the next push, Plane Compose needs to know whether to create a new item or update the existing one. This is the problem that `.plane/state.json` solves: it records the mapping between your local identifier and the remote UUID, and it stores a content hash so unchanged items can be skipped. + +The `id` field on a work item is the anchor for this tracking. It is the stable key that survives title changes, description edits, and priority updates. When you set `id: "auth-oauth"` on an item, Plane Compose will find the same remote UUID in state next time regardless of what else you change. + +Without a stable `id`, Plane Compose falls back to a hash of the item's content. This works as long as nothing changes - but the moment you edit the title, the hash changes, the old state entry no longer matches, and Plane Compose treats the item as new. A second work item is created in Plane. This is why the `id` field exists and why it matters: not as a technical requirement, but as a declaration that this item has a persistent identity across edits. + +The same principle applies to schema items. A state named `In Progress` is tracked by that name. If you rename it in your YAML to `In Review`, Plane Compose sees a deletion and a creation - unless you run `plane schema import` first to re-anchor the name to its existing remote ID. The state file is the bridge between human-readable names and the UUIDs Plane uses internally. + +--- + +### The connection model + +The simplest possible authentication design would be a single API key stored somewhere on disk. Plane Compose uses a more structured model - connections - because a single key assumption breaks quickly in practice. + +Different workspaces may require different credentials. A developer might have access to a `myteam` workspace with a personal token and a `client-project` workspace under a separate account. A CI/CD system may use a workspace-scoped service token. A self-hosted Plane instance has a different server URL entirely. A single global API key cannot represent all of these at once. + +A connection bundles three things: a server URL, an auth type, and a token. Each connection gets an ID. Workspaces are then linked to connections, so that when a command reads `workspace: myteam` from `plane.yaml`, it knows which set of credentials to use without you specifying it each time. You can have as many connections as you need, and switching between workspaces is handled automatically. + +This design also separates identity from configuration. `plane.yaml` contains the workspace slug - a human-readable project identity - but not the credentials. The credentials live in `~/.config/plane-compose/config.json`, separate from the repository. You can commit `plane.yaml` to Git without leaking tokens. + +--- + +### Schema and work as separate concerns + +Plane Compose separates project content into two categories with different natures and different lifecycles. + +Schema files define _what is possible_: the types of work items that exist, the states they can move through, the labels available, the features enabled. Schema changes infrequently, is owned by leads or architects, and has consequences across the entire project. Adding a new work item type is a structural decision. + +Work files define _what is happening_: the actual items, sprints, modules, and milestones. Work changes constantly - every day, by everyone on the team. A work item is a fact about current activity, not a structural definition. + +This distinction shapes how you use Plane Compose. Schema is the part of the project you want to version-control rigorously, review carefully, and propagate from a template. Work is the part you might generate programmatically, import from a CSV, or simply let the team manage through the Plane UI. Some teams commit both to Git. Others commit only schema and treat work items as ephemeral data managed through Plane directly. Both are valid uses of the tool. + +The separation also clarifies the dependency direction: schema must exist before work can be pushed, because work items reference type names, state names, and label names that need to resolve to remote UUIDs. This is why `plane push` always applies schema before work, and why a failed schema push stops the work push from proceeding. + +--- + +### Upgrading vs. importing + +There are two commands that bring external schema definitions into a project - `plane upgrade` and `plane schema import` - and they look superficially similar. The difference is in what they treat as authoritative. + +`plane schema import` treats the _current Plane remote_ as authoritative. It answers the question: "What has changed in Plane since I last synced, and how do I get my local files to reflect it?" It is reactive. You use it when your local files have drifted behind the UI - someone added a state, renamed a label, or created a new work item type through the web interface. Import pulls those changes into your local files so you can commit them and stay in sync. + +`plane upgrade` treats a _template_ as authoritative. It answers a different question: "How do I bring this project in line with a standard that exists outside it?" It is proactive. The template is a canonical schema definition your team maintains - a shared standard for how projects should be structured. Upgrade merges that standard over your local schema, resolving conflicts in favour of the template while preserving anything that only exists locally. + +The mental model for import is: _Plane knows something my files don't._ The mental model for upgrade is: _the template knows something my project should adopt._ + +A project that was initialised from a template and kept up to date with `plane upgrade` is a project aligned with a team standard. A project that is kept up to date with `plane schema import` is a project that faithfully mirrors what is in Plane, regardless of any standard.