Skip to content

Plane Compose

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

To install from source:

bash
git clone https://github.com/makeplane/compose.git
cd compose
pipx install -e .

To upgrade to the latest version:

bash
pipx upgrade plane-compose

Authenticate

bash
plane auth login

You will be prompted for:

  • 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/<workspace-slug>/settings/account/api-tokens/
  • Workspace - your workspace slug (the URL segment after app.plane.so/)

Verify it worked:

bash
plane auth list-connections

Start a new project

bash
plane init my-project --workspace myteam

Then push the schema to create the project in Plane:

bash
cd my-project
plane schema push

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 --template default
# or a Git URL:
plane init my-project --workspace myteam \
  --template https://github.com/myorg/templates/engineering

Clone an existing project

Use this when the project already exists in Plane and you want to manage it locally.

bash
plane clone myteam/API

Or using the project UUID:

bash
plane clone abc-123-def-456 --workspace myteam

This downloads plane.yaml, all schema files, and all work files into a new local directory.


Push changes to Plane

From inside the project directory:

bash
plane push

Preview what will change before pushing:

bash
plane push --dry-run

If you have only changed schema files:

bash
plane push --schema-only

If you have only changed work items:

bash
plane push --work-only

Pull remote changes from Plane

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.

bash
plane pull

To keep local additions and apply remote changes without losing local-only items:

bash
plane pull --merge

To overwrite local files entirely with what is in Plane:

bash
plane pull --force

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 schema import

To add items that exist in Plane but are absent from your local files, without touching what you already have:

bash
plane schema import --merge

To replace your local schema files entirely with whatever is in Plane:

bash
plane schema import --force

Upgrade a project to a new template version

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

Apply the upgrade:

bash
plane upgrade --template https://github.com/myorg/templates/engineering

The template merges over your local schema. Items unique to your project are preserved; conflicts are resolved in favour of the template.


Run Plane Compose in CI/CD

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

Exit code values: 0 - no changes needed, 1 - error, 2 - changes were applied.


Manage workspace configuration

Clone workspace-level configuration (work item types, members, releases) to a local directory:

bash
plane ws clone myteam

After making changes locally, push them to Plane:

bash
plane ws push

To pull the latest workspace state from Plane:

bash
plane ws pull

To pull and preserve local additions:

bash
plane ws pull --merge

Work with multiple projects

From a directory containing multiple project subdirectories:

bash
plane push --all
plane pull --all
plane status --all

To limit to a specific workspace:

bash
plane push --all --workspace myteam

To filter by project name:

bash
plane push --all --filter "api-*"

Recover from sync problems

State file deleted or corrupted:

bash
plane schema import   # reconnects schema names to remote IDs
plane pull            # restores work item entries

A specific item is stuck or needs to be re-created:

Remove its state entry so the next push treats it as new:

bash
plane state remove types.Story         # for a schema item
plane state remove work_items.AUTH-1   # for a work item

All work items need to be re-pushed:

bash
plane state clear-items
plane push

A push was interrupted and some items failed:

bash
plane push --resume

Diagnosing what went wrong:

bash
plane --debug push
tail -f ~/.config/plane-compose/plane.log

Reference

CLI commands

plane init

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.

plane init [PROJECT] [--workspace WS] [--connection CONN]
           [--path PATH] [--template TEMPLATE]
OptionDescription
PROJECTName of the directory to create. Also used as the project key if --project is not set separately.
--workspace WSSlug of the Plane workspace this project belongs to. Written into plane.yaml.
--connection CONNID of the connection to use for API calls during initialisation. Defaults to the connection linked to the workspace.
--path PATHParent directory in which to create the project folder. Defaults to the current working directory.
--template TEMPLATESource 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.

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.

plane auth login [--server-url URL] [--auth-type TYPE]
                 [--token TOKEN] [--workspace WS] [--connection CONN]
OptionDescription
--server-url URLBase 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 TYPEToken type. pat for a Personal Access Token scoped to a user. workspace for a workspace-scoped token.
--token TOKENThe API token value.
--workspace WSWorkspace slug to associate with this connection. The association is stored so that commands can resolve credentials from plane.yaml automatically.
--connection CONNOptional explicit ID to assign to this connection. If omitted, an ID is generated automatically.

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.

