Machine-friendly TypeScript CLI for Plane.so, designed for LLM agents and automation scripts.
The CLI binary is plane-cli.
This project is not affiliated with Plane.
Note
This project is built for LLM agents and was developed with substantial LLM assistance. Its primary interface is stable JSON for automation, not interactive human CLI ergonomics.
bun install
bun run buildRun locally after building:
./dist/plane-cli --help
./dist/plane-cli project list --jsonDuring development:
bun src/index.ts project list --jsonPlane's public API is REST. Plane Cloud uses:
https://api.plane.so/
Self-hosted Plane instances can use a custom baseUrl per workspace. The CLI
authenticates with X-API-Key for personal access tokens or OAuth bearer tokens
from a Plane app. Secrets are kept out of JSON output.
The CLI searches these config files, in order:
.plane-cli.yaml
.plane-cli.json
~/.config/plane-cli/config.yaml
~/.config/plane-cli/config.json
YAML example:
defaultWorkspace: personal
workspaces:
personal:
workspaceSlug: my-team
apiKey: plane_api_xxx
selfhosted:
workspaceSlug: engineering
baseUrl: https://plane.example.com
apiKey: plane_api_yyy
enterprise-oauth:
workspaceSlug: engineering
baseUrl: https://plane.example.com
auth:
type: oauth
flow: client_credentials
clientId: plane_oauth_client_id
clientSecret: plane_oauth_client_secret
appInstallationId: plane_app_installation_id
accessToken: plane_oauth_access_tokenNamespaced environment variables may override config API keys:
export PLANE_API_KEY_PERSONAL="plane_api_xxx"
export PLANE_WORKSPACE="personal"Workspace names are uppercased and non-alphanumeric characters become
underscores. For example, engineering-prod maps to
PLANE_API_KEY_ENGINEERING_PROD.
To bind a repository checkout to a configured Plane workspace, add a dotfile at the repo root:
printf "personal\n" > .plane-cli-workspaceTo also bind issue commands to a default Plane project, use YAML:
workspace: personal
project: WebThe project hint is used as the default --project for project-scoped commands.
An explicit --project flag always wins.
Add or update a workspace API key:
plane-cli auth api-key \
--workspace personal \
--api-key "$PLANE_API_KEY" \
--default \
--jsonFor self-hosted Plane:
plane-cli auth api-key \
--workspace selfhosted \
--base-url https://plane.example.com \
--api-key "$PLANE_API_KEY" \
--jsonFor self-hosted Plane Enterprise, create a Plane app in the workspace
integrations UI and use the instance URL as --base-url.
For LLM agents and automation, use the bot/app-installation flow:
plane-cli auth oauth bot \
--workspace zoo \
--base-url https://plane.thezoo.house \
--client-id "$PLANE_OAUTH_CLIENT_ID" \
--client-secret "$PLANE_OAUTH_CLIENT_SECRET" \
--app-installation-id "$PLANE_APP_INSTALLATION_ID" \
--default \
--jsonFor a Linear-style browser login as your user, register this redirect URI in the Plane app first:
http://127.0.0.1:8717/callback
Then run:
plane-cli auth oauth login \
--workspace zoo \
--base-url https://plane.thezoo.house \
--client-id "$PLANE_OAUTH_CLIENT_ID" \
--client-secret "$PLANE_OAUTH_CLIENT_SECRET" \
--default \
--jsonIf you need a different callback URL, pass --redirect-uri and make sure the
same URL is configured in the Plane app. The CLI discovers the Plane
workspaceSlug during auth setup and stores it in config. --workspace-slug
exists only as an override for unusual server responses.
Successful JSON output:
{
"ok": true,
"workspace": "personal",
"data": {}
}Error JSON output:
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Missing required flag --project",
"details": {}
}
}auth api-key --workspace name --api-key plane_api_... [--base-url url] [--workspace-slug slug] [--default]
auth oauth bot --workspace name --base-url url --client-id id --client-secret secret --app-installation-id id [--workspace-slug slug] [--default]
auth oauth login --workspace name --base-url url --client-id id --client-secret secret [--redirect-port 8717] [--redirect-uri uri] [--scope scope] [--workspace-slug slug] [--default]
auth oauth code --workspace name --base-url url --client-id id --client-secret secret --redirect-uri uri --code code [--workspace-slug slug] [--default]
auth oauth url --base-url url --client-id id --redirect-uri uri [--scope scope] [--state state]
config show
config path
workspace current [--workspace name]
workspace validate [--workspace name]
user me
member list
project list [--fields fields] [--per-page 100] [--order-by field]
project get <project>
project create --name name [--identifier KEY] [--description text]
project update <project> [--name name] [--description text] [--identifier KEY]
project archive <project> --confirm
project unarchive <project>
project delete <project> --confirm
issue list --project project [--state id] [--assignee id] [--label id] [--fields fields] [--per-page 100]
issue search --query text [--project project] [--limit 10] [--workspace-search]
issue advanced-search [--query text] [--project project] [--filters-json json | --filters-file file] [--limit 10] [--workspace-search]
issue get <KEY-123 | issue-id> [--project project]
issue create --title title [--project project] [--description markdown] [--priority urgent|high|medium|low|none] [--state state-id] [--assignee user-id] [--label label-id] [--parent issue-id]
issue update <issue> --project project [--title title] [--description markdown] [--priority priority] [--state state-id] [--assignee user-id] [--label label-id]
issue delete <issue> --project project --confirm
issue type-schema --project project [--include members,labels] [--type-id type-id]
issue type-list --project project
issue link list <issue> --project project
issue link get <link-id> <issue> --project project
issue link create <issue> --project project --url url [--title title]
issue link update <link-id> <issue> --project project [--url url] [--title title]
issue link delete <link-id> <issue> --project project --confirm
issue attachment list <issue> --project project
issue attachment get <attachment-id> <issue> --project project
issue attachment request-upload <issue> --project project --name filename --size bytes [--type mime]
issue attachment complete <attachment-id> <issue> --project project
issue attachment upload <issue> --project project --file path [--name filename] [--type mime]
issue attachment update <attachment-id> <issue> --project project [--name filename] [--type mime]
issue attachment delete <attachment-id> <issue> --project project --confirm
state|label|module|cycle|page list --project project
state|label|module|cycle|page create --project project --name name [resource flags]
state|label|module|cycle|page update <id> --project project [resource flags]
state|label|module|cycle|page delete <id> --project project --confirm
cycle|module add-item <container-id> <issue-id> --project project
cycle|module remove-item <container-id> <issue-id> --project project
cycle|module items <container-id> --project project
comment list <issue> --project project
comment create <issue> --project project --body markdown
comment update <comment-id> <issue> --project project --body markdown
comment delete <comment-id> <issue> --project project --confirm
Aliases include issue, issues, wi, work-item; project, projects;
state, states; label, labels; module, modules; cycle, cycles;
and list/ls.
bun run test
bun run build
bun run lint
npm pack --dry-run