plane auth logout CONNECTION_ID [--force]
OptionDescription
CONNECTION_IDID of the connection to remove, as shown by plane auth list-connections.
--forceSkips 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.

plane auth list-connections

plane auth connect-workspace

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.

plane auth connect-workspace WORKSPACE_SLUG --connection CONN_ID
OptionDescription
WORKSPACE_SLUGThe Plane workspace slug to link.
--connection CONN_IDID of the connection to link it to. Required.

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]
OptionDescription
WORKSPACE_SLUGThe workspace slug to unlink.
--connection CONN_IDID of the connection to unlink from. If omitted and the workspace has only one associated connection, that connection is unlinked automatically.

plane schema validate

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.

plane schema push

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--dry-runComputes and displays the full change plan without making any API calls or modifying any files.
--forceSkips the interactive confirmation prompt before applying changes.

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.

plane schema import [PROJECT] [--path PATH] [--merge] [--force]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--mergeWrites remote schema items that are absent from local files into the appropriate YAML files. Existing local items are not modified. Additive only.
--forceReplaces the contents of local schema files entirely with what is returned from the Plane API. Any local-only items are lost.

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--connection CONNOverrides the connection resolved from plane.yaml for this invocation only.
--dry-runComputes and displays the full change plan without making any API calls or writing to state.
--forceSkips the interactive confirmation prompt before applying changes.
--schema-onlyPushes only schema files. Equivalent to running plane schema push. Skips workitems, cycles, modules, and milestones.
--work-onlySkips the schema push phase and pushes only work files. Requires schema to already be in sync.
--skip SECTIONExcludes a specific section from the push. Repeatable. Valid values: workitems, cycles, modules, milestones.
--allDiscovers all project directories under the current directory and pushes each one.
--workspace WSWhen used with --all, restricts discovery to projects belonging to this workspace.
--filter PATTERNWhen used with --all, applies a glob pattern to filter project directory names.
--no-conflict-checkSkips the pre-push API call that detects remote conflicts. Reduces API usage. Recommended for CI/CD pipelines where conflicts are not expected.
--exit-codeReturns 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.
--resumeReads 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.

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--connection CONNOverrides the connection resolved from plane.yaml for this invocation only.
--mergeApplies remote changes to local files while preserving items that exist locally but not remotely. Local-only items are not deleted.
--forceOverwrites local files with remote content without prompting. Local-only items are lost.
--schema-onlyPulls only schema files. Skips workitems, cycles, modules, and milestones.
--work-onlyPulls only work files. Skips schema.
--skip SECTIONExcludes a specific section from the pull. Repeatable. Valid values: workitems, cycles, modules, milestones.
--with-propertiesIncludes custom property values in the pulled work items. Enabled by default.
--no-propertiesExcludes custom property values from pulled work items. The properties map is omitted from each work item in the output file.
--allDiscovers all project directories under the current directory and pulls each one.
--workspace WSWhen used with --all, restricts discovery to projects belonging to this workspace.
--filter PATTERNWhen used with --all, applies a glob pattern to filter project directory names.

plane clone

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.

plane clone PROJECT [--directory DIR] [--path PATH]
            [--workspace WS] [--connection CONN]
            [--schema-only] [--skip SECTION] [--with-properties]
OptionDescription
PROJECTProject identifier. Accepts three formats: workspace/KEY shorthand (e.g. myteam/API), a project key with --workspace flag, or a UUID with --workspace flag.
--directory DIRName of the local directory to create. Defaults to the project key.
--path PATHParent directory in which to create the project folder. Defaults to the current working directory.
--workspace WSWorkspace slug. Required when PROJECT is a key or UUID rather than a shorthand.
--connection CONNOverrides the connection resolved from the workspace for this invocation only.
--schema-onlyDownloads schema files only. Skips workitems, cycles, modules, and milestones.
--skip SECTIONExcludes a specific section from the clone. Repeatable. Valid values: workitems, cycles, modules, milestones.
--with-propertiesIncludes custom property values in the cloned work items. Enabled by default.

plane diff

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.

plane diff [PROJECT] [--path PATH] [--connection CONN]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--connection CONNOverrides the connection resolved from plane.yaml for this invocation only.

Output categories:

CategoryMeaning
new_localItem exists in local files but has no corresponding remote record.
modified_localItem exists both locally and remotely, and the local version differs from the remote.
modified_remoteItem exists both locally and remotely, and the remote version differs from what was last synced.
conflictsItem has been modified both locally and remotely since the last sync.
in_syncLocal and remote versions are identical.
deleted_remoteItem has been deleted from Plane but still exists in local files.

plane validate

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.

plane validate [PATH] [--offline] [--json]
OptionDescription
PATHFilesystem path to the project root. Defaults to the current directory.
--offlineSkips the API call to fetch the remote schema. Validates only against local schema files.
--jsonOutputs validation errors as a JSON array instead of formatted text. Useful for scripting and CI pipelines.

plane status

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.

plane status [PATH] [--all] [--workspace WS] [--filter PATTERN] [--json]
OptionDescription
PATHFilesystem path to the project root. Defaults to the current directory.
--allDiscovers all project directories under the current directory and reports status for each.
--workspace WSWhen used with --all, restricts discovery to projects belonging to this workspace.
--filter PATTERNWhen used with --all, applies a glob pattern to filter project directory names.
--jsonOutputs the status report as JSON.

plane upgrade

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.

plane upgrade [PROJECT] --template TEMPLATE [--path PATH]
              [--include-data] [--dry-run] [--force]
              [--skip-pull] [--schema-only]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--template TEMPLATETemplate source. Required. Accepts a built-in name, a local path, a Git HTTPS URL, or a Git SSH URL.
--path PATHExplicit filesystem path to the project root.
--include-dataAlso copies cycles, modules, and work items from the template into the local work files.
--dry-runComputes and displays the upgrade plan without modifying any files or making any API calls.
--forceSkips the interactive confirmation prompt before applying the upgrade.
--skip-pullSkips pulling the latest schema from Plane before computing the merge. Uses the current local schema as the base.
--schema-onlyApplies only schema changes from the template. Does not copy cycles, modules, or work items even if --include-data is set.

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--jsonOutputs the state as raw JSON.

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--forceSkips the confirmation prompt.

plane state clear-items

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]
OptionDescription
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit filesystem path to the project root.
--forceSkips the confirmation prompt.

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]
OptionDescription
PATH_STRDot-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.
PROJECTProject key or directory. Defaults to the current directory.
--path PATHExplicit 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]
OptionDescription
WORKSPACEWorkspace slug to clone.
--directory DIRName of the local directory to create. Defaults to the workspace slug.
--path PATHParent directory for the workspace folder. Defaults to the current working directory.
--connection CONNOverrides the connection resolved from the workspace slug for this invocation only.
--forceOverwrites an existing local directory without prompting.
--skip SECTIONExcludes 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]
OptionDescription
WORKSPACEWorkspace slug. Defaults to the value in workspace.yaml.
--path PATHExplicit filesystem path to the workspace root.
--mergePreserves local-only items while applying remote changes.
--forceOverwrites local files with remote content without prompting. Local-only items are lost.
--skip SECTIONExcludes 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]
OptionDescription
WORKSPACEWorkspace slug. Defaults to the value in workspace.yaml.
--path PATHExplicit filesystem path to the workspace root.
--dry-runComputes and displays the change plan without making any API calls.
--forceSkips 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]
OptionDescription
WORKSPACEWorkspace slug. Defaults to the value in workspace.yaml.
--path PATHExplicit 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]
OptionDescription
WORKSPACEWorkspace slug. Defaults to the value in workspace.yaml.
--path PATHExplicit filesystem path to the workspace root.
--template TEMPLATETemplate source. Accepts a built-in name, local path, Git HTTPS URL, or Git SSH URL.
--dry-runDisplays the upgrade plan without applying it.
--forceSkips the confirmation prompt.
--skip-pullSkips pulling the latest workspace schema from Plane before computing the merge.
--schema-onlyApplies 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

plane rate reset

Resets the local rate limit counters to zero. Does not affect Plane's server-side rate limiting.

plane rate reset

Global options

These options are accepted by every plane command.

OptionDescription
--version, -VPrints the installed version of Plane Compose and exits.
--verbose, -vEnables verbose output. Prints additional detail about each operation as it runs.
--debugEnables 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.

<project>/
├── 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>/
├── 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.

FieldTypeDescription
typestringAlways project. Identifies this directory as a project (as opposed to a workspace).
workspacestringSlug of the Plane workspace this project belongs to. Used to resolve the correct connection from ~/.config/plane-compose/config.json.
connectionstringOptional. ID of a specific connection to use for all operations on this project. Overrides the workspace-to-connection mapping.
project.keystringShort project identifier, maximum 10 uppercase characters. Used as the prefix in work item sequence IDs (e.g. API-42).
project.namestringHuman-readable project name as displayed in Plane.
project.uuidstringRemote UUID of the project. Populated automatically after the first plane schema push.
project.descriptionstringOptional. Project description as displayed in Plane.
project.networkstringVisibility setting. public makes the project visible to all workspace members. private restricts visibility. Defaults to public.
project.timezonestringIANA timezone string (e.g. UTC, America/New_York). Affects due date display and cycle date calculations in Plane.
defaults.typestringDefault 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.workflowstringDefault workflow applied when a work item type does not specify one. Must match a key in schema/workflows.yaml.
templatestringSource 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
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

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.

FieldTypeDescription
<TypeName>mapTop-level key is the type name as referenced in work items and workflows.
descriptionstringHuman-readable description of the type, displayed in the Plane UI.
workflowstringName of the workflow from schema/workflows.yaml that governs state transitions for this type.
is_epicbooleanWhen true, work items of this type can act as parents for other work items, enabling hierarchy. Defaults to false.
logo_props.iconstringName of the icon displayed for this type in the Plane UI.
logo_props.background_colorstringHex colour string for the icon background (e.g. #6366f1).
propertieslistList of custom property definitions attached to this type. See property field reference below.
properties[].namestringProperty name as displayed in the Plane UI and as the key in work item properties maps.
properties[].typestringData type of the property. See property type table below.
properties[].requiredbooleanWhen true, the property must have a value before a work item of this type can be marked as done.
properties[].optionslistList of option strings. Required when type is option.
properties[].is_multibooleanWhen true and type is option, the property accepts multiple selected values. Defaults to false.

Property types:

TypeDescriptionNotes
textSingle or multi-line text input.Alias: string.
numberInteger numeric value.
decimalFloating-point numeric value.
dateCalendar date in YYYY-MM-DD format.
datetimeDate and time in ISO 8601 format.
optionDropdown selector. Single or multi-select depending on is_multi.Alias: enum. Requires options list.
booleanTrue/false checkbox.
urlURL string with validation.
emailEmail address string with validation.
member_pickerReference to one or more Plane workspace members.
relationReference to another work item or user. Set relation_type: user or relation_type: issue.
release_pickerReference to a Plane release tag.Populated on pull; push is blocked by the Plane API. Read-only in practice.
fileFile attachment reference.
formulaComputed value derived from other fields.Push not yet supported.
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

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.

FieldTypeDescription
<StateName>mapTop-level key is the state name, referenced in work/workitems.yaml and in schema/workflows.yaml.
groupstringFunctional category of the state. One of: backlog, unstarted, started, completed, cancelled. Determines how Plane aggregates and reports on work items in this state.
colorstringHex colour string used to represent this state in the Plane UI (e.g. #22c55e).
allow_issue_creationbooleanWhen true, new work items can be created directly in this state. Defaults to true.
is_defaultbooleanWhen 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.

FieldTypeDescription
<WorkflowName>mapTop-level key is the workflow name, referenced from schema/types.yaml and plane.yaml.
descriptionstringHuman-readable description of the workflow.
is_activebooleanWhen false, the workflow is defined but not enforced by Plane.
work_item_typeslistNames of work item types from schema/types.yaml that this workflow governs.
stateslistNames of states from schema/states.yaml that are valid within this workflow.
transitionsmapOptional. 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.<State>[].tostringName of the target state for this transition. Must be in the workflow's states list.
transitions.<State>[].typestringtransition for a direct state change. approval for a change that requires approval before completing.
transitions.<State>[].required_approvalsintegerFor approval type only. Number of approvers required. null means all listed approvers must approve.
transitions.<State>[].approverslistFor 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.

FieldTypeDescription
namestringLabel name as referenced in work item labels lists.
colorstringHex colour string used to render the label chip in the Plane UI.
idstringRemote 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"

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.

FieldTypeDescription
cyclesbooleanEnables time-boxed sprint cycles. When false, work/cycles.yaml is ignored on push and pull.
modulesbooleanEnables modules for grouping work by feature area. When false, work/modules.yaml is ignored.
pagesbooleanEnables wiki-style pages within the project.
viewsbooleanEnables saved filtered views.
intakesbooleanEnables a public intake form for submitting work items from outside the workspace.
epicsbooleanEnables work item hierarchy. Requires at least one type with is_epic: true in schema/types.yaml.
work_item_typesbooleanEnables custom work item types. When false, Plane uses only the default type.
workflowsbooleanEnables custom workflow enforcement. When false, state transitions are unrestricted.
parallel_cyclesbooleanAllows multiple active cycles to run simultaneously.
project_updatesbooleanEnables 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.

FieldTypeRequiredDescription
idstringRecommendedStable 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.
titlestringYesWork item title as displayed in Plane.
typestringNoName of the work item type from schema/types.yaml. Defaults to defaults.type in plane.yaml.
statestringNoName of the state from schema/states.yaml. Defaults to the state with is_default: true.
prioritystringNoPriority level. One of: urgent, high, medium, low, none. Defaults to none.
labelslistNoList of label names from schema/labels.yaml.
assigneeslistNoList of email addresses of Plane workspace members to assign to this work item.
watcherslistNoList of email addresses of Plane workspace members who receive notifications for this work item.
start_datestringNoPlanned start date in YYYY-MM-DD format.
due_datestringNoPlanned due date in YYYY-MM-DD format.
descriptionstringNoFull description in Markdown format.
parentstringNoSequence 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_bylistNoList of sequence IDs of work items that must be completed before this one can begin.
blockinglistNoList of sequence IDs of work items that cannot begin until this one is completed.
duplicate_ofstringNoSequence ID of the work item this one duplicates.
relates_tolistNoList of sequence IDs of work items related to this one without a specific dependency relationship.
propertiesmapNoCustom 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.

FieldTypeDescription
namestringCycle name as displayed in Plane. Used as the tracking key in state.
descriptionstringOptional description of the cycle's goal or scope.
start_datestringCycle start date in YYYY-MM-DD format.
end_datestringCycle end date in YYYY-MM-DD format.
idstringRemote 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.

FieldTypeDescription
namestringModule name as displayed in Plane. Used as the tracking key in state.
descriptionstringOptional description of the module's scope.
start_datestringModule start date in YYYY-MM-DD format.
end_datestringModule end date in YYYY-MM-DD format.
idstringRemote 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.

FieldTypeDescription
namestringMilestone name as displayed in Plane. Used as the tracking key in state. Maps to the title field in the Plane API.
target_datestringTarget completion date in YYYY-MM-DD format.
work_itemslistList of work item sequence IDs (e.g. API-1) to associate with this milestone.
idstringRemote 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.

VariableDefaultDescription
PLANE_CONFIG_DIR~/.config/plane-composePath 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_URLhttps://api.plane.soBase 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_MINUTE50Maximum 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_TIMEOUT30Timeout in seconds for individual API requests. Requests that exceed this limit are retried once before failing.
PLANE_DEBUGfalseWhen true, enables debug-level log output to ~/.config/plane-compose/plane.log. Equivalent to passing --debug on every command.
PLANE_VERBOSEfalseWhen true, prints additional operational detail to stdout. Equivalent to passing --verbose on every command.
PLANE_LOG_TO_FILEfalseWhen 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 auth logout <connection-id>
plane auth login

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.<key>, 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
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.