--- url: 'https://developers.plane.so/self-hosting/overview.html' description: >- Deploy Plane on your own infrastructure with Docker, Kubernetes, or Podman. Complete self-hosting guides for open-source project management with full control and customization. --- # Deploy Plane on your infrastructure Take complete control of your project management infrastructure by deploying Plane on your own servers. Self-hosting Plane gives you full ownership of your data and the flexibility to deploy wherever you need - from a single Docker container to enterprise Kubernetes clusters. ## Why self-host Plane? **Data sovereignty and privacy**\ Keep all your project data within your own infrastructure. Perfect for organizations with strict data residency requirements or privacy regulations. **Complete control**\ Customize every aspect of Plane to match your workflows. Control when and how updates are applied, and integrate with your existing tools and infrastructure. **Compliance and security**\ Meet regulatory requirements like GDPR, HIPAA, SOC 2, or industry-specific standards by maintaining full control over data storage and access. **No vendor lock-in**\ Your data remains accessible in open formats. Migrate, backup, or customize without restrictions. ## Deployment methods Choose the deployment method that best fits your infrastructure and team size: [Other deployment methods](/self-hosting/methods/overview) ## Configuration and governance Once deployed, configure your Plane instance to match your organization's needs: --- --- url: 'https://developers.plane.so/self-hosting/self-hosting-101.html' description: >- What self-hosting Plane involves, how it's licensed, what your team operates, and how to plan a deployment. --- # Self-hosting 101 Self-hosting Plane means running the full application stack on infrastructure you control. This page covers what that involves, how Plane is licensed, what your team operates, and how to plan a deployment. If you're ready to deploy, jump to the [deployment scenarios on the overview](/self-hosting/overview). *** ## 1. What self-hosting Plane means Self-hosting Plane means deploying Plane, the project and knowledge management platform, on infrastructure you control. Plane ships as a bundled Docker or Kubernetes deployment that includes the application and everything it depends on, so you can stand it up with a single command on a single machine. Everything user-facing runs inside your network: the web app, the API, file uploads, real-time collaboration, search indexing, and AI inference paths. You control the data, the network path users take to reach Plane, the upgrade timing, the auth provider, and the integrations Plane talks to internally. For the [Commercial Edition](/self-hosting/editions-and-versions#commercial), Plane operates the license validation server at the [Prime portal](https://prime.plane.so/licenses). The [Airgapped Edition](/self-hosting/editions-and-versions#airgapped) removes that dependency and runs entirely offline. For production deployments, you can also point Plane at managed services for the database and storage layers (RDS, Cloud SQL, S3, GCS, and similar) instead of running them yourself. See [Plane Architecture](/self-hosting/plane-architecture) for the full system anatomy and [External services](/self-hosting/govern/database-and-storage) for the managed-service options. *** ## 2. Cloud vs self-hosted | Dimension | [Plane Cloud](https://app.plane.so/sign-in) | Self-hosted | | --------------------------------- | ------------------------------------------- | --------------------------------------------- | | Time to first user | About 30 seconds | 20mins via Docker | | Data residency | US (default), EU available on demand | Anywhere you deploy | | Network isolation | Public internet | VPC, private network, or fully air-gapped | | Upgrade timing | Continuous | You choose your window | | Feature freshness | New features ship here first | Commercial gets them next, Community last | | Operational overhead | Zero | Real and ongoing | | Backups and DR | Plane operates | You design and operate | | Support escalation | One vendor | Your platform team first, then Plane support | | Compliance posture | Plane's certifications apply | Your perimeter, your audit | | Integration with internal systems | Egress only (public APIs) | Direct access to private services | | Cost model | Per-seat subscription | Per-seat subscription + On demand | | Audit logs, SCIM, advanced auth | Business Plus | Available on plans (Commercial and Airgapped) | [Cloud and self-hosted migration](/self-hosting/manage/migrate-plane) is supported in the Cloud-to-self-hosted direction. *** ## 3. How licensing works Plane's licensing has two layers, and confusing them is the most common mistake new self-hosters make. **The edition is the codebase you run.** There are three self-hosted editions: [Community](/self-hosting/editions-and-versions#community), [Commercial](/self-hosting/editions-and-versions#commercial), and [Airgapped](/self-hosting/editions-and-versions#airgapped). Each has its own release cycle. They are separate codebases, not feature toggles on the same binary. **The plan is the set of features your license key unlocks** on the Commercial and Airgapped editions. Plans are Free, Pro, Business, and Enterprise Grid. You activate a plan by pasting a license key from the [Prime portal](https://prime.plane.so/licenses) into your workspace settings. ### The three editions **Community Edition** is open source under [AGPL v3.0](https://github.com/makeplane/plane/blob/preview/LICENSE.txt). Free, no license key, full source available, you can audit and modify it. Feature parity with the Free tier of Cloud, with no Pro, Business, or Enterprise features. To unlock paid features, switch to Commercial. See [Upgrade Community to Commercial](/self-hosting/upgrade-from-community). **Commercial Edition** is closed-source. It includes a built-in Free tier of 12 user seats per workspace, which means you can run Commercial in production at small scale without buying a license. To unlock Pro, Business, or Enterprise Grid features, you activate a license key. Commercial gets full feature parity with Cloud. **Airgapped Edition** is the Commercial Edition adapted for environments without internet access. Same features, offline license activation, updates pulled from your own Docker registry. See [Airgapped requirements](/self-hosting/methods/airgapped-requirements). ### How license activation works License keys come from the [Prime portal](https://prime.plane.so/licenses), which you log into with the email you used to purchase. To activate on a self-hosted instance: 1. Copy the license key from Prime. 2. In your Plane workspace, go to **Workspace Settings > Billing and plans**. 3. Click **Activate this Workspace**, paste the key, click **Activate**. Each license key is bound to one workspace and one machine. To move it (different server, different workspace, reinstall), use **Delink license key** in Billing and plans, then run `prime-cli restart`, then activate elsewhere. The full procedure is at [Activate Pro and Business](/self-hosting/manage/manage-licenses/activate-pro-and-business). The **Sync plan** button in Billing and plans pulls the latest subscription state from Prime, including plan type, seat count, expiration, and feature flags. Use it whenever Prime and your workspace disagree. For Enterprise Grid, see [Activate Enterprise](/self-hosting/manage/manage-licenses/activate-enterprise). For airgapped activation, see [Activate airgapped](/self-hosting/manage/manage-licenses/activate-airgapped) and [Activate airgapped Enterprise](/self-hosting/manage/manage-licenses/activate-airgapped-enterprise). ### When something goes wrong License-state failures (expiry, seat overflow, key conflicts, network errors during activation) surface as specific errors. The full reference is at [License errors](/self-hosting/troubleshoot/license-errors). Point your operations runbook there, since these are usually fast fixes once you know the symptom. ### AGPL in practice If you're running the Community Edition, AGPL v3.0 has a few real-world boundaries: * **Running Community for your internal team.** Fine, no obligations beyond AGPL terms. * **Modifying Community for internal use.** Fine, AGPL specifically allows this. * **Hosting Community as a service for external customers.** AGPL requires you to publish your modifications, including any changes you've made. * **Embedding Community inside a commercial product.** AGPL-affecting territory. Either comply with AGPL's source-disclosure requirements or [talk to sales](https://plane.so/talk-to-sales) about a commercial license. Most teams self-hosting Plane for internal project management never hit these boundaries. If you're not sure where you sit, ask your legal team or contact us before building anything customer-facing on top of Community. *** ## 4. When self-hosted fits Self-hosting Plane fits the following scenarios. **1. Regulatory or contractual data residency.** GDPR with strict country-level residency, sector rules (HIPAA, financial regulations), or customer contracts that specify where data sits. Self-hosting puts data on infrastructure you can point an auditor at. **2. Air-gapped or sovereign cloud.** Your network has no outbound internet, or you operate in a sovereign cloud where third-party SaaS isn't permitted. The [Airgapped Edition](/self-hosting/methods/airgapped-requirements) is built for this. **3. Internal-only integrations.** You need Plane to integrate with services that aren't on the public internet: internal Git, internal ticketing, internal SSO, internal monitoring. Self-hosted lives on the same network as the things it talks to. **4. Upgrade timing control.** Multi-week change windows, frozen periods around financial reporting, change-advisory-board approvals. Self-hosted lets you upgrade on your calendar. See [Upgrade Plane](/self-hosting/manage/upgrade-plane). *** ## 5. What you're running Plane is a multi-service application: eight application services plus a data layer. ### Application services * **Web.** The main user-facing Next.js app at your primary domain. * **Space.** Public-facing project pages (deployed views, intake forms, and similar). * **Admin.** The admin console for instance-level configuration. * **Live.** Real-time collaboration backend (Yjs WebSocket server) for pages and work items. * **API.** The Django REST API that everything else calls. * **Worker.** Celery background workers for async jobs (notifications, webhooks, exports, AI). * **Beat.** Celery scheduler that triggers periodic tasks. * **Migrator.** One-shot DB migration job that runs on each upgrade. ### Data layer * **Postgres.** Primary database. Holds workspaces, projects, work items, pages, users, and everything transactional. * **Redis.** Cache, session store, real-time pub/sub. * **RabbitMQ.** Message broker for the Celery workers. * **Object storage.** S3-compatible (MinIO, AWS S3, GCS, Azure Blob). File uploads, attachments, exports, and AI artifacts. * **OpenSearch** *(optional).* Full-text search index. Without it, search falls back to Postgres-based search. The full breakdown of versions, ports, resource recommendations, and dependency graph is at [Plane Architecture](/self-hosting/plane-architecture). For the complete environment-variable surface across all services, see [Environment variables](/self-hosting/govern/environment-variables). *** ## 6. What your team operates ### Skills your operators need * **Container operations.** Docker basics for [Docker Compose](/self-hosting/methods/docker-compose), Kubernetes and Helm if you go [HA](/self-hosting/methods/kubernetes). * **Postgres operations.** Backups, restores, upgrades, basic tuning. * **TLS and DNS.** [Custom domains](/self-hosting/govern/custom-domain), [SSL certificates](/self-hosting/govern/configure-ssl), [reverse proxy](/self-hosting/govern/reverse-proxy) configuration. * **Identity provider setup.** [SAML](/self-hosting/govern/saml-sso), [OIDC](/self-hosting/govern/oidc-sso), [LDAP](/self-hosting/govern/ldap), or OAuth, depending on what your org uses. * **Object storage operations.** Buckets, lifecycle policies, [private bucket](/self-hosting/govern/private-bucket) configuration. * **Secrets management.** [External secrets](/self-hosting/govern/external-secrets) integration if you use Vault, AWS Secrets Manager, or similar. ### Time budget * **Week 1: deploy and stand up.** Pick a [deployment method](/self-hosting/methods/overview), provision infrastructure, install, configure auth, smoke-test. 1 to 3 days of an FTE for Docker Compose, 3 to 7 days for production Kubernetes. * **Month 1: harden for production.** Backup and restore drill, monitoring, integrations, user onboarding, runbook documentation. Another 3 to 5 days spread across the month. * **Year 1: operate.** Roughly 4 minor upgrades plus 1 to 2 major upgrades, ongoing patches, capacity planning, and user support escalations. Plan for 0.1 to 0.25 FTE at small scale, 0.5 to 1.0 FTE at large scale. ### On-call and incident response Self-hosted Plane sits inside your operational perimeter. Decide ownership and escalation before you go live: who's on the rotation, what your internal runbook covers, when you escalate to Plane support. The [troubleshooting docs](/self-hosting/troubleshoot/overview) cover common cases, and your [support tier](/self-hosting/overview#get-help) determines response times when you need to escalate. *** ## 7. What changes between editions Editions differ in three ways that matter operationally: feature availability, release cadence, and license model. **Feature availability.** Community has parity with the Free tier of Cloud. Commercial and Airgapped get full parity with Cloud's paid plans (Pro, Business, Enterprise Grid), license-gated. **Release cadence.** Cloud is the test bed. New features ship there first, then Commercial, then Community. If your team needs the latest features quickly, that affects which edition fits. **License model.** Community is AGPL with no key. Commercial uses an online license key tied to one workspace and one machine. Airgapped uses an offline license bundle. Edition transitions are supported but generally one-directional in practice. See [Community to Commercial](/self-hosting/upgrade-from-community) and [Community to Airgapped](/self-hosting/manage/community-to-airgapped). Full feature, version, and codebase comparison: [Plane Editions](/self-hosting/editions-and-versions). Latest changes by edition: [Changelog](https://plane.so/changelog). *** ## 8. Lifecycle: deploy, configure, operate, upgrade Self-hosting Plane is four phases. ### Deploy Pick a method, provision infrastructure, install the binary or chart, run the migrator, smoke-test. Hours to days. Decisions: which [deployment method](/self-hosting/methods/overview), where Postgres and object storage live (managed services or self-run), which domain, which network topology. Changing these later is painful. ### Configure Authentication, network and TLS, secrets, integrations, and [instance admin](/self-hosting/govern/instance-admin) setup. Days to a week for a hardened production deployment. [Authentication](/self-hosting/govern/authentication) is often a full day's work. Picking and configuring [Google](/self-hosting/govern/google-oauth), [GitHub](/self-hosting/govern/github-oauth), [SAML](/self-hosting/govern/saml-sso), [OIDC](/self-hosting/govern/oidc-sso), or [LDAP](/self-hosting/govern/ldap), plus the [reset password flow](/self-hosting/govern/reset-password) and [email delivery](/self-hosting/govern/communication), takes most of the configure phase. ### Operate Day-2 work: user management, monitoring, backups, support, [logs](/self-hosting/manage/view-logs), capacity. Ongoing. The non-negotiable here is [Backup and restore](/self-hosting/manage/backup-restore). Set it up before users log in. Test the restore path within the first month. A backup you haven't restored from is a backup you don't actually have. ### Upgrade Plane ships frequently. Minor upgrades every 4 to 8 weeks, majors less often. Each upgrade involves a change window, a backup, the upgrade itself via [Upgrade Plane](/self-hosting/manage/upgrade-plane), and post-upgrade verification. Skipping upgrades for 6+ months means bigger upgrade-time risk, more breaking changes to handle at once, and missed security patches. *** ## 9. Patterns and anti-patterns Patterns we've seen repeated across self-hosted deployments. **Anti-pattern: Postgres in the same Docker container as Plane in production.** Fine for [Docker AIO](/self-hosting/methods/docker-aio) evaluations. In production, you can't snapshot the database independently, you can't scale it, and a container restart is a database restart. Use a managed Postgres or run it in a separate, properly backed-up container. [External services](/self-hosting/govern/database-and-storage) walks through both options. **Anti-pattern: skipping backups for the first three months.** Backups should be running before the first real user logs in. See [Backup and restore](/self-hosting/manage/backup-restore). **Anti-pattern: one environment, no staging.** Every upgrade becomes a production incident. A small staging instance, even on a single VM, lets you run upgrades there first. **Anti-pattern: underestimating object storage growth.** File uploads, page attachments, AI artifacts, and exports add up faster than people expect. Set lifecycle rules early and monitor growth. The [private bucket](/self-hosting/govern/private-bucket) and [database and storage](/self-hosting/govern/database-and-storage) docs cover the basics. **Anti-pattern: ignoring upgrades for six months or more.** The longer you wait, the bigger the gap, the more breaking changes accumulate, and the more security patches you've missed. Pick a cadence, quarterly minimum. **Anti-pattern: one person knows the configuration.** Document your specific setup (env vars, IdP configuration, backup destinations, certificate renewal procedure) somewhere your team can find it. The [environment variables reference](/self-hosting/govern/environment-variables) is where to start. **Pattern that works: managed services for the data layer.** Run Plane application services in containers, but use managed Postgres and managed object storage (RDS, Cloud SQL, S3, GCS). You inherit your cloud provider's backup, scaling, and durability story for the highest-stakes pieces. **Pattern that works: separate licenses per environment.** A separate license key for staging keeps prod clean and makes upgrade testing realistic. **Pattern that works: integrate with your existing observability stack.** Ship Plane logs and metrics into whatever you already use rather than inventing monitoring for it. Start with [View logs](/self-hosting/manage/view-logs). *** ## 10. Planning checklist Before you go to production, have a clear answer to each of these: 1. **Why self-hosting.** The specific reason: data residency, air-gapped network, internal integrations, upgrade control, scale economics, customization. 2. **Owner.** A specific person or team responsible for the system. 3. **Backup and disaster recovery target.** RPO and RTO in concrete numbers. 4. **Upgrade cadence.** Monthly, quarterly, or another rhythm. See [Upgrade Plane](/self-hosting/manage/upgrade-plane). 5. **Escalation path.** Internal on-call rotation and the support tier you escalate to externally. Once these are settled, head to the [Self-hosting overview](/self-hosting/overview) and pick a deployment scenario. *** ## 11. Next steps **Try Plane self-hosted.** [Docker AIO](/self-hosting/methods/docker-aio) gives you a single container with embedded services in about 10 minutes (POC only). **Plan a production deployment.** Read [Plane Editions](/self-hosting/editions-and-versions) to pick your edition. Read [Plane Architecture](/self-hosting/plane-architecture) to plan capacity and network. Then pick a [deployment method](/self-hosting/methods/overview). **Talk to a human.** [Talk to sales](https://plane.so/talk-to-sales) for pricing, contracts, professional services, and airgapped. [Community Discord](https://discord.gg/plane) for open questions. [GitHub issues](https://github.com/makeplane/plane/issues) for bugs and feature requests. [**Continue to: Self-hosting overview →**](/self-hosting/overview) --- --- url: 'https://developers.plane.so/self-hosting/editions-and-versions.html' description: >- Compare Plane Community, Pro, Business, and Enterprise editions. Understand features, pricing tiers, and version differences for self-hosted deployments. --- # Understanding Plane's editions Plane comes in four editions by how its deployed. Our Cloud is our only hosted edition as of 2025. Additionally, we offer three unique self-hosted editions tailored to meet two sets of unique needs—the open-source Community Edition, the recommended Commercial Edition, and the Airgapped Edition. ## About our self-hosted editions ### Community Built with transparency in mind, the Community Edition, * Is governed by the AGPL v3.0 license, ensuring free and open usage * Allows contribution to the repo by way of modifications and customizations * Has no code dependencies or restrictions on and from the Commercial Edition It’s ideal for those who want to try Plane first, audit the code for security, and see how each one of services works with the others. Several tens of thousands of uses have used it and a significant number have contributed to it. The Community Edition is at par with the Free tier of the Cloud edition in its feature availability. To upgrade to paid plans, you must first switch to the Commercial Edition. ### Commercial Designed for teams that want governance, compliance, and privacy controls, the Commercial Edition is ideal for teams that want to try Plane with an intent to unlock advanced work management and security features. This edition also comes with a Free tier, but also lets you upgrade seamlessly to all our paid plans. It offers, * Full feature parity with our Cloud * A bundle of 12 Free user seats per workspace so there are no surprises when you upgrade * An intuitive upgrade flow that automatically calculates the number of seats you need by the number of users with paid roles in your workspace, so you never have to guess ### Airgapped Built for organizations with strict security and compliance requirements, the Airgapped Commercial Edition provides the same powerful features as the Commercial Edition but operates in completely isolated environments without internet connectivity. The Airgapped Edition offers: * **Complete isolation** Operates entirely within your network perimeter with no external dependencies or outbound connections. * **Full feature parity** Includes all features available in the standard Commercial Edition, including advanced work management, security controls, and governance tools * **Version updates** Updates from your own docker registry. * **Self-contained architecture** All services, dependencies, and resources are bundled for deployment in restricted networks * **Compliance-ready** Designed to meet requirements for environments that prohibit external network communication ## Why we separate editions We’ve designed Plane’s editions to serve diverse user needs while staying true to the ethos of open source. * The **Community Edition** is completely open-source, with no restrictions beyond those outlined in the [AGPL v3.0 license](https://github.com/makeplane/plane/blob/preview/LICENSE.txt). This is the edition that is now ranking at #1 in our space on GitHub. * The **Commercial Edition** remains closed-source to offer enterprise-grade features and seamless scalability for businesses. * The **Airgapped Edition** extends the Commercial Edition's capabilities to isolated environments, ensuring organizations with strict security requirements can still benefit from Plane's full feature set. Unlike some open-core companies, we’ve adopted a clean separation to keep things simple and transparent. There’s no hidden code that limits modifications on the Community Edition, and no forced migrations from one edition to another. ## Differences in versions between editions Each of our editions is built on a distinct codebase. Versions with each differ for how we ship new code per our three separate release cycles. This distinction allows us to * Use the Cloud as a test bed for new features before they come to our self-hosted editions * Innovate quickly on a more controlled Commercial Edition * Be intentful and deliberate with changes to the Community Edition For both the Commercial, Airgapped, and Community Editions, version updates are in your control. Regular updates ensure you’re benefiting from the latest features and improvements. See [Update Plane](/self-hosting/manage/upgrade-plane) for how to upgrade your versions. ## Changelog We maintain a detailed changelog for all editions. [Check it out](https://plane.so/changelog) and bookmark it to stay informed about the latest features, bug fixes, and improvements by edition. --- --- url: 'https://developers.plane.so/self-hosting/plane-architecture.html' description: >- Understand Plane self-hosted architecture including web server, API, workers, database, Redis, and storage components. System requirements and service topology. --- # Plane self-hosted architecture Plane consists of multiple services working together to provide project management capabilities. ![Plane architecture](/images/airgapped/plane-architecture.webp#hero) ### Frontend services **Web**\ The main application interface where users interact with projects, work items, and pages. This service serves the UI and handles client-side routing. **Space**\ This powers public sharing. It lets you publish projects, views, pages to the web, so others can view without needing to log in. **Admin**\ Instance administration interface for workspace owners and administrators. Manages billing, licensing, workspace settings, and user permissions. ### API server **API**\ The core REST API that handles all data operations. All frontend services communicate with this API for creating, reading, updating, and deleting data. **Worker**\ Background job processor that handles async operations like file processing, notification dispatch, and data imports. Workers pull jobs from RabbitMQ and execute them independently. **Beat worker**\ Scheduled task executor that runs periodic jobs like data cleanup, report generation, and reminder notifications. Uses a cron-like scheduling system. **Migrator**\ Database schema management service that runs on deployment to apply schema changes and data migrations. Runs once during upgrades then exits. ### Supporting services **Proxy**\ Handles incoming traffic and routes it to the appropriate services. Manages certificates and reverse proxying. In Docker deployments, Plane uses Caddy for automatic SSL certificate management and traffic routing. **Live**\ Real-time collaboration service powered by WebSockets. Handles cursor positions, live updates, and presence indicators for multiple users working simultaneously. **Monitor**\ Used for license validation and activation. It checks the license status and ensures your instance is compliant. **Silo**\ Integration backend that manages connections to GitHub, GitLab, and Slack. Handles OAuth flows, webhook processing, and API communication with external systems. **Intake**\ Email ingestion service that converts incoming emails into work items or comments. Requires SMTP configuration and DNS setup. ### Infrastructure dependencies **PostgreSQL**\ Primary relational database storing all application data including projects, work items, users, and configuration. Plane requires PostgreSQL 15.7+ or 16.x. **Redis/Valkey**\ In-memory cache and session store. Used for caching frequently accessed data, storing user sessions, and managing real-time collaboration state. **RabbitMQ**\ Message queue for asynchronous task processing. Workers pull jobs from queues for background operations like imports, exports, and notifications. **MinIO/S3**\ Object storage for file uploads, attachments, and generated exports; can be replaced with any S3-compatible storage system. **OpenSearch**\ Optional search indexing service for enhanced search capabilities. Not required for basic Plane functionality. --- --- url: 'https://developers.plane.so/self-hosting/methods/overview.html' description: >- Choose the best deployment method for your infrastructure. Deploy Plane with Docker, Kubernetes, Podman, or in airgapped environments. --- # Install Plane Choose a deployment method based on your infrastructure and requirements. ## System requirements * **CPU:** 2 cores (x64/AMD64 or AArch64/ARM64) * **RAM:** 4GB (8GB recommended for production) * **OS:** Ubuntu, Debian, CentOS, Amazon Linux 2 or 2023, macOS, Windows with WSL2 ## Deployment methods Plane supports a wide range of deployment options from simple single-container setups to enterprise-grade Kubernetes clusters. ### Container deployments Core deployment methods for running Plane with containerized services: ### Platform deployments Deploy Plane using specialized platforms and orchestration tools: ### Airgapped deployments For environments without internet access or with strict security requirements: --- --- url: 'https://developers.plane.so/self-hosting/methods/docker-compose.html' description: >- Install Plane using Docker Compose. Step-by-step guide for deploying Plane with Docker on your server with all required services. --- # Docker Compose This guide shows you the steps to deploy a self-hosted instance of Plane using Docker. ::: tip If you want to upgrade from Community to the Commercial edition, see [Upgrade to Commercial Edition](/self-hosting/upgrade-from-community). ::: ## Install Plane Plane Pro and Plane Business are enabled on this edition, so the Free plan on this edition is easier to trial our paid plans from. ### Prerequisites * **CPU:** 2 cores (x64/AMD64 or AArch64/ARM64) * **RAM:** 4GB (8GB recommended for production) * **OS:** Ubuntu, Debian, CentOS, Amazon Linux 2 or 2023, macOS, Windows with WSL2 ::: info Ensure you're using the **latest version of Docker Compose**. Check your Docker Compose version with `docker-compose --version` and update if needed. ::: ### Procedure 1. `ssh` into your machine as the root user (or user with sudo access) per the norms of your hosting provider. 2. Run the command below: ```bash curl -fsSL https://prime.plane.so/install/ | sh - ``` 3. Follow the instructions on the terminal. Hit `Enter` or `Return` to continue. 4. Enter the domain name where you will access the Plane app in the format `domain.tld` or `subdomain.domain.tld`. 5. Choose one of the options below: * **Express**: Plane installs with the default configurations. * **Advanced**: You can customize the database, Redis, storage and other settings. ::: warning When self-hosting Plane for production use, it is strongly recommended to configure [external database and storage](/self-hosting/govern/database-and-storage). This ensures that your data remains secure and accessible even if the local machine crashes or encounters hardware issues. Relying solely on local storage for these components increases the risk of data loss and service disruption. ::: 6. The installation will take a few minutes to complete and you will see the message **Plane has successfully installed**. You can access the Plane application on the domain you provided during the installation. 7. If you've purchased a paid plan, [activate your license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) to unlock premium features. ::: details Install Community Edition The Commercial edition comes with a free plan and the flexibility to upgrade to a paid plan at any point. If you still want to install the Community edition, follow the steps below: #### Prerequisites * Docker installed and running. Choose one of the following options: * **Option 1**\ Create an EC2 machine on AWS. It must of minimum **t3.medium/t3a.medium**. Run the below command to install docker engine. ```bash curl -fsSL https://get.docker.com | sh - ``` * **Option 2**\ Install [Docker Desktop](https://www.docker.com/products/docker-desktop/). * OS with bash scripting enabled (Ubuntu, Linux AMI, macOS). Windows systems need to have [gitbash](https://git-scm.com/download/win). * User context used must have access to docker services. In most cases, use `sudo su` to switch as root user. * Use the terminal (or gitbash) window to run all the future steps. #### Installation 1. Create a folder named `plane-selfhost` on your machine for deployment and data storage. ```bash mkdir plane-selfhost ``` 2. Navigate to this folder using the cd command. ```bash cd plane-selfhost ``` 3. Download the latest stable release. ```bash curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh ``` 4. Make the file executable. ```bash chmod +x setup.sh ``` 5. Run the following command: ```bash ./setup.sh ``` This will prompt you with the below options. ```bash Select a Action you want to perform: 1) Install (arm64) 2) Start 3) Stop 4) Restart 5) Upgrade 6) View Logs 7) Backup Data 8) Exit Action [2]: 1 ``` 6. Enter `1` as input. This will create a folder `plane-app` or `plane-app-preview` (in case of preview deployment) and will download the `docker-compose.yaml` and `plane.env` files. 7. Enter `8` to exit. 8. Set up the environment variables. You can use any text editor to edit this file. Below are the most importants keys you must refer to: * `LISTEN_HTTP_PORT`: This is set to `80` by default. Make sure the port you choose to use is not preoccupied. For example, `LISTEN_HTTP_PORT=8080` * `LISTEN_HTTPS_PORT`: This is set to `443` by default. Make sure the port you choose to use is not preoccupied. For example, `LISTEN_HTTPS_PORT=4430` * `WEB_URL`: This is set to `http://localhost` by default. Change this to the FQDN you plan to use along with LISTEN\_HTTP\_PORT. For example, `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`. * `CORS_ALLOWED_ORIGINS`: This is set to `http://localhost` by default. Change this to the FQDN you plan to use along with LISTEN\_HTTP\_PORT. For example, `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`. 9. Run the following command to continue with the setup. ```bash ./setup.sh ``` 10. Enter `2` as input to start the services. You will something like this:\ ![Downloading docker images](/images/docker-compose/download-docker.png) Be patient as it might take some time based on your download speed and system configuration. If all goes well, you must see something like this: ![Downloading completed](/images/docker-compose/download-complete.png) This is the confirmation that all images were downloaded and the services are up and running. You have successfully self-hosted the Plane instance. Access the application by going to IP or domain you have configured it on. For example, `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`. ##### Stop server In case you want to make changes to the environment variables in the `plane.env` file, we recommend that you stop the services before doing that. Run the `./setup.sh` command. Enter `3` to stop the services. If all goes well, you will see something like this: ![Stop Services](/images/docker-compose/stopped-docker.png) ##### Restart server In case you want to make changes to `plane.env` variables without stopping the server or noticed some abnormalities in services, you can restart the services. Run the `./setup.sh` command. Enter `4` to restart the services. If all goes well, you will see something like this: ![Restart Services](/images/docker-compose/restart-docker.png) ::: ## Troubleshoot * [Error during Docker Compose execution](/self-hosting/troubleshoot/installation-errors#error-during-docker-compose-execution) * [Migrator container exited](/self-hosting/troubleshoot/installation-errors#migrator-container-exited) --- --- url: 'https://developers.plane.so/self-hosting/methods/docker-aio.html' description: >- Deploy Plane with Docker All-in-One (AIO) setup. Quick installation guide for running Plane in a single Docker container. --- # Docker AIO (All-in-One) The Plane Commercial All-in-One (AIO) Docker image packages all Plane services into a single container, making it the fastest way to get Plane running. ## What's included Your single AIO container includes all these services running together: * **Web App** - The main Plane web interface you'll use * **Space** - Public project spaces for external collaboration * **Admin** - Administrative interface * **API Server** - Backend API * **Live Server** - Real-time collaboration features * **Silo** - Integration services * **Monitor** - Feature flags and payments * **Email Server** - SMTP server for notifications * **Proxy** (Port 80, 20025, 20465, 20587) - Caddy reverse proxy * **Worker and Beat Worker** - Background task processing ### Port Mapping The following ports are exposed: * `80`: Main web interface (HTTP) * `443`: HTTPS (if SSL configured) * `20025`: SMTP port 25 * `20465`: SMTP port 465 (SSL/TLS) * `20587`: SMTP port 587 (STARTTLS) ## Prerequisites * [Docker](https://docs.docker.com/engine/) * Set up these external services: * *PostgreSQL*\ For data storage * *Redis*\ For caching and session management * *RabbitMQ* For message queuing * *S3-compatible storage*\ For file uploads (AWS S3 or MinIO) ## Install Plane 1. Download the image with: ```bash docker pull makeplane/plane-aio-commercial:stable ``` 2. Run the following command to deploy the Plane AIO container. Make sure to replace all placeholder values (e.g., `your-domain.com`, `user:pass`) with your actual configuration. ::: warning All environment variables are required for the container to function correctly. ::: ```bash docker run --name plane-aio --rm -it \ -p 80:80 \ -p 20025:20025 \ -p 20465:20465 \ -p 20587:20587 \ -e DOMAIN_NAME=your-domain.com \ -e DATABASE_URL=postgresql://user:pass@host:port/database \ -e REDIS_URL=redis://host:port \ -e AMQP_URL=amqp://user:pass@host:port/vhost \ -e AWS_REGION=us-east-1 \ -e AWS_ACCESS_KEY_ID=your-access-key \ -e AWS_SECRET_ACCESS_KEY=your-secret-key \ -e AWS_S3_BUCKET_NAME=your-bucket \ makeplane/plane-aio-commercial:stable ``` If you're running on an IP address, use this example: ```bash MYIP=192.168.68.169 docker run --name myaio --rm -it \ -p 80:80 \ -p 20025:20025 \ -p 20465:20465 \ -p 20587:20587 \ -e DOMAIN_NAME=${MYIP} \ -e DATABASE_URL=postgresql://plane:plane@${MYIP}:15432/plane \ -e REDIS_URL=redis://${MYIP}:16379 \ -e AMQP_URL=amqp://plane:plane@${MYIP}:15673/plane \ -e AWS_REGION=us-east-1 \ -e AWS_ACCESS_KEY_ID= \ -e AWS_SECRET_ACCESS_KEY= \ -e AWS_S3_BUCKET_NAME=plane-app \ -e AWS_S3_ENDPOINT_URL=http://${MYIP}:19000 \ -e FILE_SIZE_LIMIT=10485760 \ makeplane/plane-aio-commercial:stable ``` 3. Once it's running, you can access the Plane application on the domain you provided during the deployment. 4. If you've purchased a paid plan, [activate your license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) to unlock premium features. ## Volume mounts ### Recommended persistent volumes ```bash -v /path/to/logs:/app/logs \ -v /path/to/data:/app/data ``` ### Workspace license DB ```bash -v /path/to/monitordb:/app/monitor ``` ### SSL certificate support For HTTPS support, mount certificates: ```bash -v /path/to/certs:/app/email/tls ``` ## Environment variables (optional) ### Network and Protocol * `SITE_ADDRESS`: Server bind address (default: `:80`) * `APP_PROTOCOL`: Protocol to use (`http` or `https`, default: `http`) ### Email configuration * `INTAKE_EMAIL_DOMAIN`: Domain for intake emails (default: `intake.`) * `LISTEN_SMTP_PORT_25`: SMTP port 25 mapping (default: `20025`) * `LISTEN_SMTP_PORT_465`: SMTP port 465 mapping (default: `20465`) * `LISTEN_SMTP_PORT_587`: SMTP port 587 mapping (default: `20587`) * `SMTP_DOMAIN`: SMTP server domain (default: `0.0.0.0`) * `TLS_CERT_PATH`: Path to TLS certificate file (optional) * `TLS_PRIV_KEY_PATH`: Path to TLS private key file (optional) ### Security and secrets * `MACHINE_SIGNATURE`: Unique machine identifier (auto-generated if not provided) * `SECRET_KEY`: Django secret key (default provided) * `SILO_HMAC_SECRET_KEY`: Silo HMAC secret (default provided) * `AES_SECRET_KEY`: AES encryption key (default provided) * `LIVE_SERVER_SECRET_KEY`: Live server secret (default provided) ### File handling * `FILE_SIZE_LIMIT`: Maximum file upload size in bytes (default: `5242880` = 5MB) ### Integration callbacks * `INTEGRATION_CALLBACK_BASE_URL`: Base URL for OAuth callbacks ### API configuration * `API_KEY_RATE_LIMIT`: API key rate limit (default: `60/minute`) ### Third-party integrations * `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`: GitHub integration * `GITHUB_APP_NAME`, `GITHUB_APP_ID`, `GITHUB_PRIVATE_KEY`: GitHub App integration * `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`: Slack integration * `GITLAB_CLIENT_ID`, `GITLAB_CLIENT_SECRET`: GitLab integration ## Build the image To build the AIO image yourself: ```bash cd deploy/aio/commercial ./build.sh --release=v1.11.1 ``` Available build options: * `--release`: Plane version to build (required) * `--image-name`: Custom image name (default: `plane-aio-commercial`) ## Troubleshoot The container will validate required environment variables on startup and display helpful error messages if any are missing. ### Logs All service logs are available in `/app/logs/`: * Access logs: `/app/logs/access/` * Error logs: `/app/logs/error/` ### Health checks The container runs multiple services managed by Supervisor. Check service status: ```bash docker exec -it supervisorctl status ``` ## Production considerations * Use proper SSL certificates for HTTPS * Configure proper backup strategies for data * Monitor resource usage and scale accordingly * Use external load balancer for high availability * Regularly update to latest versions * Secure your environment variables and secrets --- --- url: 'https://developers.plane.so/self-hosting/methods/docker-swarm.html' description: >- Deploy Plane on Docker Swarm cluster. Guide for running Plane in a distributed Docker Swarm environment with high availability. --- # Deploy Plane with Docker Swarm This guide shows you the steps to deploy a self-hosted instance of the Plane Commercial Edition using Docker Swarm. ## Install Plane ### Prerequisites * Before you get started, make sure you have a Docker Swarm environment set up and ready to go. * Your setup should support either amd64 or arm64 architectures. ### Procedure 1. **Download the required deployment files** * `swarm-compose.yml` – Defines Plane's services and dependencies. ```bash curl -fsSL https://prime.plane.so/releases//swarm-compose.yml -o swarm-compose.yml ``` * `variables.env` – Stores environment variables for your deployment. ```bash curl -fsSL https://prime.plane.so/releases//variables.env -o plane.env ``` ::: warning The `` value should be v1.8.3 or higher. ::: 2. **Configure environment variables**\ Before deploying, edit the `variables.env` file in your preferred text editor and update the following values: * `DOMAIN_NAME` – (required) Your application's domain name. * `SITE_ADDRESS` – (required) The full domain name (FQDN) of your instance. * `MACHINE_SIGNATURE` – (required) A unique identifier for your machine. You can generate this by running below code in terminal: ```sh sed -i 's/MACHINE_SIGNATURE=.*/MACHINE_SIGNATURE='$(openssl rand -hex 16)'/' plane.env ``` * `CERT_EMAIL` – (optional) Email address for SSL certificate generation (only needed if you're setting up HTTPS). 3. **Configure external DB, Redis, and RabbitMQ** ::: warning When self-hosting Plane for production use, it is strongly recommended to configure external database and storage. This ensures that your data remains secure and accessible even if the local machine crashes or encounters hardware issues. Relying solely on local storage for these components increases the risk of data loss and service disruption. ::: * `DATABASE_URL` – Connection string for your external database. * `REDIS_URL` – Connection string for your external Redis instance. * `AMQP_URL` – Connection string for your external RabbitMQ server. 4. **Load the environment variables** ```bash set -o allexport; source ; set +o allexport; ``` 5. **Deploy the stack** ```bash docker stack deploy -c plane ``` That's it! This will deploy Plane as a Swarm stack, and your instance should be accessible on your configured domain. 6. If you've purchased a paid plan, [activate your license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) to unlock premium features. --- --- url: 'https://developers.plane.so/self-hosting/methods/download-config.html' description: >- Download docker-compose.yml and variables.env files for a specific Plane release as a zip archive. --- # Download Docker config files If you're running a custom Docker setup and don't use `prime-cli`, you can download the `docker-compose.yml` and `variables.env` files for any Plane release directly. ## Endpoint ```bash curl "https://prime.plane.so/api/v2/setup/?version=&airgapped=&platform=" -o plane.zip ``` **Authentication:** None required (public endpoint) ## Parameters | Parameter | Required | Default | Description | | ----------- | -------- | ------- | ---------------------------------------------------------------------------- | | `version` | Yes | — | Release tag (e.g., `v2.6.3`) | | `airgapped` | No | `false` | Set to `true` for airgapped compose files | | `platform` | No | `amd64` | Target architecture: `amd64` or `arm64`. Only applies when `airgapped=true`. | ## What's in the zip **Standard download** * `docker-compose.yml` * `variables.env` **Airgapped download** * `airgapped-docker-compose-{platform}.yml` * `variables.env` ## Quick download **Standard setup** ```bash curl "https://prime.plane.so/api/v2/setup/?version=v2.6.3" -o plane.zip unzip plane.zip ``` **Airgapped setup (AMD64)** ```bash curl "https://prime.plane.so/api/v2/setup/?version=v2.6.3&airgapped=true" -o plane.zip unzip plane.zip ``` **Airgapped setup (ARM64)** ```bash curl "https://prime.plane.so/api/v2/setup/?version=v2.6.3&airgapped=true&platform=arm64" -o plane.zip unzip plane.zip ``` Replace `v2.6.3` with the version you need. See the [releases page](https://plane.so/changelog?category=self-hosted) for available versions. ### Error responses | Status | Cause | Response | | ------ | ----------------------------------- | ------------------------------------------------------- | | 400 | Missing `version` parameter | `{"error": "version query parameter is required"}` | | 400 | Invalid `platform` value | `{"error": "platform must be amd64 or arm64"}` | | 400 | Server missing GitHub configuration | `{"error": "missing required settings"}` | | 404 | Release tag not found | `{"error": "release not found"}` | | 404 | Config files missing from release | `{"error": "assets not found in release: "}` | | 500 | GitHub API failure | `{"error": "Failed to fetch release information"}` | --- --- url: 'https://developers.plane.so/self-hosting/methods/kubernetes.html' description: >- Deploy Plane on Kubernetes using Helm charts. Complete guide for production-ready Kubernetes deployment with scaling and management. --- # Kubernetes This guide shows you the steps to deploy a self-hosted instance of Plane using Kubernetes. ::: tip If you want to upgrade from Community to the Commercial edition, see [Upgrade to Commercial Edition](/self-hosting/upgrade-from-community). ::: ## Install Plane Plane Pro and Plane Business are enabled on this edition, so the Free plan on this edition is easier to trial our paid plans from. ### Prerequisites * A working Kubernetes cluster * `kubectl` and `helm` on the client system that you will use to install our Helm charts ::: info Ensure you use use the latest Helm chart version. ::: ### Procedure 1. Open terminal or any other command-line app that has access to Kubernetes tools on your local system. 2. Set the following environment variables: ```bash PLANE_VERSION=v2.6.3 ``` ```bash DOMAIN_NAME= ``` ::: warning When configuring the PLANE\_VERSION environment variable, **do not** set it to `stable`. Always specify the latest version number (e.g., `v2.6.3`). Using `stable` can lead to unexpected issues. ::: 3. Add the Plane helm chart repo. ```bash helm repo add plane https://helm.plane.so/ ``` 4. Use one of the following ways to deploy Plane: * **Quick setup**: This is the fastest way to deploy Plane with the default settings. This will create stateful deployments for Postgres, Redis/Valkey, and Minio with a persistent volume claim using the `longhorn` storage class. This also sets up the Ingress routes for you using `nginx` ingress class. To customize these settings, see the [Custom ingress routes](#custom-ingress-routes). Run the following command to deploy Plane: ``` helm upgrade --install plane-app plane/plane-enterprise \ --create-namespace \ --namespace plane \ --set license.licenseDomain=${DOMAIN_NAME} \ --set license.licenseServer=https://prime.plane.so \ --set planeVersion=${PLANE_VERSION} \ --set ingress.enabled=true \ --set ingress.ingressClass=nginx \ --set env.storageClass=longhorn \ --timeout 10m \ --wait \ --wait-for-jobs ``` ::: info This is the minimum required to set up Plane Commercial edition. You can change the default namespace from `plane`, the default app name from `plane-app`, the default storage class from `longhorn`, and the default ingress class from `nginx` to whatever you would like to. To use a custom StorageClass, add `--set env.storageClass=` to the command above. You can also pass other settings referring to the **Configuration Settings** toggle section below. ::: * **Advanced setup**: ::: warning When self-hosting Plane for production use, it is strongly recommended to configure [external database and storage](/self-hosting/methods/kubernetes#configuration-settings). This ensures that your data remains secure and accessible even if the local machine crashes or encounters hardware issues. Relying solely on local storage for these components increases the risk of data loss and service disruption. ::: For more control over your setup, follow the steps below: i. Run the script below to download the `values.yaml` file and edit using any editor like Vim or Nano. Make sure you set the required environment variables listed below: * `planeVersion: v2.6.3` * `license.licenseDomain: ` * `license.licenseServer: https://prime.plane.so` * `ingress.enabled: ` * `ingress.ingressClass: ` * `env.storageClass: ` See the **Configuration settings** toggle section for more details. ```bash helm upgrade --install plane-app plane/plane-enterprise \ --create-namespace \ --namespace plane \ -f values.yaml \ --timeout 10m \ --wait \ --wait-for-jobs ``` ii. If you've purchased a paid plan, [activate your license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) to unlock premium features. ## Configuration settings #### License | Setting | Default | Required | Description | | --------------------- | :-----------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | planeVersion | v2.6.3 | Yes | Specifies the version of Plane to be deployed. Copy this from prime.plane.so. | | license.licenseDomain | 'plane.example.com' | Yes | The fully-qualified domain name (FQDN) in the format `sudomain.domain.tld` or `domain.tld` that the license is bound to. It is also attached to your `ingress` host to access Plane. | ### Airgapped Settings | Setting | Default | Required | Description | | ---------------------- | :-----: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | airgapped.enabled | false | No | Enable airgapped mode for the Plane API. | | airgapped.s3Secrets | \[] | No | List of Kubernetes Secrets containing CA certificates to install. Each entry requires `name` (Secret name) and `key` (filename in the Secret). Example: `kubectl -n plane create secret generic plane-s3-ca --from-file=s3-custom-ca.crt=/path/to/ca.crt`. Supports multiple certs (e.g. S3 + internal CA). Available in v2.6.3 and later. | | airgapped.s3SecretName | "" | No | **Deprecated** Name of a single Kubernetes Secret containing the S3 CA cert. Used only when `s3Secrets` is empty. Use `s3Secrets` instead. | | airgapped.s3SecretKey | "" | No | **Deprecated** Key (filename) of the cert file inside the Secret. Used only when `s3Secrets` is empty. Set together with `airgapped.s3SecretName`. Use `s3Secrets` instead. | #### CA certificate configuration (For airgapped deployments only) Plane supports custom CA certificates for connecting to S3-compatible storage and other internal services in airgapped environments. * **New deployments:** Use `airgapped.s3Secrets` as shown in the table above. * **Existing deployments using `s3SecretName` and `s3SecretKey`:** Your configuration still works. Migrate only if you need to use multiple CA certificates. #### Migrating to the new configuration :::warning Requires Plane v2.6.3 or later. ::: The new `s3Secrets` configuration supports multiple CA certificates, useful if you need to trust certificates from different sources (e.g., S3 endpoint CA and internal PKI). If you only need a single certificate, migration is optional. To migrate: 1. Add your existing secret to the `s3Secrets` list: ```yaml airgapped: enabled: true s3Secrets: - name: plane-s3-ca # your existing s3SecretName value key: s3-custom-ca.crt # your existing s3SecretKey value # s3SecretName and s3SecretKey can be removed after migration ``` 2. Remove `s3SecretName` and `s3SecretKey` from your values file. 3. Upgrade your Helm release. #### Docker Registry | Setting | Default | Required | Description | | ----------------------------- | :-----------------: | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | dockerRegistry.enabled | false | No | Enable to configure image pull secrets for pulling images from a private docker registry. When enabled, you can either provide credentials to create a new secret or use an existing Kubernetes secret. | | dockerRegistry.existingSecret | | No | Name of an existing Kubernetes secret containing docker registry credentials. When specified, the chart will use this secret for `imagePullSecrets` instead of creating a new one. The secret should be of type `kubernetes.io/dockerconfigjson`. If left empty, credentials below will be used to create a new secret. | | dockerRegistry.registry | index.docker.io/v1/ | No | Docker registry URL. Only used when `dockerRegistry.existingSecret` is empty. | | dockerRegistry.loginid | | No | Login ID / Username for the docker registry. Only used when `dockerRegistry.existingSecret` is empty. | | dockerRegistry.password | | No | Password or Token for the docker registry. Only used when `dockerRegistry.existingSecret` is empty. | #### Postgres | Setting | Default | Required | Description | | ----------------------------------- | :--------------------: | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.postgres.local\_setup | true | | Plane uses `postgres` as the primary database to store all the transactional data. This database can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws rds or similar services). Set this to `true` when you choose to setup stateful deployment of `postgres`. Mark it as `false` when using a remotely hosted database | | services.postgres.image | `postgres:15.7-alpine` | | Using this key, user must provide the docker image name to setup the stateful deployment of `postgres`. (must be set when `services.postgres.local_setup=true`) | | services.postgres.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of postgres. (must be set when `services.postgres.local_setup=true`) | | services.postgres.servicePort | 5432 | | This key sets the default port number to be used while setting up stateful deployment of `postgres`. | | services.postgres.volumeSize | 2Gi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | env.pgdb\_username | plane | | Database credentials are requried to access the hosted stateful deployment of `postgres`. Use this key to set the username for the stateful deployment. | | env.pgdb\_password | plane | | Database credentials are requried to access the hosted stateful deployment of `postgres`. Use this key to set the password for the stateful deployment. | | env.pgdb\_name | plane | | Database name to be used while setting up stateful deployment of `Postgres` | | services.postgres.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.postgres.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of postgres. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.postgres.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of postgres. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.postgres.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of postgres. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.postgres.labels | {} | | This key allows you to set custom labels for the stateful deployment of postgres. This is useful for organizing and selecting resources in your Kubernetes cluster. | | services.postgres.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of postgres. This is useful for adding metadata or configuration hints to your resources. | | env.pgdb\_remote\_url | | | Users can also decide to use the remote hosted database and link to Plane deployment. Ignoring all the above keys, set `services.postgres.local_setup` to `false` and set this key with remote connection url. | #### Redis/Valkey Setup | Setting | Default | Required | Description | | -------------------------------- | :---------------------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.redis.local\_setup | true | | Plane uses `redis` to cache the session authentication and other static data. This database can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws rds or similar services). Set this to `true` when you choose to setup stateful deployment of `redis`. Mark it as `false` when using a remotely hosted database | | services.redis.image | `valkey/valkey:7.2.11-alpine` | | Using this key, user must provide the docker image name to setup the stateful deployment of `redis`. (must be set when `services.redis.local_setup=true`) | | services.redis.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of redis. (must be set when services.redis.local\_setup=true) | | services.redis.servicePort | 6379 | | This key sets the default port number to be used while setting up stateful deployment of `redis`. | | services.redis.volumeSize | 500Mi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | services.redis.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.redis.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of redis. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.redis.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of redis. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.redis.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of redis. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.redis.labels | {} | | This key allows you to set custom labels for the stateful deployment of redis. This is useful for organizing and selecting resources in your Kubernetes cluster. | | services.redis.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of redis. This is useful for adding metadata or configuration hints to your resources. | | env.remote\_redis\_url | | | Users can also decide to use the remote hosted database and link to Plane deployment. Ignoring all the above keys, set `services.redis.local_setup` to `false` and set this key with remote connection url. | #### RabbitMQ Setup | Setting | Default | Required | Description | | --------------------------------------- | :---------------------------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | services.rabbitmq.local\_setup | true | | Plane uses `rabbitmq` as message queuing system. This can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws mq or similar services). Set this to `true` when you choose to setup stateful deployment of `rabbitmq`. Mark it as `false` when using a remotely hosted service | | services.rabbitmq.image | `rabbitmq:3.13.6-management-alpine` | | Using this key, user must provide the docker image name to setup the stateful deployment of `rabbitmq`. (must be set when `services.rabbitmq.local_setup=true`) | | services.rabbitmq.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of `rabbitmq`. (must be set when `services.rabbitmq.local_setup=true`) | | services.rabbitmq.servicePort | 5672 | | This key sets the default port number to be used while setting up stateful deployment of `rabbitmq`. | | services.rabbitmq.managementPort | 15672 | | This key sets the default management port number to be used while setting up stateful deployment of `rabbitmq`. | | services.rabbitmq.volumeSize | 100Mi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | services.rabbitmq.default\_user | plane | | Credentials are requried to access the hosted stateful deployment of `rabbitmq`. Use this key to set the username for the stateful deployment. | | services.rabbitmq.default\_password | plane | | Credentials are requried to access the hosted stateful deployment of `rabbitmq`. Use this key to set the password for the stateful deployment. | | services.rabbitmq.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.rabbitmq.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of rabbitmq. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.rabbitmq.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of rabbitmq. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.rabbitmq.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of rabbitmq. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.rabbitmq.labels | {} | | This key allows you to set custom labels for the stateful deployment of rabbitmq. This is useful for organizing and selecting resources in your Kubernetes cluster. | | services.rabbitmq.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of rabbitmq. This is useful for adding metadata or configuration hints to your resources. | | services.rabbitmq.external\_rabbitmq\_url | | | Users can also decide to use the remote hosted service and link to Plane deployment. Ignoring all the above keys, set `services.rabbitmq.local_setup` to `false` and set this key with remote connection url. | #### OpenSearch Setup | Setting | Default | Required | Description | | ------------------------------------- | :--------------------------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.opensearch.local\_setup | false | | Plane uses `opensearch` as the search and analytics engine. This can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. AWS OpenSearch Service or similar services). Set this to `true` when you choose to setup stateful deployment of `opensearch`. Mark it as `false` when using a remotely hosted service | | services.opensearch.image | opensearchproject/opensearch:3.3.2 | | Using this key, user must provide the docker image name to setup the stateful deployment of `opensearch`. (must be set when `services.opensearch.local_setup=true`) | | services.opensearch.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of `opensearch`. (must be set when `services.opensearch.local_setup=true`) | | services.opensearch.servicePort | 9200 | | This key sets the default port number to be used while setting up stateful deployment of `opensearch`. | | services.opensearch.volumeSize | 5Gi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | services.opensearch.username | plane | | Credentials are required to access the hosted stateful deployment of `opensearch`. Use this key to set the username for the stateful deployment. | | services.opensearch.password | Secure@Pass#123!%^&\* | | Credentials are required to access the hosted stateful deployment of `opensearch`. Use this key to set the password. **Password Complexity Requirements:** Must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one digit, and one special character (e.g., `!@#$%^&*`). | | services.opensearch.memoryLimit | 3Gi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.opensearch.cpuLimit | 750m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.opensearch.memoryRequest | 2Gi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.opensearch.cpuRequest | 500m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.opensearch.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.opensearch.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of opensearch. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.opensearch.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of opensearch. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.opensearch.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of opensearch. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.opensearch.labels | {} | | This key allows you to set custom labels for the stateful deployment of opensearch. This is useful for organizing and selecting resources in your Kubernetes cluster. | | services.opensearch.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of opensearch. This is useful for adding metadata or configuration hints to your resources. | | env.opensearch\_remote\_url | | | Users can also decide to use the remote hosted service and link to Plane deployment. Set `services.opensearch.local_setup` to `false` and set this key with remote connection url. | | env.opensearch\_remote\_username | | | Username for remote OpenSearch service. Required when `services.opensearch.local_setup=false` and `env.opensearch_remote_url` is set. Note: This is not a secret and should be configured in values.yaml, not in external secrets. | | env.opensearch\_remote\_password | | | Password for remote OpenSearch service. Required when `services.opensearch.local_setup=false` and `env.opensearch_remote_url` is set. Can be configured in values.yaml or provided via external secrets (`opensearch_existingSecret` with `OPENSEARCH_PASSWORD`). **Password Complexity Requirements:** Must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one digit, and one special character. | | env.opensearch\_index\_prefix | plane\_ | | Prefix to be used for OpenSearch indices. This helps organize indices in a multi-tenant or multi-environment setup. | #### Doc Store (Minio/S3) Setup | Setting | Default | Required | Description | | ------------------------------------- | :----------------: | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.minio.local\_setup | true | | Plane uses `minio` as the default file storage drive. This storage can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws S3 or similar services). Set this to `true` when you choose to setup stateful deployment of `minio`. Mark it as `false` when using a remotely hosted database | | services.minio.image | minio/minio:latest | | Using this key, user must provide the docker image name to setup the stateful deployment of `minio`. (must be set when `services.minio.local_setup=true`) | | services.minio.image\_mc | minio/mc:latest | | Using this key, user must provide the docker image name to setup the job deployment of `minio client`. (must be set when `services.minio.local_setup=true`) | | services.minio.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of minio. (must be set when services.minio.local\_setup=true) | | services.minio.volumeSize | 3Gi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | services.minio.root\_user | admin | | Storage credentials are requried to access the hosted stateful deployment of `minio`. Use this key to set the username for the stateful deployment. | | services.minio.root\_password | password | | Storage credentials are requried to access the hosted stateful deployment of `minio`. Use this key to set the password for the stateful deployment. | | services.minio.env.minio\_endpoint\_ssl | false | | (Optional) Env to enforce HTTPS when connecting to minio uploads bucket | | env.docstore\_bucket | uploads | Yes | Storage bucket name is required as part of configuration. This is where files will be uploaded irrespective of if you are using `Minio` or external `S3` (or compatible) storage service | | env.doc\_upload\_size\_limit | 5242880 | Yes | Document Upload Size Limit (default to 5Mb) | | services.minio.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.minio.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of minio. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.minio.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of minio. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.minio.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of minio. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.minio.labels | {} | | This key allows you to set custom labels for the stateful deployment of minio. This is useful for organizing and selecting resources in your Kubernetes cluster. | | services.minio.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of minio. This is useful for adding metadata or configuration hints to your resources. | | env.aws\_access\_key | | | External `S3` (or compatible) storage service provides `access key` for the application to connect and do the necessary upload or download operations. To be provided when `services.minio.local_setup=false` | | env.aws\_secret\_access\_key | | | External `S3` (or compatible) storage service provides `secret access key` for the application to connect and do the necessary upload or download operations. To be provided when `services.minio.local_setup=false` | | env.aws\_region | | | External `S3` (or compatible) storage service providers creates any buckets in user selected region. This is also shared with the user as `region` for the application to connect and do the necessary upload or download operations. To be provided when `services.minio.local_setup=false` | | env.aws\_s3\_endpoint\_url | | | External `S3` (or compatible) storage service providers shares a `endpoint_url` for the integration purpose for the application to connect and do the necessary upload or download operations. To be provided when `services.minio.local_setup=false` | | env.use\_storage\_proxy | false | | When set to `true`, all S3 (or compatible) file GET requests from the browser are proxied through Plane's API service instead of accessing the S3 endpoint directly. Enable this if your storage endpoint is not accessible publicly or you want to control download access through the API. | #### Web Deployment | Setting | Default | Required | Description | | ------------------------------ | :------------------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | services.web.replicas | 1 | Yes | Kubernetes helps you with scaling up or down the deployments. You can run 1 or more pods for each deployment. This key helps you set up the number of replicas you want to run for this deployment. It must be >=1 | | services.web.memoryLimit | 1000Mi | | Every deployment in Kubernetes can be set to use the maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.web.cpuLimit | 500m | | Every deployment in Kubernetes can be set to use the maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.web.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.web.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.web.image | `makeplane/web-commercial` | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | services.web.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of web. | | services.web.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.web.nodeSelector | {} | | This key allows you to set the node selector for the deployment of web. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.web.tolerations | \[] | | This key allows you to set the tolerations for the deployment of web. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.web.affinity | {} | | This key allows you to set the affinity rules for the deployment of web. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.web.labels | {} | | Custom labels to add to the web deployment | | services.web.annotations | {} | | Custom annotations to add to the web deployment | #### Space Deployment | Setting | Default | Required | Description | | -------------------------------- | :--------------------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | services.space.replicas | 1 | Yes | Kubernetes helps you with scaling up or down the deployments. You can run 1 or more pods for each deployment. This key helps you set up the number of replicas you want to run for this deployment. It must be >=1 | | services.space.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use the maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.space.cpuLimit | 500m | | Every deployment in kubernetes can be set to use the maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.space.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.space.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.space.image | `makeplane/space-commercial` | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | services.space.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of space. | | services.space.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.space.nodeSelector | {} | | This key allows you to set the node selector for the deployment of space. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.space.tolerations | \[] | | This key allows you to set the tolerations for the deployment of space. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.space.affinity | {} | | This key allows you to set the affinity rules for the deployment of space. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.space.labels | {} | | Custom labels to add to the space deployment | | services.space.annotations | {} | | Custom annotations to add to the space deployment | #### Admin Deployment | Setting | Default | Required | Description | | -------------------------------- | :--------------------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | services.admin.replicas | 1 | Yes | Kubernetes helps you with scaling up or down the deployments. You can run 1 or more pods for each deployment. This key helps you set up the number of replicas you want to run for this deployment. It must be >=1 | | services.admin.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use the maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.admin.cpuLimit | 500m | | Every deployment in kubernetes can be set to use the maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.admin.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.admin.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.admin.image | `makeplane/admin-commercial` | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | services.admin.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of admin. | | services.admin.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.admin.nodeSelector | {} | | This key allows you to set the node selector for the deployment of admin. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.admin.tolerations | \[] | | This key allows you to set the tolerations for the deployment of admin. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.admin.affinity | {} | | This key allows you to set the affinity rules for the deployment of admin. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.admin.labels | {} | | Custom labels to add to the admin deployment. | | services.admin.annotations | {} | | Custom annotations to add to the admin deployment. | #### Live Service Deployment | Setting | Default | Required | Description | | ---------------------------------- | :--------------------------------: | :------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.live.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be >=1 | | services.live.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.live.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.live.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.live.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.live.image | `makeplane/live-commercial` | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | services.live.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of live. | | env.live\_sentry\_dsn | | | (optional) Live service deployment comes with some of the preconfigured integration. Sentry is one among those. Here user can set the Sentry provided DSN for this integration. | | env.live\_sentry\_environment | | | (optional) Live service deployment comes with some of the preconfigured integration. Sentry is one among those. Here user can set the Sentry environment name (as configured in Sentry) for this integration. | | env.live\_sentry\_traces\_sample\_rate | | | (optional) Live service deployment comes with some of the preconfigured integration. Sentry is one among those. Here user can set the Sentry trace sample rate (as configured in Sentry) for this integration. | | env.live\_server\_secret\_key | htbqvBJAgpm9bzvf3r4urJer0ENReatceh | | Live Server Secret Key | | env.external\_iframely\_url | "" | | External Iframely service URL. If provided, the local Iframely deployment will be skipped and the live service will use this external URL | | services.live.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service. | | services.live.nodeSelector | {} | | This key allows you to set the node selector for the deployment of live. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.live.tolerations | \[] | | This key allows you to set the tolerations for the deployment of live. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.live.affinity | {} | | This key allows you to set the affinity rules for the deployment of live. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.live.labels | {} | | Custom labels to add to the live deployment. | | services.live.annotations | {} | | Custom annotations to add to the live deployment. | #### Monitor Deployment | Setting | Default | Required | Description | | ---------------------------------- | :----------------------------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.monitor.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use the maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.monitor.cpuLimit | 500m | | Every deployment in kubernetes can be set to use the maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.monitor.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.monitor.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.monitor.image | `makeplane/monitor-commercial` | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | services.monitor.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of monitor. | | services.monitor.volumeSize | 100Mi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | services.monitor.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.monitor.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of monitor. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.monitor.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of monitor. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.monitor.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of monitor. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.monitor.labels | {} | | Custom labels to add to the monitor deployment | | services.monitor.annotations | {} | | Custom annotations to add to the monitor deployment | #### API Deployment | Setting | Default | Required | Description | | ------------------------------ | :----------------------------: | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.api.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you set up the number of replicas you want to run for this deployment. It must be >=1 | | services.api.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use the maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.api.cpuLimit | 500m | | Every deployment in kubernetes can be set to use the maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.api.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.api.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.api.image | `makeplane/backend-commercial` | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | services.api.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of api. | | env.sentry\_dsn | | | (optional) API service deployment comes with some of the preconfigured integration. Sentry is one among those. Here user can set the Sentry-provided DSN for this integration. | | env.sentry\_environment | | | (optional) API service deployment comes with some of the preconfigured integration. Sentry is one among those. Here user can set the Sentry environment name (as configured in Sentry) for this integration. | | env.api\_key\_rate\_limit | 60/minute | | (optional) User can set the maximum number of requests the API can handle in a given time frame. | | env.web\_url | | | (optional) Custom Web URL for the application. If not set, it will be auto-generated based on the license domain and SSL settings. | | services.api.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.api.nodeSelector | {} | | This key allows you to set the node selector for the deployment of api. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.api.tolerations | \[] | | This key allows you to set the tolerations for the deployment of api. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.api.affinity | {} | | This key allows you to set the affinity rules for the deployment of api. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.api.labels | {} | | Custom labels to add to the API deployment | | services.api.annotations | {} | | Custom annotations to add to the API deployment | #### Silo Deployment | Setting | Default | Required | Description | | :-------------------------------------------- | :--------------------------------- | :-------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.silo.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be >=1 | | services.silo.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.silo.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.silo.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.silo.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.silo.image | `makeplane/silo-commercial` | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | services.silo.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of `silo`. | | services.silo.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the service | | services.silo.nodeSelector | {} | | This key allows you to set the node selector for the deployment of silo. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.silo.tolerations | \[] | | This key allows you to set the tolerations for the deployment of silo. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.silo.affinity | {} | | This key allows you to set the affinity rules for the deployment of silo. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.silo.labels | {} | | Custom labels to add to the silo deployment | | services.silo.annotations | {} | | Custom annotations to add to the silo deployment | | services.silo.connectors.slack.enabled | false | | Slack Integration | | services.silo.connectors.slack.client\_id | "" | required if `services.silo.connectors.slack.enabled` is `true` | Slack Client ID | | services.silo.connectors.slack.client\_secret | "" | required if `services.silo.connectors.slack.enabled` is `true` | Slack Client Secret | | services.silo.connectors.github.enabled | false | | Github App Integration | | services.silo.connectors.github.client\_id | "" | required if `services.silo.connectors.github.enabled` is `true` | Github Client ID | | services.silo.connectors.github.client\_secret | "" | required if `services.silo.connectors.github.enabled` is `true` | Github Client Secret | | services.silo.connectors.github.app\_name | "" | required if `services.silo.connectors.github.enabled` is `true` | Github App Name | | services.silo.connectors.github.app\_id | "" | required if `services.silo.connectors.github.enabled` is `true` | Github App ID | | services.silo.connectors.github.private\_key | "" | required if `services.silo.connectors.github.enabled` is `true` | Github Private Key | | services.silo.connectors.gitlab.enabled | false | | Gitlab App Integration | | services.silo.connectors.gitlab.client\_id | "" | required if `services.silo.connectors.gitlab.enabled` is `true` | Gitlab Client ID | | services.silo.connectors.gitlab.client\_secret | "" | required if `services.silo.connectors.gitlab.enabled` is `true` | Gitlab Client Secret | | env.silo\_envs.mq\_prefetch\_count | 10 | | Prefetch count for RabbitMQ | | env.silo\_envs.batch\_size | 60 | | Batch size for Silo | | env.silo\_envs.request\_interval | 400 | | Request interval for Silo | | env.silo\_envs.sentry\_dsn | | | Sentry DSN | | env.silo\_envs.sentry\_environment | | | Sentry Environment | | env.silo\_envs.sentry\_traces\_sample\_rate | | | Sentry Traces Sample Rate | | env.silo\_envs.hmac\_secret\_key | \ | | HMAC Secret Key | | env.silo\_envs.aes\_secret\_key | "dsOdt7YrvxsTIFJ37pOaEVvLxN8KGBCr" | | AES Secret Key | #### Plane AI deployment ::: info Plane AI database Plane AI uses a separate PostgreSQL database. Create a new database (e.g. `plane_pi`) and connect it using `env.pg_pi_db_remote_url` in values, or **PLANE\_PI\_DATABASE\_URL** when using `pi_api_env_existingSecret`. ::: | Setting | Default | Required | Description | | --------------------------------- | :--------------------------------: | :------: | ---------------------------------------------------------------------------------------------------------------------------------------- | | services.pi.enabled | false | No | Set to `true` to enable the Plane AI service and its API, worker, beat, and migrator workloads. | | services.pi.replicas | 1 | Yes | Number of replicas for the Plane AI API deployment. It must be >=1. | | services.pi.memoryLimit | 1000Mi | | Memory limit for the Plane AI API deployment. | | services.pi.cpuLimit | 500m | | CPU limit for the Plane AI API deployment. | | services.pi.memoryRequest | 50Mi | | Memory request for the Plane AI API deployment. | | services.pi.cpuRequest | 50m | | CPU request for the Plane AI API deployment. | | services.pi.image | makeplane/plane-pi-commercial | | Docker image for the Plane AI service. | | services.pi.pullPolicy | Always | | Image pull policy for the Plane AI deployment. | | services.pi.assign\_cluster\_ip | false | | Set it to `true` if you want to assign `ClusterIP` to the Plane AI API service. | | services.pi.nodeSelector | {} | | Node selector for the Plane AI API deployment. | | services.pi.tolerations | \[] | | Tolerations for the Plane AI API deployment. | | services.pi.affinity | {} | | Affinity rules for the Plane AI API deployment. | | services.pi.labels | {} | | Custom labels to add to the Plane AI API deployment. | | services.pi.annotations | {} | | Custom annotations to add to the Plane AI API deployment. | | env.pg\_pi\_db\_name | plane\_pi | | PostgreSQL database name used by Plane AI when `postgres.local_setup=true`. | | env.pg\_pi\_db\_remote\_url | "" | | PostgreSQL connection URL for Plane AI when using a remote database. Required when `postgres.local_setup=false` and Plane AI is enabled. | | env.pi\_envs.follower\_postgres\_uri | Same as Plane DATABASE\_URL | No | Connection string for a Plane PostgreSQL DB read replica. Used for read-heavy operations to reduce load on the primary database. | | env.pi\_envs.internal\_secret | tyfvfqvBJAgpm9bzvf3r4urJer0Ehfdubk | | Internal secret used by Plane AI for OAuth and internal APIs. | | env.pi\_envs.plane\_api\_host | "" | | Override for the Plane API host URL used by Plane AI. Defaults to the license domain. | | env.pi\_envs.cors\_allowed\_origins | "" | | CORS allowed origins for Plane AI API. Defaults to the license domain. | #### Plane AI Worker Deployment | Setting | Default | Required | Description | | -------------------------------- | :-----: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | services.pi\_worker.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for the Plane AI worker. This key helps you set the number of replicas. It must be >=1. | | services.pi\_worker.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for the Plane AI worker deployment to use. | | services.pi\_worker.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for the Plane AI worker deployment to use. | | services.pi\_worker.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for the Plane AI worker deployment to use. | | services.pi\_worker.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for the Plane AI worker deployment to use. | | services.pi\_worker.nodeSelector | {} | | This key allows you to set the node selector for the deployment of `pi_worker`. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.pi\_worker.tolerations | \[] | | This key allows you to set the tolerations for the deployment of `pi_worker`. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.pi\_worker.affinity | {} | | This key allows you to set the affinity rules for the deployment of `pi_worker`. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.pi\_worker.labels | {} | | Custom labels to add to the Plane AI worker deployment | | services.pi\_worker.annotations | {} | | Custom annotations to add to the Plane AI worker deployment | #### Plane AI Beat-Worker Deployment | Setting | Default | Required | Description | | ------------------------------------- | :-----: | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.pi\_beat\_worker.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for the Plane AI beat-worker. This key helps you set the number of replicas. It must be >=1. | | services.pi\_beat\_worker.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for the Plane AI beat-worker deployment to use. | | services.pi\_beat\_worker.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for the Plane AI beat-worker deployment to use. | | services.pi\_beat\_worker.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for the Plane AI beat-worker deployment to use. | | services.pi\_beat\_worker.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for the Plane AI beat-worker deployment to use. | | services.pi\_beat\_worker.nodeSelector | {} | | This key allows you to set the node selector for the deployment of `pi_beat_worker`. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.pi\_beat\_worker.tolerations | \[] | | This key allows you to set the tolerations for the deployment of `pi_beat_worker`. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.pi\_beat\_worker.affinity | {} | | This key allows you to set the affinity rules for the deployment of `pi_beat_worker`. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.pi\_beat\_worker.labels | {} | | Custom labels to add to the Plane AI beat-worker deployment | | services.pi\_beat\_worker.annotations | {} | | Custom annotations to add to the Plane AI beat-worker deployment | #### Worker Deployment | Setting | Default | Required | Description | | ----------------------------- | :-----: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | services.worker.replicas | 1 | Yes | Kubernetes helps you with scaling up or down the deployments. You can run 1 or more pods for each deployment. This key helps you set up the number of replicas you want to run for this deployment. It must be >=1 | | services.worker.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use the maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.worker.cpuLimit | 500m | | Every deployment in kubernetes can be set to use the maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.worker.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.worker.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.worker.nodeSelector | {} | | This key allows you to set the node selector for the deployment of worker. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.worker.tolerations | \[] | | This key allows you to set the tolerations for the deployment of worker. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.worker.affinity | {} | | This key allows you to set the affinity rules for the deployment of worker. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.worker.labels | {} | | Custom labels to add to the worker deployment | | services.worker.annotations | {} | | Custom annotations to add to the worker deployment | #### Beat-Worker Deployment | Setting | Default | Required | Description | | --------------------------------- | :-----: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | services.beatworker.replicas | 1 | Yes | Kubernetes helps you with scaling up or down the deployments. You can run 1 or more pods for each deployment. This key helps you set up the number of replicas you want to run for this deployment. It must be >=1 | | services.beatworker.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use the maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | services.beatworker.cpuLimit | 500m | | Every deployment in kubernetes can be set to use the maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | services.beatworker.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | services.beatworker.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | services.beatworker.nodeSelector | {} | | This key allows you to set the node selector for the deployment of beatworker. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.beatworker.tolerations | \[] | | This key allows you to set the tolerations for the deployment of beatworker. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.beatworker.affinity | {} | | This key allows you to set the affinity rules for the deployment of beatworker. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.beatworker.labels | {} | | Custom labels to add to the beat-worker deployment | | services.beatworker.annotations | {} | | Custom annotations to add to the beat-worker deployment | #### Email Service Deployment | Setting | Default | Required | Description | | ------------------------------------ | -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.email\_service.enabled | false | | Set to `true` to enable the email service deployment | | services.email\_service.replicas | 1 | | Number of replicas for the email service deployment | | services.email\_service.memoryLimit | 1000Mi | | Memory limit for the email service deployment | | services.email\_service.cpuLimit | 500m | | CPU limit for the email service deployment | | services.email\_service.memoryRequest | 50Mi | | Memory request for the email service deployment | | services.email\_service.cpuRequest | 50m | | CPU request for the email service deployment | | services.email\_service.image | makeplane/email-commercial | | Docker image for the email service deployment | | services.email\_service.pullPolicy | Always | | Image pull policy for the email service deployment | | services.email\_service.nodeSelector | {} | | This key allows you to set the node selector for the deployment of `email_service`. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.email\_service.tolerations | \[] | | This key allows you to set the tolerations for the deployment of `email_service`. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.email\_service.affinity | {} | | This key allows you to set the affinity rules for the deployment of `email_service`. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.email\_service.labels | {} | | Custom labels to add to the email service deployment | | services.email\_service.annotations | {} | | Custom annotations to add to the email service deployment | | env.email\_service\_envs.smtp\_domain | | Yes | The SMTP Domain to be used with email service | ::: info When the email service is enabled, the cert-issuer will be automatically created to handle TLS certificates for the email service. ::: #### Outbox Poller Service Deployment | Setting | Default | Required | Description | | ------------------------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.outbox\_poller.enabled | false | | Set to true to enable the outbox poller service deployment | | services.outbox\_poller.replicas | 1 | | Number of replicas for the outbox poller service deployment | | services.outbox\_poller.memoryLimit | 1000Mi | | Memory limit for the outbox poller service deployment | | services.outbox\_poller.cpuLimit | 500m | | CPU limit for the outbox poller service deployment | | services.outbox\_poller.memoryRequest | 50Mi | | Memory request for the outbox poller service deployment | | services.outbox\_poller.cpuRequest | 50m | | CPU request for the outbox poller service deployment | | services.outbox\_poller.pullPolicy | Always | | Image pull policy for the outbox poller service deployment | | services.outbox\_poller.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | services.outbox\_poller.nodeSelector | {} | | This key allows you to set the node selector for the deployment of outbox\_poller. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.outbox\_poller.tolerations | \[] | | This key allows you to set the tolerations for the deployment of outbox\_poller. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.outbox\_poller.affinity | {} | | This key allows you to set the affinity rules for the deployment of outbox\_poller. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.outbox\_poller.labels | {} | | Custom labels to add to the outbox poller deployment | | services.outbox\_poller.annotations | {} | | Custom annotations to add to the outbox poller deployment | | env.outbox\_poller\_envs.memory\_limit\_mb | 400 | | Memory limit in MB for the outbox poller | | env.outbox\_poller\_envs.interval\_min | 0.25 | | Minimum interval in minutes for polling | | env.outbox\_poller\_envs.interval\_max | 2 | | Maximum interval in minutes for polling | | env.outbox\_poller\_envs.batch\_size | 250 | | Batch size for processing outbox messages | | env.outbox\_poller\_envs.memory\_check\_interval | 30 | | Memory check interval in seconds | | env.outbox\_poller\_envs.pool.size | 4 | | Pool size for database connections | | env.outbox\_poller\_envs.pool.min\_size | 2 | | Minimum pool size for database connections | | env.outbox\_poller\_envs.pool.max\_size | 10 | | Maximum pool size for database connections | | env.outbox\_poller\_envs.pool.timeout | 30.0 | | Pool timeout in seconds | | env.outbox\_poller\_envs.pool.max\_idle | 300.0 | | Maximum idle time for connections in seconds | | env.outbox\_poller\_envs.pool.max\_lifetime | 3600 | | Maximum lifetime for connections in seconds | | env.outbox\_poller\_envs.pool.reconnect\_timeout | 5.0 | | Reconnect timeout in seconds | | env.outbox\_poller\_envs.pool.health\_check\_interval | 60 | | Health check interval in seconds | #### Automation Consumer Deployment | Setting | Default | Required | Description | | ---------------------------------------------------- | -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.automation\_consumer.enabled | false | | Set to true to enable the automation consumer service deployment | | services.automation\_consumer.replicas | 1 | | Number of replicas for the automation consumer service deployment | | services.automation\_consumer.memoryLimit | 1000Mi | | Memory limit for the automation consumer service deployment | | services.automation\_consumer.cpuLimit | 500m | | CPU limit for the automation consumer service deployment | | services.automation\_consumer.memoryRequest | 50Mi | | Memory request for the automation consumer service deployment | | services.automation\_consumer.cpuRequest | 50m | | CPU request for the automation consumer service deployment | | services.automation\_consumer.pullPolicy | Always | | Image pull policy for the automation consumer service deployment | | services.automation\_consumer.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | services.automation\_consumer.nodeSelector | {} | | This key allows you to set the node selector for the deployment of automation\_consumer. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.automation\_consumer.tolerations | \[] | | This key allows you to set the tolerations for the deployment of automation\_consumer. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.automation\_consumer.affinity | {} | | This key allows you to set the affinity rules for the deployment of automation\_consumer. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.automation\_consumer.labels | {} | | Custom labels to add to the automation consumer deployment | | services.automation\_consumer.annotations | {} | | Custom annotations to add to the automation consumer deployment | | env.automation\_consumer\_envs.event\_stream\_queue\_name | "plane.event\_stream.automations" | | Event stream queue name for automations | | env.automation\_consumer\_envs.event\_stream\_prefetch | 10 | | Event stream prefetch count | | env.automation\_consumer\_envs.exchange\_name | "plane.event\_stream" | | Exchange name for event stream | | env.automation\_consumer\_envs.event\_types | "issue" | | Event types to process | #### Iframely Deployment | Setting | Default | Required | Description | | ----------------------------------- | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | services.iframely.enabled | false | | Set to true to enable the Iframely service deployment | | services.iframely.replicas | 1 | | Number of replicas for the Iframely service deployment | | services.iframely.memoryLimit | 1000Mi | | Memory limit for the Iframely service deployment | | services.iframely.cpuLimit | 500m | | CPU limit for the Iframely service deployment | | services.iframely.memoryRequest | 50Mi | | Memory request for the Iframely service deployment | | services.iframely.cpuRequest | 50m | | CPU request for the Iframely service deployment | | services.iframely.image | makeplane/iframely:v1.2.0 | | Docker image for the Iframely service deployment | | services.iframely.pullPolicy | Always | | Image pull policy for the Iframely service deployment | | services.iframely.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | services.iframely.nodeSelector | {} | | This key allows you to set the node selector for the deployment of iframely. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | services.iframely.tolerations | \[] | | This key allows you to set the tolerations for the deployment of iframely. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | services.iframely.affinity | {} | | This key allows you to set the affinity rules for the deployment of iframely. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | services.iframely.labels | {} | | Custom labels to add to the iframely deployment | | services.iframely.annotations | {} | | Custom annotations to add to the iframely deployment | #### External Secrets Config To configure the external secrets for your application, you need to define specific environment variables for each secret category. Below is a list of the required secrets and their respective environment variables. | Secret Name | Env Var Name | Required | Description | Example Value | | ------------------------- | --------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rabbitmq\_existingSecret | RABBITMQ\_DEFAULT\_USER | Required if `rabbitmq.local_setup=true` | The default RabbitMQ user | plane | | | RABBITMQ\_DEFAULT\_PASS | Required if `rabbitmq.local_setup=true` | The default RabbitMQ password | plane | | pgdb\_existingSecret | POSTGRES\_PASSWORD | Required if `postgres.local_setup=true` | Password for PostgreSQL database | plane | | | POSTGRES\_DB | Required if `postgres.local_setup=true` | Name of the PostgreSQL database | plane | | | POSTGRES\_USER | Required if `postgres.local_setup=true` | PostgreSQL user | plane | | opensearch\_existingSecret | OPENSEARCH\_ENABLED | Yes | Flag to enable OpenSearch | 1 (enabled) or 0 (disabled) | | | OPENSEARCH\_URL | Required if OpenSearch is enabled | OpenSearch connection URL | **k8s service example:** `http://plane-opensearch.plane-ns.svc.cluster.local:9200` **external service example:** `https://your-opensearch-host:9200` | | | OPENSEARCH\_USERNAME | Required if OpenSearch is enabled | Username for OpenSearch | **local setup:** plane **remote setup:** your\_remote\_username | | | OPENSEARCH\_PASSWORD | Required if OpenSearch is enabled | Password for OpenSearch | **local setup:** Secure@Pass#123!%^&\* **remote setup:** your\_remote\_password | | | OPENSEARCH\_INITIAL\_ADMIN\_PASSWORD | Required if `opensearch.local_setup=true` | Initial admin password for local OpenSearch | Secure@Pass#123!%^&\* | | | OPENSEARCH\_INDEX\_PREFIX | Optional | Prefix for OpenSearch indices | plane\_ | | doc\_store\_existingSecret | USE\_MINIO | Yes | Flag to enable MinIO as the storage backend | 1 | | | MINIO\_ROOT\_USER | Yes | MinIO root user | admin | | | MINIO\_ROOT\_PASSWORD | Yes | MinIO root password | password | | | AWS\_ACCESS\_KEY\_ID | Yes | AWS Access Key ID | your\_aws\_key | | | AWS\_SECRET\_ACCESS\_KEY | Yes | AWS Secret Access Key | your\_aws\_secret | | | AWS\_S3\_BUCKET\_NAME | Yes | AWS S3 Bucket Name | your\_bucket\_name | | | AWS\_S3\_ENDPOINT\_URL | Yes | Endpoint URL for AWS S3 or MinIO | `http://plane-minio.plane-ns.svc.cluster.local:9000` | | | AWS\_REGION | Optional | AWS region where your S3 bucket is located | your\_aws\_region | | | FILE\_SIZE\_LIMIT | Yes | Limit for file uploads in your system | 5MB | | app\_env\_existingSecret | SECRET\_KEY | Yes | Random secret key | 60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5 | | | REDIS\_URL | Yes | Redis URL | `redis://plane-redis.plane-ns.svc.cluster.local:6379/` | | | DATABASE\_URL | Yes | PostgreSQL connection URL | k8s service example: `postgresql://plane:plane@plane-pgdb.plane-ns.svc.cluster.local:5432/plane` external service example: `postgresql://username:password@your-db-host:5432/plane` | | | AMQP\_URL | Yes | RabbitMQ connection URL | k8s service example: `amqp://plane:plane@plane-rabbitmq.plane-ns.svc.cluster.local:5672/` external service example: `amqp://username:password@your-rabbitmq-host:5672/` | | live\_env\_existingSecret | REDIS\_URL | Yes | Redis URL | `redis://plane-redis.plane-ns.svc.cluster.local:6379/` | | silo\_env\_existingSecret | SILO\_HMAC\_SECRET\_KEY | Yes | Silo HMAC secret Key | `` | | | REDIS\_URL | Yes | Redis URL | redis://plane-redis.plane-ns.svc.cluster.local:6379/ | | | DATABASE\_URL | Yes | PostgreSQL connection URL | k8s service example: postgresql://plane:plane@plane-pgdb.plane-ns.svc.cluster.local:5432/plane external service example: postgresql://username:password@your-db-host:5432/plane | | | AMQP\_URL | Yes | RabbitMQ connection URL | k8s service example: amqp://plane:plane@plane-rabbitmq.plane-ns.svc.cluster.local:5672/ external service example: amqp://username:password@your-rabbitmq-host:5672/ | | | GITHUB\_APP\_NAME | Required if `services.silo.connectors.github.enabled` is true | GitHub app name | your\_github\_app\_name | | | GITHUB\_APP\_ID | Required if `services.silo.connectors.github.enabled` is true | GitHub app ID | your\_github\_app\_id | | | GITHUB\_CLIENT\_ID | Required if `services.silo.connectors.github.enabled` is true | GitHub client ID | your\_github\_client\_id | | | GITHUB\_CLIENT\_SECRET | Required if `services.silo.connectors.github.enabled` is true | GitHub client secret key | your\_github\_client\_secret\_key | | | GITHUB\_PRIVATE\_KEY | Required if `services.silo.connectors.github.enabled` is true | GitHub private key | your\_github\_private\_key | | | SLACK\_CLIENT\_ID | Required if `services.silo.connectors.slack.enabled` is true | Slack client ID | your\_slack\_client\_id | | | SLACK\_CLIENT\_SECRET | Required if `services.silo.connectors.slack.enabled` is true | Slack client secret key | your\_slack\_client\_secret\_key | | | GITLAB\_CLIENT\_ID | Required if `services.silo.connectors.gitlab.enabled` is true | GitLab client ID | your\_gitlab\_client\_id | | | GITLAB\_CLIENT\_SECRET | Required if `services.silo.connectors.gitlab.enabled` is true | GitLab client secret key | your\_gitlab\_client\_secret\_key | | pi\_api\_env\_existingSecret | PLANE\_PI\_DATABASE\_URL | Required if `services.pi.enabled=true` | PostgreSQL connection URL for Plane AI database | **k8s service example**: `postgresql://plane:plane@plane-pgdb.plane-ns.svc.cluster.local/plane_pi` **external**: `postgresql://username:password@your-db-host:5432/plane_pi` | | | FOLLOWER\_POSTGRES\_URI | No | Connection string for a PostgreSQL read replica | Same as DATABASE\_URL. Used for read-heavy operations to reduce load on the primary database. **k8s**: `postgresql://plane:plane@plane-pgdb.plane-ns.svc.cluster.local:5432/plane` | | | AMQP\_URL | Required if `services.pi.enabled=true` | RabbitMQ connection URL | **k8s service example**: `amqp://plane:plane@plane-rabbitmq.plane-ns.svc.cluster.local:5672/` **external**: `amqp://username:password@your-rabbitmq-host:5672/` | | | AES\_SECRET\_KEY | Required if `services.pi.enabled=true` | AES secret key for Plane AI | dsOdt7YrvxsTIFJ37pOaEVvLxN8KGBCr (or your own value) | | | OPENAI\_API\_KEY | required if `services.pi.ai_providers.openai.enabled` is true | OpenAI API key | your\_openai\_api\_key | | | CLAUDE\_API\_KEY | required if `services.pi.ai_providers.claude.enabled` is true | Claude API key | your\_claude\_api\_key | | | GROQ\_API\_KEY | required if `services.pi.ai_providers.groq.enabled` is true | Groq API key | your\_groq\_api\_key | | | COHERE\_API\_KEY | required if `services.pi.ai_providers.cohere.enabled` is true | Cohere API key | your\_cohere\_api\_key | | | CUSTOM\_LLM\_API\_KEY | required if `services.pi.ai_providers.custom_llm.enabled` is true | Custom LLM API key | your\_custom\_llm\_api\_key | | | BR\_AWS\_SECRET\_ACCESS\_KEY | required if `services.pi.ai_providers.embedding_model.enabled` is true | AWS secret for embedding model | your\_aws\_secret\_access\_key | | | BR\_AWS\_SESSION\_TOKEN | required if embedding model uses temporary credentials | AWS session token for embedding model | your\_aws\_session\_token | #### Ingress and SSL Setup | Setting | Default | Required | Description | | --------------------------- | --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ingress.enabled | true | | Ingress setup in kubernetes is a common practice to expose application to the intended audience. Set it to false if you are using external ingress providers like Cloudflare | | ingress.minioHost | | | Based on above configuration, if you want to expose the minio web console to set of users, use this key to set the host mapping or leave it as EMPTY to not expose interface. | | ingress.rabbitmqHost | | | Based on above configuration, if you want to expose the rabbitmq web console to set of users, use this key to set the host mapping or leave it as EMPTY to not expose interface. | | ingress.ingressClass | nginx | Yes | Kubernetes cluster setup comes with various options of ingressClass. Based on your setup, set this value to the right one (eg. nginx, traefik, etc). Leave it to default in case you are using external ingress provider. | | ingress.ingress\_annotations | { `"nginx.ingress.kubernetes.io/proxy-body-size": "5m"` } | | Ingress controllers comes with various configuration options which can be passed as annotations. Setting this value lets you change the default value to user required. | | ssl.createIssuer | false | | Kubernets cluster setup supports creating issuer type resource. After deployment, this is step towards creating secure access to the ingress url. Issuer is required for you generate SSL certifiate. Kubernetes can be configured to use any of the certificate authority to generate SSL (depending on CertManager configuration). Set it to true to create the issuer. Applicable only when ingress.enabled=true | | ssl.issuer | http | | CertManager configuration allows user to create issuers using http or any of the other DNS Providers like cloudflare, digitalocean, etc. As of now Plane supports http, cloudflare, digitalocean | | ssl.token | | | To create issuers using DNS challenge, set the issuer api token of dns provider like cloudflare or digitalocean (not required for http) | | ssl.server | https://acme-v02.api.letsencrypt.org/directory | | Issuer creation configuration need the certificate generation authority server url. Default URL is the Let's Encrypt server | | ssl.email | plane@example.com | | Certificate generation authority needs a valid email id before generating certificate. Required when ssl.createIssuer=true | | ssl.generateCerts | false | | After creating the issuers, user can still not create the certificate untill sure of configuration. Setting this to true will try to generate SSL certificate and associate with ingress. Applicable only when ingress.enabled=true and ssl.createIssuer=true | | ssl.tls\_secret\_name | | | If you have a custom TLS secret name, set this to the name of the secret. Applicable only when ingress.enabled=true and ssl.createIssuer=false | #### Common Environment Settings | Setting | Default | Required | Description | | ---------------- | :------------------------------------------------: | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | env.storageClass | longhorn | | Creating the persitant volumes for the stateful deployments needs the `storageClass` name. Set the correct value as per your kubernetes cluster configuration. | | env.secret\_key | 60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5 | Yes | This must be a random string which is used for hashing/encrypting the sensitive data within the application. Once set, changing this might impact the already hashed/encrypted data | #### Extra Environment Variables | Setting | Default | Required | Description | | -------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | extraEnv | \[] | No | Global extra environment variables that will be applied to all workloads. This allows you to add custom environment variables to all deployments (web, api, worker, etc.). Useful for proxy settings, custom configurations, or any environment-specific variables. Some example variables are HTTP\_PROXY, HTTPS\_PROXY, NO\_PROXY. | ## Custom Ingress Routes If you are planning to use 3rd party ingress providers, here is the available route configuration. | Host | Path | Service | Required | | ----------------------- | :-------------: | --------------------------------------- | :-------------------------------------------------------------------------- | | plane.example.com | / | | Yes | | plane.example.com | /spaces/\* | | Yes | | plane.example.com | /god-mode/\* | | Yes | | plane.example.com | /live/\* | | Yes | | plane.example.com | /silo/\* | | Yes (if `services.silo.enabled=true` ) | | plane.example.com | /pi/\* | | Yes (if `services.pi.enabled=true`) | | plane.example.com | /api/\* | | Yes | | plane.example.com | /auth/\* | | Yes | | plane.example.com | /graphql/\* | | Yes | | plane.example.com | /marketplace/\* | | Yes | | plane.example.com | /uploads/\* | | Yes (Only if using local setup) | | plane-minio.example.com | / | | (Optional) if using local setup, this will enable minio console access | | plane-mq.example.com | / | | (Optional) if using local setup, this will enable management console access | ::: details Install Community Edition The Commercial edition comes with a free plan and the flexibility to upgrade to a paid plan at any point. If you still want to install the Community edition, follow the steps below: #### Prerequisites * A working Kubernetes cluster * `kubectl` and `helm` on the client system that you will use to install our Helm charts #### Installation 1. Open Terminal or any other command-line app that has access to Kubernetes tools on your local system. 2. Add the Helm Repo ```bash helm repo add makeplane https://helm.plane.so/ helm repo update ``` 3. Use one of the following ways to deploy Plane: - **Quick setup** This is the fastest way to deploy Plane with default settings. This will create stateful deployments for Postgres, Redis, and Minio with a persistent volume claim using the `longhorn` storage class. This also sets up the ingress routes for you using `nginx` ingress class. ::: tip To customize this, see `Custom ingress routes` below. ::: Continue to be on the same Terminal window as you have so far, copy the code below, and paste it on your Terminal screen. ```bash helm install plane-app makeplane/plane-ce \ --create-namespace \ --namespace plane-ce \ --set planeVersion=stable \ --set ingress.appHost="plane.example.com" \ --set ingress.minioHost="plane-minio.example.com" \ --set ingress.ingressClass=nginx \ --set postgres.storageClass=longhorn \ --set redis.storageClass=longhorn \ --set minio.storageClass=longhorn \ --timeout 10m \ --wait \ --wait-for-jobs ``` ::: tip This is the minimum required to set up Plane-CE. You can change the default namespace from `plane-ce`, the default app name from `plane-app`, the default storage class from `[postgres, redis, minio].storageClass`, and the default ingress class from `ingress.ingressClass` to whatever you would like to. You can also pass other settings referring to `Configuration Settings` section. ::: * **Advanced setup** For more control over your set-up, run the script below to download the `values.yaml` file and and edit using any editor like Vim or Nano. ```bash helm show values makeplane/plane-ce > values.yaml vi values.yaml ``` ::: tip See **Configuration settings** below for more details. ::: After saving the `values.yaml` file, continue to be on the same Terminal window as on the previous steps, copy the code below, and paste it on your Terminal screen. ```bash helm install plane-app makeplane/plane-ce \ --create-namespace \ --namespace plane-ce \ -f values.yaml \ --timeout 10m \ --wait \ --wait-for-jobs ``` #### Configuration settings ##### Plane Version | Setting | Default | Required | Description | | ------------ | ------- | -------- | ----------- | | planeVersion | v1.1.0 | Yes | | ##### Postgres DB Setup | Setting | Default | Required | Description | | -------------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | postgres.local\_setup | true | | Plane uses postgres as the primary database to store all the transactional data. This database can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws rds or similar services). Set this to true when you choose to setup stateful deployment of postgres. Mark it as false when using a remotely hosted database | | postgres.image | postgres:15.7-alpine | | Using this key, user must provide the docker image name to setup the stateful deployment of postgres. (must be set when `postgres.local_setup=true`) | | postgres.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of postgres. (must be set when `postgres.local_setup=true`) | | postgres.servicePort | 5432 | | This key sets the default port number to be used while setting up stateful deployment of postgres. | | postgres.volumeSize | 5Gi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | env.pgdb\_username | plane | | Database credentials are requried to access the hosted stateful deployment of postgres. Use this key to set the username for the stateful deployment. | | env.pgdb\_password | plane | | Database credentials are requried to access the hosted stateful deployment of postgres. Use this key to set the password for the stateful deployment. | | env.pgdb\_name | plane | | Database name to be used while setting up stateful deployment of Postgres | | env.pgdb\_remote\_url | | | Users can also decide to use the remote hosted database and link to Plane deployment. Ignoring all the above keys, set postgres.local\_setup to false and set this key with remote connection url. | | postgres.storageClass | `` | | Creating the persitant volumes for the stateful deployments needs the storageClass name. Set the correct value as per your kubernetes cluster configuration. | | postgres.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | postgres.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of postgres. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | postgres.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of postgres. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | postgres.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of postgres. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | postgres.labels | {} | | This key allows you to set custom labels for the stateful deployment of postgres. This is useful for organizing and selecting resources in your Kubernetes cluster. | | postgres.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of postgres. This is useful for adding metadata or configuration hints to your resources. | ##### Redis/Valkey Setup | Setting | Default | Required | Description | | ----------------------- | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | redis.local\_setup | true | | Plane uses redis to cache the session authentication and other static data. This database can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws rds or similar services). Set this to true when you choose to setup stateful deployment of redis. Mark it as false when using a remotely hosted database | | redis.image | `valkey/valkey:7.2.5-alpine` | | Using this key, user must provide the docker image name to setup the stateful deployment of redis. (must be set when redis.local\_setup=true) | | redis.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of redis. (must be set when `redis.local_setup=true`) | | redis.servicePort | 6379 | | This key sets the default port number to be used while setting up stateful deployment of redis. | | redis.volumeSize | 1Gi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | env.remote\_redis\_url | | | Users can also decide to use the remote hosted database and link to Plane deployment. Ignoring all the above keys, set redis.local\_setup to false and set this key with remote connection url. | | redis.storageClass | `` | | Creating the persitant volumes for the stateful deployments needs the storageClass name. Set the correct value as per your kubernetes cluster configuration. | | redis.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | redis.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of redis. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | redis.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of redis. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | redis.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of redis. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | redis.labels | {} | | This key allows you to set custom labels for the stateful deployment of redis. This is useful for organizing and selecting resources in your Kubernetes cluster. | | redis.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of redis. This is useful for adding metadata or configuration hints to your resources. | ##### RabbitMQ Setup | Setting | Default | Required | Description | | ------------------------------ | --------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rabbitmq.local\_setup | true | | Plane uses rabbitmq as message queuing system. This can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws mq or similar services). Set this to true when you choose to setup stateful deployment of rabbitmq. Mark it as false when using a remotely hosted service | | rabbitmq.image | rabbitmq:3.13.6-management-alpine | | Using this key, user must provide the docker image name to setup the stateful deployment of rabbitmq. (must be set when `rabbitmq.local_setup=true`) | | rabbitmq.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of rabbitmq. (must be set when `rabbitmq.local_setup=true`) | | rabbitmq.servicePort | 5672 | | This key sets the default port number to be used while setting up stateful deployment of rabbitmq. | | rabbitmq.managementPort | 15672 | | This key sets the default management port number to be used while setting up stateful deployment of rabbitmq. | | rabbitmq.volumeSize | 100Mi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | rabbitmq.storageClass | `` | | Creating the persitant volumes for the stateful deployments needs the storageClass name. Set the correct value as per your kubernetes cluster configuration. | | rabbitmq.default\_user | plane | | Credentials are requried to access the hosted stateful deployment of rabbitmq. Use this key to set the username for the stateful deployment. | | rabbitmq.default\_password | plane | | Credentials are requried to access the hosted stateful deployment of rabbitmq. Use this key to set the password for the stateful deployment. | | rabbitmq.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | rabbitmq.external\_rabbitmq\_url | | | Users can also decide to use the remote hosted service and link to Plane deployment. Ignoring all the above keys, set rabbitmq.local\_setup to false and set this key with remote connection url. | | rabbitmq.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of rabbitmq. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | rabbitmq.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of rabbitmq. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | rabbitmq.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of rabbitmq. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | rabbitmq.labels | {} | | This key allows you to set custom labels for the stateful deployment of rabbitmq. This is useful for organizing and selecting resources in your Kubernetes cluster. | | rabbitmq.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of rabbitmq. This is useful for adding metadata or configuration hints to your resources. | ##### Doc Store (Minio/S3) Setup ::: tip On Amazon EKS you can skip the access key and secret entirely and let Plane assume an IAM role instead. See [Use IRSA or EKS Pod Identity for S3 storage](/self-hosting/govern/aws-workload-identity). ::: | Setting | Default | Required | Description | | ---------------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | minio.local\_setup | true | | Plane uses minio as the default file storage drive. This storage can be hosted within kubernetes as part of helm chart deployment or can be used as hosted service remotely (e.g. aws S3 or similar services). Set this to true when you choose to setup stateful deployment of postgres. Mark it as false when using a remotely hosted database | | minio.image | minio/minio:latest | | Using this key, user must provide the docker image name to setup the stateful deployment of minio. (must be set when `minio.local_setup=true`) | | minio.image\_mc | minio/mc:latest | | Using this key, user must provide the docker image name to setup the job deployment of minio client. (must be set when `minio.local_setup=true`) | | minio.pullPolicy | IfNotPresent | | Using this key, user can set the pull policy for the stateful deployment of minio. (must be set when `minio.local_setup=true`) | | minio.volumeSize | 5Gi | | While setting up the stateful deployment, while creating the persistant volume, volume allocation size need to be provided. This key helps you set the volume allocation size. Unit of this value must be in Mi (megabyte) or Gi (gigabyte) | | minio.root\_user | admin | | Storage credentials are requried to access the hosted stateful deployment of minio. Use this key to set the username for the stateful deployment. | | minio.root\_password | password | | Storage credentials are requried to access the hosted stateful deployment of minio. Use this key to set the password for the stateful deployment. | | minio.env.minio\_endpoint\_ssl | false | | (Optional) Env to enforce HTTPS when connecting to minio uploads bucket | | env.docstore\_bucket | uploads | Yes | Storage bucket name is required as part of configuration. This is where files will be uploaded irrespective of if you are using Minio or external S3 (or compatible) storage service | | env.doc\_upload\_size\_limit | 5242880 | Yes | Document Upload Size Limit (default to 5Mb) | | env.aws\_access\_key | | | External S3 (or compatible) storage service provides access key for the application to connect and do the necessary upload/download operations. To be provided when `minio.local_setup=false` | | env.aws\_secret\_access\_key | | | External S3 (or compatible) storage service provides secret access key for the application to connect and do the necessary upload/download operations. To be provided when `minio.local_setup=false` | | env.aws\_region | | | External S3 (or compatible) storage service providers creates any buckets in user selected region. This is also shared with the user as region for the application to connect and do the necessary upload/download operations. To be provided when `minio.local_setup=false` | | env.aws\_s3\_endpoint\_url | | | External S3 (or compatible) storage service providers shares a endpoint\_url for the integration purpose for the application to connect and do the necessary upload/download operations. To be provided when minio.`local_setup=false` | | minio.storageClass | `` | | Creating the persitant volumes for the stateful deployments needs the storageClass name. Set the correct value as per your kubernetes cluster configuration. | | minio.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | minio.nodeSelector | {} | | This key allows you to set the node selector for the stateful deployment of minio. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | minio.tolerations | \[] | | This key allows you to set the tolerations for the stateful deployment of minio. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | minio.affinity | {} | | This key allows you to set the affinity rules for the stateful deployment of minio. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | minio.labels | {} | | This key allows you to set custom labels for the stateful deployment of minio. This is useful for organizing and selecting resources in your Kubernetes cluster. | | minio.annotations | {} | | This key allows you to set custom annotations for the stateful deployment of minio. This is useful for adding metadata or configuration hints to your resources. | ##### Service Account and Cloud Identity Available in `plane-ce` chart version 1.8.0 and later. Every workload runs under this one ServiceAccount, so annotating it grants the identity to the datastores too. See [Use IRSA or EKS Pod Identity for S3 storage](/self-hosting/govern/aws-workload-identity). | Setting | Default | Required | Description | | ---------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | serviceAccount.create | true | | Set to false to run as a ServiceAccount that is managed outside this chart. | | serviceAccount.name | | | Name of the ServiceAccount every workload runs as. Defaults to `-srv-account`. | | serviceAccount.annotations | {} | | Annotations on the ServiceAccount. This is where a cloud workload-identity binding goes, such as `eks.amazonaws.com/role-arn` for IRSA. Helm values only, because this is a map. | | serviceAccount.podLabels | {} | | Extra pod-template labels. Not needed for IRSA or EKS Pod Identity; Azure Workload Identity requires one. | | serviceAccount.cloudIdentity | false | | Advisory only and changes nothing in the rendered output. Declares that this ServiceAccount is bound to a cloud identity configured out of band, which enables related warnings. | ##### Web Deployment | Setting | Default | Required | Description | | --------------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | web.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be `>=1` | | web.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | web.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | web.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | web.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | web.image | makeplane/plane-frontend | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | web.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of web. | | web.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | web.nodeSelector | {} | | This key allows you to set the node selector for the deployment of web. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | web.tolerations | \[] | | This key allows you to set the tolerations for the deployment of web. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | web.affinity | {} | | This key allows you to set the affinity rules for the deployment of web. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | web.labels | {} | | Custom labels to add to the web deployment | | web.annotations | {} | | Custom annotations to add to the web deployment | ##### Space Deployment | Setting | Default | Required | Description | | ----------------------- | --------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | space.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be `>=1` | | space.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | space.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | space.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | space.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | space.image | makeplane/plane-space | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | space.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of space. | | space.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | space.nodeSelector | {} | | This key allows you to set the node selector for the deployment of space. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | space.tolerations | \[] | | This key allows you to set the tolerations for the deployment of space. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | space.affinity | {} | | This key allows you to set the affinity rules for the deployment of space. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | space.labels | {} | | Custom labels to add to the space deployment | | space.annotations | {} | | Custom annotations to add to the space deployment | ##### Admin Deployment | Setting | Default | Required | Description | | ----------------------- | --------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | admin.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be `>=1` | | admin.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | admin.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | admin.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | admin.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | admin.image | makeplane/plane-admin | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | admin.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of admin. | | admin.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | admin.nodeSelector | {} | | This key allows you to set the node selector for the deployment of admin. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | admin.tolerations | \[] | | This key allows you to set the tolerations for the deployment of admin. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | admin.affinity | {} | | This key allows you to set the affinity rules for the deployment of admin. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | admin.labels | {} | | Custom labels to add to the admin deployment | | admin.annotations | {} | | Custom annotations to add to the admin deployment | ##### Live Service Deployment | Setting | Default | Required | Description | | ---------------------- | ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | live.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be `>=1` | | live.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | live.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | live.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | live.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | live.image | makeplane/plane-live | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | live.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of live. | | live.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | live.nodeSelector | {} | | This key allows you to set the node selector for the deployment of live. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | live.tolerations | \[] | | This key allows you to set the tolerations for the deployment of live. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | live.affinity | {} | | This key allows you to set the affinity rules for the deployment of live. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | live\_server\_secret\_key | htbqvBJAgpm9bzvf3r4urJer0ENReatceh | Yes | This key sets the secret key for the live server. This is required for secure communication and authentication in the live server component. | | live.labels | {} | | Custom labels to add to the live deployment | | live.annotations | {} | | Custom annotations to add to the live deployment | ##### API Deployment | Setting | Default | Required | Description | | ---------------------- | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | api.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be `>=1` | | api.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | api.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | api.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | api.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | api.image | makeplane/plane-backend | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | api.pullPolicy | Always | | Using this key, user can set the pull policy for the deployment of api. | | env.sentry\_dsn | | | (optional) API service deployment comes with some of the preconfigured integration. Sentry is one among those. Here user can set the Sentry provided DSN for this integration. | | env.sentry\_environment | | | (optional) API service deployment comes with some of the preconfigured integration. Sentry is one among those. Here user can set the Sentry environment name (as configured in Sentry) for this integration. | | env.api\_key\_rate\_limit | 60/minute | | (optional) User can set the maximum number of requests the API can handle in a given time frame. | | api.assign\_cluster\_ip | false | | Set it to true if you want to assign ClusterIP to the service | | api.nodeSelector | {} | | This key allows you to set the node selector for the deployment of api. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | api.tolerations | \[] | | This key allows you to set the tolerations for the deployment of api. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | api.affinity | {} | | This key allows you to set the affinity rules for the deployment of api. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | api.labels | {} | | Custom labels to add to the API deployment | | api.annotations | {} | | Custom annotations to add to the API deployment | ##### Worker Deployment | Setting | Default | Required | Description | | -------------------- | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | worker.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be `>=1` | | worker.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | worker.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | worker.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | worker.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | worker.image | makeplane/plane-backend | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | worker.nodeSelector | {} | | This key allows you to set the node selector for the deployment of worker. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | worker.tolerations | \[] | | This key allows you to set the tolerations for the deployment of worker. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | worker.affinity | {} | | This key allows you to set the affinity rules for the deployment of worker. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | worker.labels | {} | | Custom labels to add to the worker deployment | | worker.annotations | {} | | Custom annotations to add to the worker deployment | ##### Beat-Worker Deployment | Setting | Default | Required | Description | | ------------------------ | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | beatworker.replicas | 1 | Yes | Kubernetes helps you with scaling up/down the deployments. You can run 1 or more pods for each deployment. This key helps you setting up number of replicas you want to run for this deployment. It must be `>=1` | | beatworker.memoryLimit | 1000Mi | | Every deployment in kubernetes can be set to use maximum memory they are allowed to use. This key sets the memory limit for this deployment to use. | | beatworker.cpuLimit | 500m | | Every deployment in kubernetes can be set to use maximum cpu they are allowed to use. This key sets the cpu limit for this deployment to use. | | beatworker.memoryRequest | 50Mi | | Every deployment in kubernetes can be set to use minimum memory they are allowed to use. This key sets the memory request for this deployment to use. | | beatworker.cpuRequest | 50m | | Every deployment in kubernetes can be set to use minimum cpu they are allowed to use. This key sets the cpu request for this deployment to use. | | beatworker.image | makeplane/plane-backend | | This deployment needs a preconfigured docker image to function. Docker image name is provided by the owner and must not be changed for this deployment | | beatworker.nodeSelector | {} | | This key allows you to set the node selector for the deployment of beatworker. This is useful when you want to run the deployment on specific nodes in your Kubernetes cluster. | | beatworker.tolerations | \[] | | This key allows you to set the tolerations for the deployment of beatworker. This is useful when you want to run the deployment on nodes with specific taints in your Kubernetes cluster. | | beatworker.affinity | {} | | This key allows you to set the affinity rules for the deployment of beatworker. This is useful when you want to control how pods are scheduled on nodes in your Kubernetes cluster. | | beatworker.labels | {} | | Custom labels to add to the beat-worker deployment | | beatworker.annotations | {} | | Custom annotations to add to the beat-worker deployment | ##### Common Environment Settings | Setting | Default | Required | Description | | -------------------------- | -------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | env.secret\_key | 60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5 | Yes | This must a random string which is used for hashing/encrypting the sensitive data within the application. Once set, changing this might impact the already hashed/encrypted data | | env.default\_cluster\_domain | cluster.local | Yes | Set this value as configured in your kubernetes cluster. cluster.local is usally the default in most cases. | --- --- url: 'https://developers.plane.so/self-hosting/govern/high-availability.html' description: >- How to deploy Plane Commercial Edition on Kubernetes with high availability using the plane-enterprise Helm chart. --- # High Availability on Kubernetes This guide covers what high availability means, how the `plane-enterprise` Helm chart workloads behave under failure, and exactly what to configure so your deployment survives the loss of a single availability zone or node without manual recovery. The setup is cloud-agnostic. If you're deploying on AWS with Karpenter, there's a dedicated section for you. Read this alongside the chart's [README](https://github.com/makeplane/helm-charts/blob/master/charts/plane-enterprise/README.md) and [values.yaml](https://github.com/makeplane/helm-charts/blob/master/charts/plane-enterprise/values.yaml). ## What HA means here Plane Commercial Edition is a single-region application. There's one primary Postgres, one Redis, one message queue, one search cluster. High availability here means Plane keeps serving traffic when **one AZ or one node disappears**, not that you can run two independent active-active regions. That's an important distinction for how you plan your infrastructure. You're engineering for node and AZ fault tolerance, not geographic redundancy. The playbook: run stateless workloads with multiple replicas spread across AZs, and replace every in-chart stateful service with a managed, multi-AZ equivalent. ## Workload tiers Every workload in the chart falls into one of three tiers. The tier determines how you scale it, how it recovers from failure, and what HA configuration it needs. ### Tier 1 - Stateless, scale horizontally These run as `Deployment`s with no local state. Scale them freely across nodes and AZs. `api`, `web`, `space`, `admin`, `live`, `worker`, `silo`, `email_service`, `outbox_poller`, `automation_consumer`, `pi`, `pi_worker`, `runner`, `iframely` Run at least `replicas: 2` per service. Use `replicas >= 2` for `api`, `worker`, `web`, and `live` - they carry the most traffic. ### Tier 2 - Singletons (replicas: 1 only) These do scheduled or coordinator work. **Do not scale any of them past `replicas: 1`** - running two copies doubles job execution. | Workload | Kind | Why it stays at 1 | | ---------------- | ----------- | -------------------------------------------- | | `monitor` | StatefulSet | Coordinator role; owns a `ReadWriteOnce` PVC | | `beatworker` | Deployment | Celery beat - schedules periodic Plane jobs | | `pi_beat_worker` | Deployment | PI beat - schedules periodic PI jobs | | `migrator` | Job | DB migration; runs once per release | | `pi-migrator` | Job | PI DB migration; runs once per release | The stateless singletons (`beatworker`, `pi_beat_worker`) reschedule onto a healthy node within seconds when their node fails. `monitor` is different: it owns an AZ-bound `ReadWriteOnce` PVC. On AZ failure, Kubernetes has to reschedule it onto a node in a live AZ and reattach the volume - expect a **60–120 second** recovery window. That's acceptable because `monitor` is an internal component, not user-facing. `migrator` and `pi-migrator` are run-once-per-release Jobs. They aren't long-running, but they still must not run in parallel. ### Tier 3 - Local stateful (not HA) The chart ships optional in-cluster StatefulSets for development and small deployments: `postgres`, `redis`, `rabbitmq`, `opensearch`, `minio` These use single-replica `ReadWriteOnce` PVCs. They're **not HA.** Their data is pinned to one disk in one AZ, and the chart doesn't configure replication, failover, or quorum. **For every HA deployment, set `local_setup: false` for every Tier-3 service** and point Plane at managed, multi-AZ equivalents. The [External managed services](#external-managed-services) section has the exact value keys. ## Cluster prerequisites Your cluster needs the following before installing in HA mode. **1. Worker nodes in at least three AZs.** Three is the minimum for any quorum service (etcd, Postgres synchronous replicas, OpenSearch master quorum). Two AZs survive single-AZ loss for stateless workloads but can't maintain quorum. **2. A default `StorageClass` with `volumeBindingMode: WaitForFirstConsumer`.** This is non-negotiable when Tier-2 singletons run on nodes provisioned just-in-time (Karpenter, Cluster Autoscaler). Without it, a PVC can bind to a zone before the pod schedules, leaving the pod unable to find a matching node. Example for AWS EBS gp2: ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gp2 parameters: type: gp2 fsType: ext4 provisioner: ebs.csi.aws.com volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Retain allowVolumeExpansion: true ``` Then set this in `values.yaml`: ```yaml env: storageClass: gp2 ``` **3. A cross-zone load balancer.** Traffic must reach pods in any AZ. | Cloud | Recommendation | | ------- | ------------------------------------------------- | | AWS | NLB or ALB with cross-zone load balancing enabled | | GCP | Default global LB | | Azure | Standard Load Balancer with zones `[1,2,3]` | | On-prem | MetalLB in BGP mode, or an external LB | **4. A working `IngressClass`.** The chart supports `traefik` (default) or `nginx`. Deploy the ingress controller with `replicas >= 2` spread across AZs. **5. AZ-aware node labels.** Kubernetes uses `topology.kubernetes.io/zone` for AZ awareness. Managed clusters populate this automatically. Verify your nodes carry this label if you're on a self-managed cluster. ## Recommended topology ```text ┌──────────────────────────┐ │ External Load Balancer │ │ (cross-zone enabled) │ └────────────┬─────────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ AZ-a │ │ AZ-b │ │ AZ-c │ │ │ │ │ │ │ │ ingress │ │ ingress │ │ ingress │ │ api x N │ │ api x N │ │ api x N │ │ web x N │ │ web x N │ │ web x N │ │ worker │ │ worker │ │ worker │ │ … │ │ … │ │ … │ └─────────┘ └─────────┘ └─────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ┌───────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐ │ Managed │ │ Managed │ │ Object │ │ Postgres │ │ Redis │ │ Storage │ │ (multi-AZ) │ │ (multi-AZ) │ │ (S3-class) │ └──────────────┘ └──────────────┘ └──────────────┘ ┌──────────────┐ ┌──────────────┐ │ Managed │ │ Managed │ │ RabbitMQ │ │ OpenSearch │ │ (cluster) │ │ (multi-AZ) │ └──────────────┘ └──────────────┘ ``` Tier-1 pods spread across AZs. All Tier-3 state lives in managed services that handle their own replication and failover. ## External managed services ### Value keys The chart supports pointing each stateful component at a remote managed service. Use these value keys. | Component | Disable local | External URL / credentials | | ------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Postgres | `services.postgres.local_setup: false` | `env.pgdb_remote_url`, `env.pg_pi_db_remote_url`; optional read replica via `services.postgres.read_replica.enabled` + `services.postgres.read_replica.remote_url` | | Redis | `services.redis.local_setup: false` | `env.remote_redis_url` | | RabbitMQ | `services.rabbitmq.local_setup: false` | `services.rabbitmq.external_rabbitmq_url` | | OpenSearch | `services.opensearch.local_setup: false` | `env.opensearch_remote_url`, `env.opensearch_remote_username`, `env.opensearch_remote_password`; optional `env.opensearch_index_prefix` for multi-tenant clusters | | Object store | `services.minio.local_setup: false` | `env.aws_access_key`, `env.aws_secret_access_key`, `env.aws_region`, `env.aws_s3_endpoint_url`, `env.docstore_bucket` | ### What HA looks like for each service Setting `local_setup: false` doesn't make your data tier HA on its own. The managed service you point Plane at must also be HA. Here's what each one needs. * **Postgres** - Multi-AZ primary with synchronous replication and automated failover. Use RDS Multi-AZ, Cloud SQL HA, Azure Flexible Server zone-redundant, or self-managed Patroni. * **Redis** - A replica group with automatic failover. Use ElastiCache Multi-AZ, Memorystore HA, or Redis Sentinel/Cluster. Redis failover drops in-flight connections; Plane reconnects automatically. * **RabbitMQ** - A true cluster with quorum queues across ≥3 nodes in ≥3 AZs. CloudAMQP and Amazon MQ for RabbitMQ in cluster mode both work. A single-node managed RabbitMQ is **not** HA. * **OpenSearch** - ≥3 master-eligible nodes across 3 AZs, plus data nodes spread across AZs. * **Object storage** - S3, GCS, and Azure Blob are multi-AZ by design. ## Spreading pods across availability zones ### How the chart exposes scheduling controls The chart exposes `nodeSelector`, `tolerations`, and `affinity` on every service (see `templates/_helpers.tpl` → `plane.podScheduling`). Use these to spread Tier-1 pods across AZs. :::info The chart doesn't natively support `topologySpreadConstraints` - that's on the roadmap. Use `podAntiAffinity` in the meantime. It's functionally equivalent for AZ spreading. ::: ### Recommended pattern: soft AZ anti-affinity + hard node anti-affinity Use a hard rule to prevent two replicas landing on the same node, and a soft rule to prefer spreading across AZs. The soft AZ rule means the scheduler can still place pods if one AZ is under pressure. ```yaml services: api: replicas: 3 affinity: podAntiAffinity: # Hard: never put two api pods on the same node requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app.name operator: In values: - --api topologyKey: kubernetes.io/hostname # Soft: prefer spreading api pods across AZs preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app.name operator: In values: - --api topologyKey: topology.kubernetes.io/zone ``` The chart labels every workload with `app.name` set to {{ .Release.Namespace }}-{{ .Release.Name }}-\. For a release named `plane` in namespace `plane`, that's `plane-plane-api` for the API. :::warning **Watch for this** The hard hostname anti-affinity rule requires at least as many schedulable nodes as the workload's replica count. Three `api` replicas need three nodes available, or pods sit `Pending`. If you can't guarantee that (small cluster, dedicated taints), relax the hostname rule to `preferredDuringSchedulingIgnoredDuringExecution`. ::: Apply this pattern to every Tier-1 service: `web`, `space`, `admin`, `live`, `worker`, `silo`, `email_service`, `outbox_poller`, `automation_consumer`, `pi`, `pi_worker`, `runner`, `iframely`. ### Pinning workloads to specific node pools Use `nodeSelector` and `tolerations` to route a workload to a specific pool - for example, spot instances for batch workers: ```yaml services: worker: replicas: 6 nodeSelector: workload-class: batch tolerations: - key: workload-class operator: Equal value: batch effect: NoSchedule ``` ## PodDisruptionBudgets :::info Native PDB rendering is planned for a future release. Apply the manifests below yourself until then. ::: PDBs protect Tier-1 deployments from voluntary disruption - a node drain or cluster upgrade - taking a service down entirely. Without them, Kubernetes can evict all pods of a deployment simultaneously. Apply this manifest in the same namespace as your release. Replace `RELEASE` and `NAMESPACE` with your values. ```yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: plane-api-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app.name: NAMESPACE-RELEASE-api --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: plane-web-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app.name: NAMESPACE-RELEASE-web --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: plane-space-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app.name: NAMESPACE-RELEASE-space --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: plane-admin-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app.name: NAMESPACE-RELEASE-admin --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: plane-live-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app.name: NAMESPACE-RELEASE-live --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: plane-worker-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app.name: NAMESPACE-RELEASE-worker --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: plane-silo-pdb namespace: NAMESPACE spec: minAvailable: 1 selector: matchLabels: app.name: NAMESPACE-RELEASE-silo ``` Add similar PDBs for `pi`, `pi_worker`, `outbox_poller`, `automation_consumer`, `email_service`, `runner`, and `iframely` if you have enabled them. :::warning **Don't create PDBs for Tier-2 singletons** (`beatworker`, `pi_beat_worker`, `monitor`, `migrator`). A `minAvailable: 1` PDB on a `replicas: 1` workload blocks node drains entirely. ::: ## HorizontalPodAutoscalers :::info Native HPA rendering is planned for a future release. Apply the manifests below yourself until then. ::: HPAs scale Tier-1 services automatically under load. The thresholds below match the default resource requests in `values.yaml`. Tune `averageUtilization` and `maxReplicas` based on observed production load. ```yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: plane-api-hpa namespace: NAMESPACE spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: RELEASE-api-wl minReplicas: 3 maxReplicas: 12 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: plane-worker-hpa namespace: NAMESPACE spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: RELEASE-worker-wl minReplicas: 3 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: plane-web-hpa namespace: NAMESPACE spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: RELEASE-web-wl minReplicas: 2 maxReplicas: 8 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 ``` :::warning **Never create an HPA for `beatworker`, `pi_beat_worker`, `monitor`, or any migration Job.** Scheduled jobs would fire multiple times. ::: ## Karpenter on AWS If you're on EKS, Karpenter is the recommended node provisioner for Plane Commercial Edition. It's AZ-aware, provisions nodes in seconds, and lets you mix on-demand and spot capacity per workload type. ### Minimum versions * Karpenter ≥ v1.0 * Kubernetes ≥ 1.29 * AWS Load Balancer Controller ≥ v2.7 * AWS EBS CSI driver installed ### EC2NodeClass One `EC2NodeClass` covers most installs. Use AL2023, IMDSv2-only, and gp2 root volumes. ```yaml apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass metadata: name: plane-default spec: amiFamily: AL2023 amiSelectorTerms: - alias: al2023@latest role: KarpenterNodeRole-CLUSTER_NAME subnetSelectorTerms: - tags: karpenter.sh/discovery: CLUSTER_NAME securityGroupSelectorTerms: - tags: karpenter.sh/discovery: CLUSTER_NAME blockDeviceMappings: - deviceName: /dev/xvda ebs: volumeType: gp2 volumeSize: 100Gi encrypted: true deleteOnTermination: true metadataOptions: httpEndpoint: enabled httpTokens: required httpPutResponseHopLimit: 1 ``` ### NodePools Two NodePools cover most deployments: an on-demand pool for general Tier-1 workloads, and a spot pool for batch workers (`worker`, `pi_worker`, `runner`, `outbox_poller`, `automation_consumer`). ```yaml apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: plane-general spec: template: spec: nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: plane-default requirements: - key: kubernetes.io/arch operator: In values: [amd64] - key: karpenter.sh/capacity-type operator: In values: [on-demand] - key: karpenter.k8s.aws/instance-category operator: In values: [c, m] - key: karpenter.k8s.aws/instance-generation operator: Gt values: ["5"] - key: topology.kubernetes.io/zone operator: In values: [REGION-a, REGION-b, REGION-c] expireAfter: 720h limits: cpu: "200" memory: 400Gi disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 1m --- apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: plane-spot spec: template: spec: nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: plane-default taints: - key: workload-class value: batch effect: NoSchedule requirements: - key: kubernetes.io/arch operator: In values: [amd64] - key: karpenter.sh/capacity-type operator: In values: [spot] - key: karpenter.k8s.aws/instance-category operator: In values: [c, m, r] - key: karpenter.k8s.aws/instance-generation operator: Gt values: ["5"] - key: topology.kubernetes.io/zone operator: In values: [REGION-a, REGION-b, REGION-c] expireAfter: 24h limits: cpu: "400" memory: 800Gi disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 5m ``` Match the spot NodePool taint with tolerations in your values: ```yaml services: worker: tolerations: - key: workload-class operator: Equal value: batch effect: NoSchedule nodeSelector: karpenter.sh/nodepool: plane-spot ``` ### How Karpenter interacts with AZ spread * Karpenter respects `podAntiAffinity` when deciding which AZ to provision a node in. The affinity patterns from the previous section are sufficient to drive Karpenter's AZ distribution - no extra configuration needed. * Don't add `karpenter.sh/do-not-disrupt: "true"` to Tier-1 pods. They're stateless. Let Karpenter consolidate them freely. * Do add it to Tier-2 singletons (`beatworker`, `pi_beat_worker`, `monitor`) and to in-flight long-running Jobs (`migrator`). They tolerate rescheduling, but you don't want Karpenter bouncing them during a deployment: ```yaml services: beatworker: annotations: karpenter.sh/do-not-disrupt: "true" ``` * `consolidateAfter: 1m` on the on-demand pool keeps the cluster cost-efficient. Raise it to `5m` or `10m` if you see churn during normal scaling. The spot pool's `expireAfter: 24h` forces daily node recycling, spreading the impact of spot interruptions across time rather than concentrating them. ## Ingress and load balancer * Deploy the ingress controller (`traefik` or `nginx`) with `replicas >= 2` spread across AZs using the same `podAntiAffinity` pattern. * Enable cross-zone load balancing on the cloud LB. On AWS: ```yaml service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" ``` * The `live` service uses WebSockets. Make sure your ingress controller and LB don't have idle-timeout values that drop long-lived connections. The default AWS NLB idle timeout is 350s - that's usually fine. ALB defaults to 60s and needs raising for WebSocket connections. * The chart configures request-body size limits via `ingress.traefik.maxRequestBodyBytes` (Traefik) and `nginx.ingress.kubernetes.io/proxy-body-size` (nginx). Tune these to your expected file upload size. ## Backup and disaster recovery HA protects against AZ and node failure. Backups protect against logical corruption, accidental deletion, and ransomware. You need both. | Component | Backup mechanism | Recommended retention | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | Postgres | Managed-service automated backups + PITR | 30 days, PITR ≥ 7 days | | Object storage | Bucket versioning + lifecycle to a different bucket/region | 90 days | | OpenSearch | Snapshots to object storage | 7 days | | Redis | Optional; treat as cache + queue. Document what your team loses on a full Redis failure (sessions, in-flight Celery tasks). | - | | RabbitMQ | Definitions export (users, queues, bindings) on a schedule; messages are transient | - | | Kubernetes objects | Velero, namespace-scoped, daily | 30 days | **Run a restore drill** before go-live and at least once per quarter. A backup that's never been restored is an assumption, not a guarantee. ## Pre-go-live checklist Work through every item before sending real traffic. * \[ ] Cluster has worker nodes in ≥3 AZs * \[ ] Default `StorageClass` is `WaitForFirstConsumer` * \[ ] `env.storageClass` is set to that class * \[ ] All Tier-3 `local_setup` flags are `false` * \[ ] Managed Postgres is multi-AZ with synchronous replica * \[ ] Managed Redis has replica + auto-failover * \[ ] Managed RabbitMQ is a true cluster across ≥3 AZs * \[ ] Managed OpenSearch has ≥3 masters across ≥3 AZs * \[ ] Object storage is multi-AZ (S3/GCS/Blob) with versioning enabled * \[ ] Every Tier-1 service has `replicas >= 2` (3 for `api`, `worker`, `web`) * \[ ] Every Tier-1 service has a `podAntiAffinity` block (hostname + zone) * \[ ] Every Tier-1 service has a PDB * \[ ] HPAs applied for `api`, `worker`, `web` at minimum * \[ ] No HPA or PDB on `beatworker`, `pi_beat_worker`, `monitor`, `migrator` * \[ ] Ingress controller runs with `replicas >= 2` spread across AZs * \[ ] LB has cross-zone load balancing enabled * \[ ] Backups configured and a restore drill has succeeded * \[ ] Failure drill: cordon and drain every node in one AZ; Plane stays up * \[ ] Failure drill: kill the active Postgres node; Plane recovers ## Known chart gaps The following capabilities aren't natively provided by the chart and need to be applied separately. | Gap | Workaround | | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | No native `topologySpreadConstraints` in `plane.podScheduling` | Use `podAntiAffinity` as shown in the spreading section - functionally equivalent for AZ spread | | No PDBs rendered by the chart | Apply the PDB manifests from the PodDisruptionBudgets section | | No HPAs rendered by the chart | Apply the HPA manifests from the HorizontalPodAutoscalers section | | In-chart Tier-3 StatefulSets are single-replica, RWO | Set `local_setup: false` and use managed services | | `monitor` is a singleton StatefulSet | Accept the 60–120s reschedule window on AZ failure - it's internal and non-user-facing | ## Reference values.yaml for HA A minimal example that disables every local stateful service and gives each Tier-1 workload three replicas with AZ anti-affinity. Adapt names to your release. ```yaml planeVersion: v2.6.3 license: licenseServer: https://prime.plane.so licenseDomain: plane.example.com ingress: enabled: true ingressClass: traefik env: storageClass: gp2 pgdb_remote_url: "postgres://plane:***@pg-primary.example.internal:5432/plane?sslmode=require" pg_pi_db_remote_url: "postgres://plane:***@pg-primary.example.internal:5432/plane_pi?sslmode=require" remote_redis_url: "redis://:***@redis.example.internal:6379/0" opensearch_remote_url: "https://opensearch.example.internal:9200" opensearch_remote_username: plane opensearch_remote_password: "***" aws_access_key: "***" aws_secret_access_key: "***" aws_region: us-east-1 aws_s3_endpoint_url: https://s3.us-east-1.amazonaws.com docstore_bucket: plane-uploads-prod web_url: https://plane.example.com instance_admin_email: admin@example.com cors_allowed_origins: https://plane.example.com services: postgres: local_setup: false read_replica: enabled: true remote_url: "postgres://plane:***@pg-reader.example.internal:5432/plane?sslmode=require" redis: local_setup: false rabbitmq: local_setup: false external_rabbitmq_url: "amqps://plane:***@rabbitmq.example.internal:5671/plane" opensearch: local_setup: false minio: local_setup: false api: replicas: 3 affinity: &spread-api podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - { key: app.name, operator: In, values: [plane-plane-api] } topologyKey: kubernetes.io/hostname preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - { key: app.name, operator: In, values: [plane-plane-api] } topologyKey: topology.kubernetes.io/zone web: { replicas: 3 } space: { replicas: 2 } admin: { replicas: 2 } live: { replicas: 3 } worker: { replicas: 4 } silo: { enabled: true, replicas: 2 } beatworker: { replicas: 1 } # singleton - do not scale pi_beat_worker: { replicas: 1 } # singleton - do not scale ``` Repeat the `affinity` block (varying the pod label) for every Tier-1 service. YAML anchors (`&spread-api` / `*spread-api`) help avoid repetition. --- --- url: 'https://developers.plane.so/self-hosting/methods/fips-deployment.html' description: >- Deploy the FIPS variant of Plane Enterprise on a FIPS-enforcing host, including prerequisites, image list, and verification. --- # FIPS deployment Plane publishes a FIPS variant of every application image alongside the standard set. These images are built on Red Hat UBI 10, apply the system-wide FIPS cryptographic policy, and run their cryptography against FIPS-validated modules (Red Hat's OpenSSL FIPS provider for the Python and static services; the Go FIPS 140-3 module for the Go services). They are intended for deployments that must meet FIPS 140-3 expectations, such as US Federal or GovCloud environments. ::: warning **The single most important prerequisite** FIPS mode is a property of the **host**, not of the image. A FIPS image on a non-FIPS host starts cleanly and looks identical from the inside while providing none of the guarantees. Read [Host prerequisite](#host-prerequisite) first. ::: ## Images The FIPS images use the same names as the standard `-commercial` images with a `-fips` suffix, in the `makeplane` Docker Hub organization: | Service | Image | | ------------- | --------------------------------------- | | Backend / API | `makeplane/backend-commercial-fips` | | Web | `makeplane/web-commercial-fips` | | Admin | `makeplane/admin-commercial-fips` | | Space | `makeplane/space-commercial-fips` | | Live | `makeplane/live-commercial-fips` | | Silo | `makeplane/silo-commercial-fips` | | Monitor | `makeplane/monitor-commercial-fips` | | Email | `makeplane/email-commercial-fips` | | Plane AI | `makeplane/plane-pi-commercial-fips` | | Proxy | `makeplane/proxy-commercial-fips` | | Flux | `makeplane/flux-commercial-fips` | | Node runner | `makeplane/node-runner-commercial-fips` | Pin a specific release tag for any accredited deployment rather than tracking `latest` - a known, fixed image version is part of the audit trail. :::info There is no FIPS All-in-One (AIO) image. The AIO image is built on an Alpine base, which has no FIPS-validated cryptography, so a FIPS deployment uses the multi-container stack, not the AIO image. ::: ## Host prerequisite The host kernel must be booted in FIPS mode. The container inherits this through `/proc/sys/crypto/fips_enabled` and **cannot set it itself**. Verify before deploying: ```bash cat /proc/sys/crypto/fips_enabled # must print 1 ``` How you put the host into FIPS mode depends on the distribution and version: **Amazon Linux 2023, RHEL 8/9 (and Rocky, Alma)** - enable in place, then reboot: ```bash sudo dnf install -y crypto-policies-scripts sudo fips-mode-setup --enable sudo reboot ``` **RHEL 10** - `fips-mode-setup` has been removed and post-install switching is not supported: enable FIPS **at install time** with `fips=1` on the kernel command line. **Other** - boot a vendor FIPS image (a RHEL FIPS AMI, Ubuntu Pro FIPS), or install OpenShift with FIPS enabled. As a safeguard, run the FIPS images with `PLANE_REQUIRE_FIPS=1`: the containers then **refuse to start** if the host is not in FIPS mode. Without it, a FIPS image on a non-FIPS host logs a startup warning but runs. ## Deploy on Kubernetes Use the same `plane-enterprise` Helm chart as a [standard Kubernetes install](/self-hosting/methods/kubernetes) - FIPS is a values overlay, not a different chart. Three things change: 1. **Nodes** - provision a node pool whose machine image boots in FIPS mode (see [Host prerequisite](#host-prerequisite)). Label it (e.g. `fips: enabled`) and taint it (e.g. `fips=true:NoSchedule`) so only FIPS workloads land there. 2. **Images** - override every service image to its `-fips` variant. 3. **Scheduling** - every service must carry the matching `nodeSelector` and `toleration`. A pod that misses them schedules onto a stock node and **silently loses FIPS**. ```yaml # values-fips.yaml planeVersion: # Non-root with group 0, matching the FIPS images' group-0-writable directories # (see the non-root section below). securityContext: enabled: true podSecurityContext: runAsGroup: 0 fsGroup: 0 _fips_sched: &fips nodeSelector: fips: enabled tolerations: - key: fips operator: Equal value: "true" effect: NoSchedule services: api: image: makeplane/backend-commercial-fips <<: *fips web: image: makeplane/web-commercial-fips <<: *fips space: image: makeplane/space-commercial-fips <<: *fips admin: image: makeplane/admin-commercial-fips <<: *fips live: image: makeplane/live-commercial-fips <<: *fips silo: image: makeplane/silo-commercial-fips <<: *fips monitor: image: makeplane/monitor-commercial-fips <<: *fips worker: <<: *fips beatworker: <<: *fips # Every additional service you enable needs the same <<: *fips block - # e.g. Plane AI also takes image: makeplane/plane-pi-commercial-fips, and # its pi_worker / pi_beat_worker need the block too. postgres: <<: *fips redis: <<: *fips rabbitmq: <<: *fips minio: <<: *fips ``` ```bash helm repo add plane https://helm.plane.so/ helm upgrade --install plane-app plane/plane-enterprise \ --namespace plane --create-namespace \ -f values-fips.yaml ``` On OpenShift, drop the `securityContext` override and see [Running under a non-root or arbitrary UID](#running-under-a-non-root-or-arbitrary-uid-openshift). ## Verify Each container logs its posture on startup: ```text plane: FIPS mode ACTIVE (host kernel reports fips_enabled=1) ``` The Go services (monitor, email, proxy) log a corresponding line, for example `Go FIPS 140-3 module ACTIVE`. To check a running container directly: ```bash # Kernel flag inherited from the host - must print 1 docker exec plane-api cat /proc/sys/crypto/fips_enabled # The FIPS-validated OpenSSL provider must be loaded and "active" docker exec plane-api openssl list -providers # A non-approved digest must be refused - this must FAIL docker exec plane-api sh -c 'echo x | openssl md5' # Node services must report FIPS - must print 1 docker exec plane-live node -p "require('crypto').getFips()" ``` On Kubernetes, run the same checks with `kubectl exec` against any application pod, e.g. `kubectl -n plane exec deploy/plane-app-api-wl -- cat /proc/sys/crypto/fips_enabled`. ## Configuration defaults specific to FIPS images The FIPS images default to a stricter security posture than the standard images. A fresh FIPS install needs none of these changed; they matter mainly when moving an existing standard deployment onto the FIPS images. | Setting | FIPS default | Standard default | Notes | | ---------------------------------- | ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- | | `LDAP_TLS_REQUIRE_CERT` | `demand` | `never` | Validates the LDAP server's TLS certificate. Set to `never` to restore the previous behavior. | | `SAML_REJECT_DEPRECATED_ALGORITHM` | on | off | Rejects assertions signed with RSA-SHA1. The IdP must sign with SHA-256. | | `SECRET_ENCRYPTION_V2` | on | off | Writes at-rest secrets as AES-256-GCM instead of the legacy format. Both formats are always readable. | | `USAGE_ID_DIGEST` | `sha256` (required) | `md5` | Digest for Plane AI usage-ledger keys. `md5` is incompatible with a FIPS-mode Postgres, so `sha256` is required. | ## Running under a non-root or arbitrary UID (OpenShift) The FIPS application images run non-root, and FIPS mode itself requires no privilege. **Plain Kubernetes** - set `runAsUser: 1000` (the images' built-in user). For any other UID, add `runAsGroup: 0` and `fsGroup: 0`. **OpenShift (`restricted-v2`)** - works out of the box. Don't set `runAsUser`/`runAsGroup`/`fsGroup` yourself; the SCC assigns an arbitrary UID in group `0`, and the images' writable directories are group-`0` writable by design. One exception: the bundled proxy binds ports 80/443, which `restricted-v2` forbids - front it with an OpenShift Route instead. Ingress-based deployments don't use the bundled proxy. --- --- url: 'https://developers.plane.so/self-hosting/methods/podman-quadlets.html' description: >- Install Plane using Podman Quadlets. Guide for deploying Plane with Podman as a Docker alternative with systemd integration. --- # Deploy Plane with Podman Quadlets This guide shows you the steps to deploy a self-hosted instance of Plane using Podman Quadlets. ## Prerequisites Before we start, make sure you've got these covered: * A non-root user account with `systemd --user support` (most modern Linux setups have this) * Podman version **4.4 or higher** ## Set up Podman 1. Add the Podman repository. ```bash echo 'deb http://download.opensuse.org/repositories/home:/alvistack/Debian_12/ /' | sudo tee /etc/apt/sources.list.d/home:alvistack.list ``` 2. Add the GPG key. ```bash curl -fsSL https://download.opensuse.org/repositories/home:alvistack/Debian_12/Release.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/home_alvistack.gpg > /dev/null ``` 3. Refresh your package lists. ```bash sudo apt update ``` 4. Install Podman and its dependencies. ```bash sudo apt install -y podman uidmap netavark passt ``` The `uidmap` package handles user namespace mapping, `netavark` takes care of networking, and `passt` helps with network connectivity. 5. Download and extract Podman Quadlets. ```bash mkdir podman-quadlets curl -fsSL https://prime.plane.so/releases/v2.6.3/podman-quadlets.tar.gz -o podman-quadlets.tar.gz tar -xvzf podman-quadlets.tar.gz -C podman-quadlets ``` The directory contains an `install.sh` script that will handle the installation and configuration. ## Install Plane The installation script sets up Plane and configures all required services. You have two options: ### Without sudo access ```bash ./install.sh --domain your-domain.com --base-dir /your/custom/path ``` This installs Plane in your specified directory, which is useful if you want to maintain control over the installation location. ### With sudo access ```bash ./install.sh --domain your-domain.com ``` This installs Plane in `/opt/plane`, which is a standard system location. ::: info Systemd configurations are installed in `~/.config/containers/systemd/` ::: ## Configure external services (optional) If you use external services for database, Redis, RabbitMQ, OpenSearch, or object storage (MinIO/S3), edit `plane.env` in your Plane installation directory (e.g. `/opt/plane` or your custom path from `--base-dir`) before starting services. See [Environment variables](/self-hosting/govern/environment-variables) for more details. * **Database** — In the **DB SETTINGS** section, set `DATABASE_URL` or individual variables (`PGHOST`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_PORT`). * **Redis** — In the **REDIS SETTINGS** section, set `REDIS_URL` or `REDIS_HOST` and `REDIS_PORT`. * **RabbitMQ** — Set `AMQP_URL` (e.g. `amqp://username:password@your-rabbitmq-host:5672/vhost`). * **OpenSearch** — Set `OPENSEARCH_ENABLED=1`, `OPENSEARCH_URL`, and optionally `OPENSEARCH_USERNAME` and `OPENSEARCH_PASSWORD`. See [Configure OpenSearch for advanced search](/self-hosting/govern/advanced-search). * **MinIO / S3** — In the **DATA STORE SETTINGS** section, set `USE_MINIO=0` for external S3, then set `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_S3_ENDPOINT_URL`, and `AWS_S3_BUCKET_NAME`. After editing `plane.env`, start or restart services as described in [Start Plane](#start-plane) so the changes take effect. ## Start Plane ::: warning Note that you should run these commands without `sudo`. ::: 1. Reload systemd to recognize new configurations. ```bash systemctl --user daemon-reload ``` 2. Start the network service. ```bash systemctl --user start plane-nw-network.service ``` 3. Start core dependencies. ```bash systemctl --user start plane-{db,redis,mq,minio}.service ``` 4. Start backend services. ```bash systemctl --user start {api,worker,beat-worker,migrator,monitor}.service ``` 5. Start frontend services. ```bash systemctl --user start {web,space,admin,live,proxy}.service ``` The startup sequence is important: network first, then dependencies, followed by backend services, and finally frontend services. 6. If you've purchased a paid plan, [activate your license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) to unlock premium features. ### Verify service status Check that all services are running correctly: 1. Check network status. ```bash systemctl --user status plane-nw-network.service ``` 2. Check core dependencies. ```bash systemctl --user status plane-{db,redis,mq,minio}.service ``` 3. Check backend services. ```bash systemctl --user status {api,worker,beat-worker,migrator,monitor}.service ``` 4. Check frontend services. ```bash systemctl --user status {web,space,admin,live,proxy}.service ``` Your Plane installation should now be running successfully with Podman Quadlets. This setup provides automatic service restart capabilities and standard systemd management commands for maintaining your installation. ## Troubleshoot To debug service issues, examine the logs using: ```bash journalctl --user -u --no-pager ``` The logs will provide detailed information about any configuration issues or errors that may occur. --- --- url: 'https://developers.plane.so/self-hosting/methods/airgapped-requirements.html' description: >- System requirements and architecture overview for Plane airgapped deployments. Hardware specs, network topology, and prerequisites for offline installations. --- # Airgapped deployment architecture ::: info Airgapped deployments are available exclusively for Enterprise Grid customers with a minimum commitment of 100 seats. Contact our [Sales team](mailto:sales@plane.so) for trials, exceptions to the seat cut-off, tailored pricing, and licensing info. ::: This document explains Plane's architecture and specific requirements for airgapped deployments. Review this before beginning your airgapped installation on [Docker](/self-hosting/methods/airgapped-edition) or [Kubernetes](/self-hosting/methods/airgapped-edition-kubernetes). ## What is an airgapped deployment? An airgapped deployment operates in a completely isolated network environment with no external internet connectivity. This isolation is common in highly regulated industries, government facilities, and organizations with strict security requirements. Plane supports fully airgapped deployments where all components - application services, databases, storage, and integrations - operate entirely within your isolated network perimeter. ## Deployment methods Plane's Airgapped Edition can be deployed using Docker or Kubernetes. Choose the method that best fits your infrastructure. ## Airgapped cluster architecture Here's how Plane operates in an airgapped environment with internal enterprise applications: ![Airgapped cluster architecture](/images/airgapped/airgapped-cluster.webp#hero) This diagram illustrates a critical principle: **all OAuth flows and API communication remain internal to the airgapped cluster**. When integrating with self-hosted GitHub Enterprise, GitLab, or other internal services, the entire authentication and data exchange happens within your isolated network — no internet access required. For a detailed breakdown of Plane's services and infrastructure dependencies, see [Plane self-hosted architecture](/self-hosting/plane-architecture). **Critical guarantees for airgapped environments** * **No telemetry**\ Plane does not send application data, usage metrics, or telemetry outside the cluster. No analytics, crash reports, or usage statistics leave your network. * **Offline licensing**\ License validation happens through uploaded license files downloaded from the Prime portal. No internet connection required after initial license file transfer. * **Zero external dependencies**\ After initial image import, no external network connectivity is required for Plane to operate. All features work entirely within your isolated environment. * **Internal-only communication**\ All service-to-service communication stays within your cluster. Services never attempt to reach external APIs, CDNs, or third-party services. ### How integrations stay internal The airgapped cluster diagram above shows the complete data flow. Key points: * **OAuth providers** - Your internal GitHub Enterprise or GitLab instance acts as the OAuth provider * **Authorization endpoints** - All OAuth URLs point to internal systems, never external SaaS services * **API communication** - Plane makes API calls only to your internal instances * **Webhook delivery** - Internal systems send webhooks to Plane's internal endpoints * **No SaaS fallback** - Plane never attempts to reach github.com, gitlab.com, or slack.com APIs This architecture ensures complete network isolation while maintaining full integration functionality. *** ## Kubernetes-specific requirements ### Base environment Deploying airgapped Plane via Kubernetes requires preparing all dependencies to operate without any external network access. #### Container images and artifacts * Maintain an internal OCI or container registry to host all Plane service images * Prepare a controlled process to pull, verify, and mirror Plane container images and Helm charts from an online staging environment into the airgapped registry #### Kubernetes environment **Supported versions:** Kubernetes 1.31 – 1.33 **Required components:** * IngressClass configured * StorageClass available * cert-manager configured with an internal CA **Node requirements:** * Ensure node OS dependencies and container runtime packages are available from mirrored package repositories like apt, yum, or offline bundles ### Scaling Horizontal scaling is handled via replica counts configurable in `values.yaml`. Plane avoids using StatefulSets where possible due to the complexity of scaling stateful workloads in Kubernetes. The `monitor` service uses a StatefulSet. **For airgapped clusters:** * Ensure metrics-server images are mirrored if using HPA * If using node autoscaling, ensure node images are pre-loaded and registries accessible on bootstrap ### Secrets management Plane supports using existing external secret stores, provided they are reachable within the airgapped environment: * AWS Secrets Manager for private VPC with no internet * HashiCorp Vault * Self-hosted Bitwarden * Kubernetes Secrets * SOPS, sealed-secrets, if preferred ### Additional considerations * Ensure all secret providers can function without external network access * cert-manager must use an internal certificate authority * Keys and secret rotation policies should be part of the airgap operational procedures --- --- url: 'https://developers.plane.so/self-hosting/methods/airgapped-edition.html' description: >- Deploy Plane in airgapped environment without internet access. Complete guide for offline Plane installation. --- # Deploy Plane Airgapped on Docker :::info Airgapped deployments are available exclusively for Enterprise Grid customers with a minimum commitment of 100 seats. Contact our [Sales team](mailto:sales@plane.so) for trials, exceptions to the seat cut-off, tailored pricing, and licensing info. ::: This guide walks you through deploying Plane Commercial in an airgapped Docker environment using Docker Compose and pre-configured images from your private registry. ## Prerequisites Before starting, ensure you have: * Docker (version 24 or later) installed and running * Docker Compose Plugin installed (you should be able to run `docker compose` or `docker-compose`) * Access to a private Docker registry containing Plane images * Required ports opened to access the application (80, 443) :::warning While Docker can run stateful services with persistent volumes, we strongly recommend using external managed services for better reliability in backup/restore operations and disaster recovery. Consider these alternatives: * **MinIO**: Replace with AWS S3, Google Cloud Storage, or any S3-compatible service * **Redis**: Replace with Valkey or a managed Redis service * **PostgreSQL**: Use a managed PostgreSQL service * **RabbitMQ**: Use a managed message queue service * **OpenSearch**: Use a managed OpenSearch service ::: ## Install Plane 1. **Prepare Docker images for airgapped environment** Refer to [this document](/self-hosting/methods/clone-docker-images) to download the Docker images from the Plane artifact registry to your internal registry. :::info This process will NOT download or clone these infrastructure images: * `valkey/valkey:7.2.11-alpine` * `postgres:15.7-alpine` * `rabbitmq:3.13.6-management-alpine` * `minio/minio:latest` * `minio/mc:latest` * `opensearchproject/opensearch:3.3.2` If you're using local infrastructure services, you'll need to pull and transfer these images separately. ::: 2. **Download Docker Compose configuration** ```bash # Download docker-compose.yml curl -fsSL https://prime.plane.so/releases//docker-compose-airgapped.yml -o docker-compose.yml # Download environment template curl -fsSL https://prime.plane.so/releases//variables-airgapped.env -o plane.env ``` 3. **Configure environment variables** Edit the `plane.env` file to configure your deployment: ```bash # Set your domain DOMAIN_NAME=plane.yourcompany.com WEB_URL=https://plane.yourcompany.com CORS_ALLOWED_ORIGINS=https://plane.yourcompany.com SITE_ADDRESS=https://plane.yourcompany.com # Paste the generated UUID here (see note below) MACHINE_SIGNATURE=your-uuid-here ``` :::info **Generating `MACHINE_SIGNATURE`:** Run the following command in your terminal to generate a unique UUID, then paste the output as the value of `MACHINE_SIGNATURE` in `plane.env`: ```bash uuidgen ``` ::: **Update image references** in `docker-compose.yml` to point to your private registry: ```yaml services: web: image: your-registry.io/plane/web-commercial:${APP_RELEASE_VERSION} api: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} worker: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} beat-worker: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} migrator: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} importer-worker: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} automation-consumer: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} webhook-consumer: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} outbox-poller: image: your-registry.io/plane/backend-commercial:${APP_RELEASE_VERSION} space: image: your-registry.io/plane/space-commercial:${APP_RELEASE_VERSION} admin: image: your-registry.io/plane/admin-commercial:${APP_RELEASE_VERSION} live: image: your-registry.io/plane/live-commercial:${APP_RELEASE_VERSION} live-exporter: image: your-registry.io/plane/live-commercial:${APP_RELEASE_VERSION} monitor: image: your-registry.io/plane/monitor-commercial:${APP_RELEASE_VERSION} silo: image: your-registry.io/plane/silo-commercial:${APP_RELEASE_VERSION} email: image: your-registry.io/plane/email-commercial:${APP_RELEASE_VERSION} pi-api: image: your-registry.io/plane/plane-pi-commercial:${APP_RELEASE_VERSION} pi-beat: image: your-registry.io/plane/plane-pi-commercial:${APP_RELEASE_VERSION} pi-worker: image: your-registry.io/plane/plane-pi-commercial:${APP_RELEASE_VERSION} pi-migrator: image: your-registry.io/plane/plane-pi-commercial:${APP_RELEASE_VERSION} runner: image: your-registry.io/plane/node-runner-commercial:${APP_RELEASE_VERSION} iframely: image: your-registry.io/plane/iframely:v2.5.3 proxy: image: your-registry.io/plane/proxy-commercial:${APP_RELEASE_VERSION} ``` **Infrastructure services** (if using local setup): ```yaml services: plane-redis: image: valkey/valkey:7.2.11-alpine plane-db: image: postgres:15.7-alpine pi-db-init: image: postgres:15.7-alpine plane-mq: image: rabbitmq:3.13.6-management-alpine plane-minio: image: minio/minio:latest ``` ## Start Plane 1. Start the services: ```bash docker compose --env-file plane.env up -d ``` 2. Watch the logs to make sure everything starts properly. * To monitor the database migration process: ```bash docker compose logs -f migrator ``` * To monitor the API service startup: ```bash docker compose logs -f api ``` The API is healthy when you see: `api-1 listening at` Once all services are running smoothly, you can access Plane by opening your browser and going to the domain you configured. You now have Plane running in your air-gapped environment. If you run into any issues, check the logs using the commands above, or reach out to our support team for assistance. 3. [Activate your license key](/self-hosting/manage/manage-licenses/activate-airgapped) --- --- url: >- https://developers.plane.so/self-hosting/methods/airgapped-edition-kubernetes.html description: >- Deploy Plane on Kubernetes using Helm charts. Complete guide for production-ready Kubernetes deployment with scaling and management. --- # Deploy Plane Airgapped on Kubernetes ::: info Airgapped deployments are available exclusively for Enterprise Grid customers with a minimum commitment of 100 seats. Contact our [Sales team](mailto:sales@plane.so) for trials, exceptions to the seat cut-off, tailored pricing, and licensing info. ::: This guide walks you through deploying Plane Commercial in an airgapped Kubernetes environment using Helm charts and pre-packaged Docker images. ## What you'll need Before starting, ensure you have: * Kubernetes cluster (v1.31 - v1.33) * Helm 3.x installed * `kubectl` configured to access your cluster * `cert-manager` available in the cluster * A valid and working ingress controller (nginx, traefik, etc) * Required ports opened to access the application (80, 443) * SMTP ports opened if using email intake (25, 465, 587) ::: warning While Kubernetes can run stateful services with persistent volumes, and Plane's Helm chart supports deploying PostgreSQL, MinIO, RabbitMQ, and Redis, we strongly recommend using external managed services for better reliability in backup/restore operations and disaster recovery. Consider these alternatives: * **MinIO**: Replace with AWS S3, Google Cloud Storage, or any S3-compatible service * **Redis**: Replace with Valkey or a managed Redis service * **PostgreSQL**: Use a managed PostgreSQL service * **RabbitMQ**: Use a managed message queue service * **OpenSearch**: Use a managed OpenSearch service ::: ## Install Plane 1. **Download Plane Enterprise Helm chart** Get the Plane Enterprise Helm chart from the official release. Check for the latest version at [Artifact Hub](https://artifacthub.io/packages/helm/makeplane/plane-enterprise). ```bash # Using wget wget https://github.com/makeplane/helm-charts/releases/download/plane-enterprise-1.6.4/plane-enterprise-1.6.4.tgz # Using curl curl -L -O https://github.com/makeplane/helm-charts/releases/download/plane-enterprise-1.6.4/plane-enterprise-1.6.4.tgz ``` 2. **Prepare Docker images for airgapped environment** Refer to [this document](/self-hosting/methods/clone-docker-images) to download the Docker images from the public repository to your internal repository. ::: info This process will NOT download or clone these infrastructure images: * `valkey:7.2.5-alpine` * `postgres:15.7-alpine` * `rabbitmq:3.13.6-management-alpine` * `minio/minio:latest` * `minio/mc:latest` * `opensearchproject/opensearch:3.3.2` If you're using `local_setup: true` for any of these services, you'll need to pull and transfer these images separately. ::: 3. **Configure custom values file** a. Extract the default values from the Helm chart. ```bash helm show values plane-enterprise-1.6.4.tgz > custom-values.yaml ``` b. Update Docker image references Edit the `custom-values.yaml` file to point to your local or private registry images and configure important settings. **Basic configuration:** ```yaml # Specify the Plane version planeVersion: # Enable airgapped mode (REQUIRED) airgapped: enabled: true # Must be TRUE for airgapped installations # If using custom root CA for S3 storage s3Secrets: - name: plane-s3-ca key: s3-custom-ca.crt - name: plane-s3-ca-2 key: s3-custom-ca-2.crt ``` **Service images:** ```yaml services: web: image: /web-commercial api: image: /backend-commercial space: image: /space-commercial admin: image: /admin-commercial live: image: /live-commercial monitor: image: /monitor-commercial email_service: enabled: true image: /email-commercial silo: enabled: true image: /silo-commercial iframely: enabled: true image: /iframely:v1.2.0 ``` **Infrastructure services:** Configure whether to use local (in-cluster) or external services: ```yaml services: # Database and infrastructure images redis: local_setup: true # Set to false if using external service image: valkey/valkey:7.2.11-alpine postgres: local_setup: true # Set to false if using external service image: postgres:15.7-alpine rabbitmq: local_setup: true # Set to false if using external service image: rabbitmq:3.13.6-management-alpine external_rabbitmq_url: "" # Required only if using remote RabbitMQ minio: local_setup: true # Set to false if using external service image: minio/minio:latest image_mc: minio/mc:latest ``` **Environment variables:** ```yaml env: storageClass: "" remote_redis_url: "" # Required only if using remote Redis pgdb_remote_url: "" # Required only if using remote PostgreSQL # Required if MinIO local_setup is false aws_access_key: "" aws_secret_access_key: "" aws_region: "" aws_s3_endpoint_url: "" ``` c. **Configure integrations and importers** To set up integrations with external systems like Slack, GitHub, and GitLab, configure these values in `custom-values.yaml`: ```yaml services: silo: enabled: true connectors: slack: enabled: false client_id: "" client_secret: "" github: enabled: false client_id: "" client_secret: "" app_name: "" app_id: "" private_key: "" gitlab: enabled: false client_id: "" client_secret: "" env: silo_envs: batch_size: 100 mq_prefetch_count: 1 request_interval: 400 hmac_secret_key: "" aes_secret_key: "dsOdt7YrvxsTIFJ37pOaEVvLxN8KGBCr" ``` d. **Configure intake email** The email intake feature in Plane lets you capture incoming emails. Before or after setting up the application, configure DNS settings following [this guide](https://developers.plane.so/self-hosting/govern/configure-dns-email-service). Add these required values to `custom-values.yaml`: ```yaml ingress: enabled: true ingressClass: 'nginx' # Or as per your cluster ingress_annotations: {} ssl: tls_secret_name: '' # If you have a custom TLS secret name # If you want to use Let's Encrypt, set createIssuer and generateCerts to true createIssuer: false issuer: http # Allowed: cloudflare, digitalocean, http token: '' # Not required for http server: https://acme-v02.api.letsencrypt.org/directory email: plane@example.com # A valid email address generateCerts: true services: email_service: enabled: true replicas: 1 memoryLimit: 1000Mi cpuLimit: 500m memoryRequest: 50Mi cpuRequest: 50m image: /email-commercial: pullPolicy: Always nodeSelector: {} tolerations: [] affinity: {} labels: {} annotations: {} env: email_service_envs: smtp_domain: '' ``` 4. **Install or upgrade with custom values** Install Plane Enterprise using your customized values file: ```bash helm upgrade plane-app plane-enterprise-1.6.4.tgz \ --install \ --create-namespace \ --namespace plane \ -f custom-values.yaml \ --timeout 10m \ --wait \ --wait-for-jobs ``` 5. **Verify installation** Check that all components are running: ```bash # Check all pods kubectl get pods -n plane # Check services kubectl get services -n plane # Check ingress kubectl get ingress -n plane # Check persistent volumes kubectl get pv,pvc -n plane # Get the ingress URL kubectl get ingress -n plane -o wide ``` You now have Plane running in your air-gapped environment. If you run into any issues, check the logs using the commands above, or reach out to our support team for assistance. 6. [Activate your license key](/self-hosting/manage/manage-licenses/activate-airgapped). ## Additional configuration For more advanced Plane configuration options, refer to the [Kubernetes documentation](https://developers.plane.so/self-hosting/methods/kubernetes#configuration-settings). --- --- url: 'https://developers.plane.so/self-hosting/methods/clone-docker-images.html' description: >- Mirror Plane Docker images to your private container registry. Pull, tag, and push images for airgapped or restricted network deployments. --- # Clone Docker images to your private registry ::: info **Part of airgapped deployment**\ This guide is part of the airgapped deployment process. If you're setting up Plane in an airgapped environment, return to that guide after copying your images. ::: This guide shows you how to copy Docker images from the Plane artifact registry to your destination registry using the `crane` tool. ## Prerequisites ### Install crane Crane is a tool for interacting with remote container images and registries. Install it on a machine with internet access. **macOS:** ```bash brew install crane ``` **Linux:** ```bash # Download the latest release VERSION=$(curl -s https://api.github.com/repos/google/go-containerregistry/releases/latest | grep '"tag_name"' | cut -d'"' -f4) curl -sL "https://github.com/google/go-containerregistry/releases/download/${VERSION}/go-containerregistry_Linux_x86_64.tar.gz" | tar xz crane sudo mv crane /usr/local/bin/ # Verify installation crane version ``` **Windows (using WSL or Git Bash):** ```bash # Download and extract curl -sL "https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Windows_x86_64.tar.gz" | tar xz crane.exe ``` ## Plane images to copy The following Plane Commercial images need to be transferred to your private registry: ``` makeplane/admin-commercial:${APP_RELEASE_VERSION} makeplane/web-commercial:${APP_RELEASE_VERSION} makeplane/space-commercial:${APP_RELEASE_VERSION} makeplane/live-commercial:${APP_RELEASE_VERSION} makeplane/monitor-commercial:${APP_RELEASE_VERSION} makeplane/backend-commercial:${APP_RELEASE_VERSION} makeplane/iframely:v2.5.3 makeplane/silo-commercial:${APP_RELEASE_VERSION} makeplane/email-commercial:${APP_RELEASE_VERSION} makeplane/plane-pi-commercial:${APP_RELEASE_VERSION} makeplane/node-runner-commercial:${APP_RELEASE_VERSION} makeplane/proxy-commercial:${APP_RELEASE_VERSION} ``` ::: warning **Infrastructure images not included:** The Plane artifact registry does not include infrastructure images. If you're using `local_setup: true` for any infrastructure services, you'll need to pull these separately from public registries: * `valkey/valkey:7.2.11-alpine` * `postgres:15.7-alpine` * `rabbitmq:3.13.6-management-alpine` * `minio/minio:latest` * `minio/mc:latest` ::: ## Configure environment variables Set your version and destination registry before copying images. ```bash # Set your Plane version export APP_RELEASE_VERSION="v3.1.0" # Replace with your desired version # Set your destination registry export DESTINATION_REGISTRY="your-registry.io/your-namespace" ``` **Example destination registry values:** ```bash # Docker Hub export DESTINATION_REGISTRY="docker.io/yourcompany" # Google Container Registry export DESTINATION_REGISTRY="gcr.io/your-project" # Private registry export DESTINATION_REGISTRY="your-private-registry.com/plane" # AWS ECR export DESTINATION_REGISTRY="123456789012.dkr.ecr.us-east-1.amazonaws.com/plane" # Azure Container Registry export DESTINATION_REGISTRY="yourregistry.azurecr.io/plane" ``` ## Authenticate to your destination registry Before copying images, authenticate crane to your destination registry. **Docker Hub:** ```bash crane auth login docker.io -u YOUR_USERNAME -p YOUR_PASSWORD ``` **Google Container Registry:** ```bash gcloud auth configure-docker ``` **AWS ECR:** ```bash aws ecr get-login-password --region REGION | crane auth login --username AWS --password-stdin AWS_ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com ``` **Azure Container Registry:** ```bash az acr login --name YOUR_REGISTRY_NAME ``` **Harbor or other private registries:** ```bash crane auth login your-registry.com -u YOUR_USERNAME -p YOUR_PASSWORD ``` ## Copy images to your registry You can copy images individually or use the provided script to copy all images at once. ### Option 1: Copy individual images **Basic image copy:** ```bash crane copy \ makeplane/backend-commercial:${APP_RELEASE_VERSION} \ ${DESTINATION_REGISTRY}/backend-commercial:${APP_RELEASE_VERSION} ``` **Copy with specific platform (architecture):** ```bash crane copy \ --platform linux/amd64 \ makeplane/backend-commercial:${APP_RELEASE_VERSION} \ ${DESTINATION_REGISTRY}/backend-commercial:${APP_RELEASE_VERSION} ``` **Verify source image before copying:** ```bash # Check if source image exists crane manifest makeplane/backend-commercial:${APP_RELEASE_VERSION} # List all available tags crane ls makeplane/backend-commercial ``` **Verify image after copying:** ```bash # Get image digest crane digest ${DESTINATION_REGISTRY}/backend-commercial:${APP_RELEASE_VERSION} # Verify manifest crane manifest ${DESTINATION_REGISTRY}/backend-commercial:${APP_RELEASE_VERSION} ``` ### Option 2: Copy all images with a script Create a file named `copy-plane-images.sh`: ```bash #!/bin/bash set -e # Configuration APP_RELEASE_VERSION="${APP_RELEASE_VERSION:-v3.1.0}" DESTINATION_REGISTRY="${DESTINATION_REGISTRY}" if [ -z "$DESTINATION_REGISTRY" ]; then echo "Error: DESTINATION_REGISTRY environment variable is not set" echo "Example: export DESTINATION_REGISTRY='docker.io/yourcompany'" exit 1 fi # Source registry SOURCE_REGISTRY="makeplane" # Image list declare -a IMAGES=( "admin-commercial:${APP_RELEASE_VERSION}" "web-commercial:${APP_RELEASE_VERSION}" "space-commercial:${APP_RELEASE_VERSION}" "live-commercial:${APP_RELEASE_VERSION}" "monitor-commercial:${APP_RELEASE_VERSION}" "backend-commercial:${APP_RELEASE_VERSION}" "iframely:v2.5.3" "silo-commercial:${APP_RELEASE_VERSION}" "email-commercial:${APP_RELEASE_VERSION}" "plane-pi-commercial:${APP_RELEASE_VERSION}" "node-runner-commercial:${APP_RELEASE_VERSION}" "proxy-commercial:${APP_RELEASE_VERSION}" ) echo "Starting image copy process..." echo "Source: ${SOURCE_REGISTRY}" echo "Destination: ${DESTINATION_REGISTRY}" echo "Version: ${APP_RELEASE_VERSION}" echo "" # Copy each image for IMAGE in "${IMAGES[@]}"; do SOURCE="${SOURCE_REGISTRY}/${IMAGE}" DESTINATION="${DESTINATION_REGISTRY}/${IMAGE}" echo "Copying: ${SOURCE} -> ${DESTINATION}" crane copy "${SOURCE}" "${DESTINATION}" if [ $? -eq 0 ]; then echo "✓ Successfully copied ${IMAGE}" else echo "✗ Failed to copy ${IMAGE}" exit 1 fi echo "" done echo "All images copied successfully!" ``` Make the script executable and run it: ```bash chmod +x copy-plane-images.sh ./copy-plane-images.sh ``` The script will copy all Plane images to your destination registry. Each image copy is verified, and the script exits if any copy fails. ## Troubleshooting ### Authentication issues **Error:** `unauthorized: authentication required` **Solution:** Re-authenticate to your destination registry: ```bash crane auth login your-destination-registry.com ``` Verify your credentials are correct and that you have push permissions to the registry. ### Network timeouts **Error:** Large images timing out during transfer **Solution:** Crane handles retries automatically, but you can also: * Check your network connection stability * Try copying during off-peak hours * Use a machine with better network connectivity * Copy images individually rather than using the batch script ### Permission denied **Error:** `denied: requested access to the resource is denied` **Solution:** * Ensure you have push permissions to the destination registry * Verify your authentication credentials are correct * Check that the destination repository exists, or that you have permission to create it * For organizational registries, confirm your account has the necessary roles ### Image not found **Error:** `MANIFEST_UNKNOWN: manifest unknown` **Solution:** * Verify the source image exists: ```bash crane ls makeplane/backend-commercial ``` * Check that you're using the correct version tag * Ensure `APP_RELEASE_VERSION` is set correctly * Verify the image name is spelled correctly ### Rate limiting **Error:** `429 Too Many Requests` **Solution:** * Wait a few minutes and retry * Authenticate to increase rate limits * For Docker Hub, ensure you're using an authenticated account (free accounts have higher limits than anonymous) * Spread out image copies over time if hitting limits repeatedly ## Additional resources * [Crane documentation](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md) * [Crane GitHub repository](https://github.com/google/go-containerregistry) --- --- url: 'https://developers.plane.so/self-hosting/methods/coolify.html' description: >- Deploy Plane using Coolify, an open-source PaaS. One-click deployment with automatic SSL, domain configuration, and container management. --- # Deploy Plane with Coolify This guide shows you the steps to deploy a self-hosted instance of Plane using Coolify. ## Install Plane ### Prerequisites * Before you get started, make sure you have a Coolify environment set up and ready to go. * Your setup should support either amd64 or arm64 architectures. ### Procedure 1. **Download the required deployment files** `coolify-compose.yml` – Defines Plane's services and dependencies. ```bash curl -fsSL https://prime.plane.so/releases//coolify-compose.yml -o coolify-compose.yml ``` ::: warning The `` value should be v1.8.2 or higher. ::: 2. Create a new project in Coolify. 3. Add a new resource. 4. Select **Docker Compose Empty** as the deployment method. 5. Copy and paste the contents of the `coolify-compose.yml` file into the editor. 6. Configure external DB, Redis, RabbitMQ and any other required environment variables in the UI. ::: warning When self-hosting Plane for production use, it is strongly recommended to configure external database and storage. This ensures that your data remains secure and accessible even if the local machine crashes or encounters hardware issues. Relying solely on local storage for these components increases the risk of data loss and service disruption. ::: * `DATABASE_URL` – Connection string for your external database. * `REDIS_URL` – Connection string for your external Redis instance. * `AMQP_URL` – Connection string for your external RabbitMQ server. 7. Deploy to launch your Plane instance. Once the deployment is complete, your Plane instance should be accessible on the configured domain. 8. If you've purchased a paid plan, [activate your license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) to unlock premium features. --- --- url: 'https://developers.plane.so/self-hosting/methods/portainer.html' description: >- Deploy and manage Plane using Portainer container management UI. Visual Docker management with stack deployment and monitoring. --- # Deploy Plane with Portainer This guide shows you the steps to deploy a self-hosted instance of Plane using Portainer. ## Install Plane ### Prerequisites * Before you get started, make sure you have a Portainer environment set up and ready to go. * Your setup should support either amd64 or arm64 architectures. ### Procedure 1. **Download the required deployment files** `portainer-compose.yml` – Defines Plane's services and dependencies. ```bash curl -fsSL https://prime.plane.so/releases//portainer-compose.yml -o portainer-compose.yml ``` `variables.env` – Stores environment variables for your deployment. ```bash curl -fsSL https://prime.plane.so/releases//variables.env -o plane.env ``` ::: warning The `` value should be v1.8.2 or higher. ::: 2. Click **+ Add stack** on Portainer. 3. Copy and paste the contents of `portainer-compose.yml` into the editor. 4. Load environment variables from the `variables.env` file. 5. **Configure environment variables**\ Before deploying, edit the following variables: * `DOMAIN_NAME` – (required) Your application's domain name. * `SITE_ADDRESS` – (required) The full domain name (FQDN) of your instance. * `MACHINE_SIGNATURE` – (required) A unique identifier for your machine. You can generate this by running below code in terminal: ```sh sed -i 's/MACHINE_SIGNATURE=.*/MACHINE_SIGNATURE='$(openssl rand -hex 16)'/' plane.env ``` * `CERT_EMAIL` – (optional) Email address for SSL certificate generation (only needed if you're setting up HTTPS). 6. **Configure external DB, Redis, and RabbitMQ** ::: warning When self-hosting Plane for production use, it is strongly recommended to configure external database and storage. This ensures that your data remains secure and accessible even if the local machine crashes or encounters hardware issues. Relying solely on local storage for these components increases the risk of data loss and service disruption. ::: * `DATABASE_URL` – Connection string for your external database. * `REDIS_URL` – Connection string for your external Redis instance. * `AMQP_URL` – Connection string for your external RabbitMQ server. 7. Click **Deploy the stack**. That's it! Once the deployment is complete, Plane should be up and running on your configured domain. 8. If you've purchased a paid plan, [activate your license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) to unlock premium features. --- --- url: 'https://developers.plane.so/self-hosting/govern/instance-admin.html' description: >- Configure Plane instance admin settings. Learn about God mode and administrative controls for self-hosted Plane. --- # Instance admin and God mode An instance is a single self-managed installation of Plane on a private cloud or server that the `Instance admin` controls and administers. A single instance can house multiple workspaces. ::: info There may also be cases where a user IRL is running multiple instances, e.g., when using Plane for several clients. An `Instance admin` role will have to be declared for each of those instances, but it is okay to use the same email address for all of them. ::: This role lets instance admins access `/god-mode`, a route for features that help them administer and govern their Plane instance better for all users of that instance. ::: tip New instances allow skipping going to God Mode and setting up your workspace instead. Whatever you choose after secure instance set-up, we highly recommend coming quickly to /god-mode to set up at least your SMTP server so your users can start getting invite emails to projects. ::: ## Settings God Mode features a few screens as shown below. ### General The General settings page allows you to view or configure core instance details and telemetry preferences. Here’s what you can manage: * **Name of instance**\ Customize the name of your instance. * **Email**\ Displays the instance admin email address. * **Instance ID**\ Displays a unique identifier for your instance. * **Chat with us**\ Enable or disable in-app chat support for users. Disabling telemetry automatically turns this off. * **Let Plane collect anonymous usage data**\ Plane collects anonymized usage data (no PII) to help improve features and overall experience. You can turn this off anytime. See [Telemetry](/self-hosting/telemetry) for more info. ![](/images/instance-admin/god-mode-general.webp#hero) ### Email Set up your SMTP server here so you can send essential emails—password resets, exports, changes to your instance—and Plane-enabled emails—onboarding, tips and tricks, new features— to all your users. [Learn more here](/self-hosting/govern/communication). ![](/images/instance-admin/god-mode-email.webp#hero) ### Authentication Control what SSO and OAuth services your users can use to sign up and log in to your Plane instance. You can also toggle unique code and password logins on and off from here. [Learn more here](/self-hosting/govern/authentication). * **Allow anyone to sign up without an invite**\ Toggle this setting off if you want your users to join the instance only if they receive an invite. Once SSO is configured, instance admins can also use SSO to log in to God Mode. ::: info This is where you will see new SSO services and custom OAuth configs in the future. ::: ![](/images/instance-admin/god-mode-authentication.webp#hero) ### Workspaces The Workspaces section allows you to manage all workspaces within your Plane instance. * **View all Workspaces**\ Access a complete list of workspaces on your instance. * **Create Workspaces**\ You can create new workspaces directly from this section. If workspace creation is restricted, only the instance admin will have this ability. * **Restrict Workspace creation**\ Toggle the **Prevent anyone from creating a workspace** option to prevent anyone else from creating workspaces. Once enabled, only you (the instance admin) can create new workspaces. To add users to a workspace, you will need to [invite them](https://docs.plane.so/core-concepts/workspaces/members#add-member) after creating it. ::: info Workspace deletion is currently not supported. ::: ![](/images/instance-admin/god-mode-workspaces.webp#hero) ### User management View and manage all users across the instance, invite instance admins, and control access to God Mode. ![User management](/images/instance-admin/user-management.webp#hero) * View all users with their account type, status, and joining date * Invite new instance admins * Grant or remove admin access for existing users * Remove users from the instance See [User management](/self-hosting/manage/manage-instance-users) for details. ### Images in Plane You can use your own third-party libraries to update images in project settings. Configure your Unsplash key here. When we add more image libraries, they will show up here. ![](/images/instance-admin/god-mode-images.webp#hero) ## FAQs ::: details How do you know who an Instance admin is? Whoever spins up the instance or upgrades to v0.14, we assume, is the instance admin. When you see Let's secure your instance, enter your email-password combo. If you are already using Plane with those credentials, you will be logged in and will see /god-mode features. If not, we will create a new user on your local instance and you will see /god-mode. Our shrewd guess right now is users are technical enough to upgrade to or bring up a new instance with v0.14 are instance admins. If there’s a case where this isn’t true, please reach out to us before you upgrade or set up your fresh instance. ::: ::: details What if I don’t complete secure instance set-up at the time of the upgrade? We strongly recommend completing set-up at upgrade so your regular users can access Plane without trouble. Because we are introducing several sensitive admin features in `God Mode`, we will show an instance-not-set-up screen to your regular users until such a time that you can complete the setup. ![success-on-setup-existing-instances-self-hosted](/images/faq-2.png) ::: ::: details What has changed with how existing regular users of my instance log in? All existing users will log in with their usual email address-password combos if they are already doing it. If they haven’t been using a password when not OAuthing into Plane, they will now need to. If OAuth is enabled, users can continue using your OAuth methods. New users will need to choose a password or OAuth into Plane. ::: ::: details What will happen to the default captain@plane.so account that you shipped so far? For all new instances, there won’t be a `captain@plane.so` account. Instance set-up will allow you to set up a workspace and set workspace and project admins. For existing instances, the instance admin’s email will be added to each project with the same permissions as `captain@plane.so’s` so you can remove that email completely from your workspaces and projects. ::: ::: details This is unreal, but I have an instance that has a /god-mode path already. I can’t access my Plane instance. Help! That is unreal! Please reach out to us immediately on [support](https://discord.com/login?redirect_to=%2Fchannels%2F1031547764020084846%2F1094927053867995176) or on our [Discord](https://discord.com/invite/A92xrEGCge) and mark your message urgent. We will help you get your instance back pronto. ::: ::: details How will emails for password resets and onboarding be sent to users of my instance(s)? We have always let you configure your own SMTP server to send emails from within your instance. It’s also why we are being deliberate about leading the instance admin of an existing instance to `/god-mode` first. After completing secure instance set-up now, you can configure your SMTP server on the UI instead of via `.env` variables. We strongly recommend you do that to avoid password-reset failures and failures in email delivery. Please [reach out](https://discord.com/login?redirect_to=%2Fchannels%2F1031547764020084846%2F1094927053867995176) to us on [Discord](https://discord.com/invite/A92xrEGCge) if you haven’t set up SMTP and are facing troubles with your users logging in. ::: ::: details Why are you introducing passwords for app.plane.so users? What’s happening with unique links to sign up and sign in? Unique links are secure and relatively easier, but we have heard from enough of our Cloud users that they would like to log in using a more permanent and easier method. Should you want to continue using unique codes, you are covered. We will keep that option alive for good. While using Google or GitHub are good options already, not all of you would want to use them. For those that prefer a password and would like to do away with codes, we want to make that option available. ::: ::: details Is there a God Mode for Cloud admins, too? Not now, but soon enough, there will be a `God Mode` for Cloud admins. ::: --- --- url: 'https://developers.plane.so/self-hosting/govern/authentication.html' description: >- Configure authentication methods for self-hosted Plane. Setup OAuth, SSO, SAML, OIDC, LDAP and other authentication providers. --- # Overview Plane offers several methods you can choose from to let your users log in to your Plane instance. Configure these methods in Authentication on /god-mode of your instance. ## Authentication methods ### Unique code Plane lets your users log in with codes sent over email. This is disabled if SMTP is not configured for your instance. See [Communication](https://app.plane.so/plane/projects/e3ea12b0-62e3-4b8d-8ada-3379f4efc563/pages/e83af23e-b120-47b0-b241-2bee39037505) to set up SMTP if you wish to enable unique codes. ### Passwords Your users can log in with passwords that they or you set for them. This is toggled on when SMTP isn't configured for your instance. Disable it if you would like to use another authentication method below. --- --- url: 'https://developers.plane.so/self-hosting/govern/google-oauth.html' description: >- Setup Google OAuth authentication for Plane. Step-by-step guide to enable Google sign-in for your self-hosted instance. --- # Google OAuth Plane already ships with out-of-the-box support for Google OAuth. This is the easiest option to configure for Google Workspace users. ## Configure Plane as an app on Google API Console First, you will need to identify Plane as an approved OAuth app to Google. 1. Go to the [Google API console](https://console.cloud.google.com/apis) and create a new project. 2. Navigate to the **OAuth consent screen** under **APIs & Services**. Choose how you want to configure and register the Plane app, including your target users, and click **Create**. ![](/images/authentication/google/google-auth-1.png) 3. Configure the OAuth consent screen with information about the app. ![](/images/authentication/google/google-auth-2.png) 4. Navigate to the **Credentials** screen, click **Create Credentials**, and select **OAuth client ID** from the options given. ![](/images/authentication/google/google-auth-3.png) 5. Select **Web application** under the **Application type** dropdown list. Update the following fields. 1. **Authorized JavaScript origins**\ The HTTP origins that host your web application, e.g., `https://app.plane.so` 2. **Authorized redirect URIs**\ Append the path that users should be redirected to after they have authenticated with Google. `https:///auth/google/callback` and `https:///auth/mobile/google/callback/` where `` is your self-hosted instance's domain. 3. Click **Create**. 4. Get the Client ID and Client secret under **OAuth 2.0 Client IDs** on the **Credentials** screen. ![](/images/authentication/google/google-auth-4.png) ## Configure Plane ![Google Oauth Configuration](/images/custom-sso/google-oauth.png) 1. Go to `Google` on the Authentication screen of `/god mode`. 2. Add the client ID + the client secret from Google API Console. 3. Click `Save `. Your Plane instance should now work with `Sign in with Google`. ::: info We don't restrict domains in with Google OAuth yet. It's on our roadmap. ::: --- --- url: 'https://developers.plane.so/self-hosting/govern/github-oauth.html' description: >- Configure GitHub OAuth authentication for Plane. Enable GitHub sign-in for your self-hosted Plane instance. --- # Github OAuth Plane supports GitHub OAuth so your users can sign-in with GitHub instead. ## Configure Plane as an OAuth app on GitHub 1. Log in to your [GitHub account](https://github.com/). 2. Click your profile's avatar and navigate to **Settings.** 3. Click **Developer Settings** and then **OAuth Apps**. ![](/images/authentication/github/github-auth-1.png) 4. Click **Register a new application**. 5. Configure the following OAuth credentials for your Plane app. 1. **Homepage URL**\ The domain, with HTTPS, on which you host Plane, e.g., `https://app.plane.so` 2. **Authorization Callback URL**\ Append the path that users should be redirected to after they have authenticated with GitHub. e.g., `https:////auth/github/callback/` and `https:///auth/mobile/github/callback/` where `` is your self-hosted instance's domain. 6. Click `Register application` to save it. ![](/images/authentication/github/github-auth-2.png) 7. Find the app you just registered and click through to find the client ID and the client secret. You will need this for the next steps. ## Configure Plane ![GitHub Oauth Configuration](/images/custom-sso/github-oauth.png) 1. Go to `GitHub` on the Authentication screen of `/god mode`. 2. Add the client ID + the client secret from the GitHub app you just registered. 3. Click `Save `. Your Plane instance should now work with GitHub sign-in. --- --- url: 'https://developers.plane.so/self-hosting/govern/oidc-sso.html' description: >- Setup OIDC SSO authentication for Plane. Configure OpenID Connect single sign-on for enterprise authentication. --- # OIDC SSO Plane enables custom SSO via any identity provider with an official and supported implementation of OIDC standards. This page cites examples from Okta, but we will soon publish provider-specific instructions in phases. ## OIDC You will need to configure values on your IdP first and then on Plane later. ### On your preferred IdP Create a Plane client or application per your IdP's documentation and configure ↓. ::: tip `domain.tld` is the domain that you have hosted your Plane app on. ::: | **Config** | **Key** | | ------------ | ------------------------------------------ | | Origin URL | `http(s)://domain.tld/auth/oidc/` | | Callback URL | `http(s)://domain.tld/auth/oidc/callback/` | | Logout URL | `http(s)://domain.tld/auth/oidc/logout/` | ### On Plane Go to `/god-mode/authentication/oidc` on your Plane app and find the configs ↓. ::: tip Your IdP will generate some of the following configs for you. Others, you will specify yourself. Just copy them over to each field. ::: ![OIDC Configuration](/images/custom-sso/oidc-oauth.png) * Copy the `CLIENT_ID` for the Plane client or app you just created over from your IdP and paste it in the field for it. With providers like Keycloak, you have to choose a unique ID per app your configure. With providers like Okta and Auth0, you copy over the generated ID over to Plane. Typically, you will find it on the Plane application Home or Settings page on your IdP. * Copy the `CLIENT_SECRET` for the Plane client or app you created over from your IdP and paste it in the field for it. The secret is usually auto-generated and you just need to copy it over from the Plane app or client's Home or Settings page. * Copy the `TOKEN URL` from your IdP and paste it into the field for it on `/god-mode/authentication/oidc/`.\ Typically used to maintain user authentication and to persist it with refreshes, this URL lives in the `.well-known/` directory for the Plane app or client on your IdP. * Copy the `User info URL` from your IdP and paste it into the field for it on `/god-mode/authentication/oidc/`. Used to get an authenticating user's information from the IdP. Plane requires the `email` field for user authentication. The `first_name` and `last_name` fields are optional but recommended for a complete user profile. This URL can be copied from the `.well-known/` directory. * Copy the `Authorize URL` over from the `.well-known/` directory and paste it into the field for it on Plane's `/god-mode/authentication/oidc/`.\ This is the URL that Plane's login screen redirects to when your users click `Sign up with ` or `Login with `. ![Login with IdP](/images/custom-sso/plane-login.png) To test if this URL is right, see if clicking the `Login with ` button brings up your IdP's authentication screen. ![Login with Okta](/images/custom-sso/okta-signin.webp#hero) * Finally, choose a name for your IdP on Plane so you can recognize this set of configs. --- --- url: 'https://developers.plane.so/self-hosting/govern/saml-sso.html' description: >- Configure SAML SSO authentication for Plane. Setup Security Assertion Markup Language for enterprise SSO. --- # SAML SSO Plane enables custom SSO via any identity provider with an official and supported implementation of SAML standards. This page cites examples from Okta, but we will soon publish provider-specific instructions in phases. ## SAML You will need to configure values on your IdP first and then on Plane later. ::: tip `domain.tld` is the domain that you have hosted your Plane app on. ::: ### On your preferred IdP Create a Plane client or application per your IdP's documentation and configure ↓. | **Config** | **Value** | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | | Entity ID Metadata that identifies Plane as an authorized service on your IdP | `http(s)://domain.tld/auth/saml/` | | ACS URL Assertion Consumer service that your IdP will redirect to after successful authentication by a user This is roughly the counterpart of the `Callback URL` in SAML set-ups. | `http(s)://domain.tld/auth/saml/callback/` Plane supports HTTP-POST bindings. | | SLS URL Single Logout Service that your IdP will recognize to end a Plane session when a user logs out This is roughly the counterpart of the `Logout URL` in SAML set-ups. | `http(s)://domain.tld/auth/saml/logout/` | ::: tip When setting these values up on the IdP, it’s important to remember Plane does not need to provide a signing certificate like other service providers. ::: ### Let your IdP identify your users on Plane. | **Config** | **Value** | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Name ID format | emailAddress By default, your IdP should send back a username, but Plane recognizes email addresses as the username. Set the value to the above so Plane recognizes the user correctly. | ### Set additional attribute values. By default, your IdP will send the value listed under `Property`. You have to map it to the SAML attribute Plane recognizes. | **Default property value** | **Plane SAML attribute** | | -------------------------- | ------------------------ | | user.firstName | first\_name | | user.lastName | last\_name | | user.email | email | ::: info **first\_name** and **last\_name** are optional but recommended for complete user profiles. If these are not provided, Plane will create the user account with just the email address. ::: ::: tip Depending on your IdP, you will have to find both the `Name ID format` and the three other user identification properties on different screens. Please refer to your IdP's documentation when configuring these up on your IdP. Additionally, you may have to configure the IdP to sign assertions. Irrespective of that, you have to copy the signing certificate from the IdP. ::: ### On Plane ![SAML Configuration](/images/custom-sso/saml-oauth.png) ::: tip You will find all of the values for the fields below in the `/metadata` endpoint your IdP generates for the Plane app or client. ::: * Copy the `ENTITY_ID` for the Plane client or app you just created over from your IdP and paste it in the field for it. * Copy the `SSO URL` for the Plane client or app from your IdP and paste it in the field for it. This will bring up the IdP's authentication screen for your users. ![SSO URL](/images/custom-sso/okta-signin.webp#hero) * Copy the `SLS URL` for the Plane client or app from your IdP and paste it in the `Logout URL` field on Plane's `/god-mode/authentication/saml/`. * Add the name of the IdP that you want to show on your Plane instance's log-in or sign-up screens. ![Log-in Screen](/images/custom-sso/instance-login.png) * Finally, paste the signing certificate from your IdP that you got in the last step of setting up your Plane client or app on your IdP above and paste it in the field for it. --- --- url: 'https://developers.plane.so/self-hosting/govern/ldap.html' description: >- Setup LDAP authentication for Plane. Configure Lightweight Directory Access Protocol for directory-based authentication. --- # LDAP authentication LDAP (Lightweight Directory Access Protocol) authentication lets your team sign in to Plane using their existing corporate credentials. Instead of creating separate Plane passwords, users authenticate through your organization's directory service. ## Before you begin You'll need: * Plane Commercial Edition with an active Enterprise Grid license. * Don't have an Enterprise license? Contact Sales at to get started. * Already have a license? See how to [activate your Enterprise license](/self-hosting/manage/manage-licenses/activate-enterprise). * Connection details for your LDAP server. * A service account on your LDAP server with read-only access to the user directory. ## Configure LDAP authentication 1. Sign in to your Plane instance in [God Mode](/self-hosting/govern/instance-admin). ![Turn on LDAP](/images/ldap/enable-ldap.webp#hero) 2. Select **Authentication** from the left pane. 3. Click **Configure** next to **LDAP** at the bottom of the page. 4. Enter your LDAP server details. ![LDAP configuration](/images/ldap/ldap-configuration.webp#hero) * **Server URI (required)** This is the address of your LDAP server. Include the protocol and port number. **Format:** * For unencrypted connections: `ldap://hostname:389` * For encrypted connections (recommended): `ldaps://hostname:636` **Examples:** ``` ldap://ldap.company.com:389 ldaps://ad.company.com:636 ldap://192.168.1.100:389 ``` * **Bind DN (required)** This is the username of the service account that Plane will use to search your directory. Think of it as Plane's "read-only" account on your LDAP server. The format varies depending on your directory service: **Active Directory examples:** ``` cn=PlaneService,ou=Service Accounts,dc=company,dc=com plane-svc@company.com ``` **OpenLDAP examples:** ``` cn=admin,dc=example,dc=com cn=readonly,ou=services,dc=example,dc=com ``` * **Bind Password (required)** Enter the password for your service account (Bind DN). Plane encrypts and stores this securely in its database. * **User Search Base (required)** This defines where in your directory Plane should look for users. Think of it as the "starting folder" for user searches. Use the most specific path possible for better performance. **Examples:** ``` ou=users,dc=example,dc=com ou=employees,ou=people,dc=company,dc=com cn=users,dc=company,dc=local ``` * **User Search Filter (optional)** This tells Plane how to find users when they try to sign in. Use `{username}` as a placeholder - Plane replaces it with whatever the user types in the login field. **Common filters by directory type:** | Directory Type | Filter | What it does | | ---------------- | -------------------------------- | -------------------------------- | | OpenLDAP | `(uid={username})` | Searches by user ID | | Active Directory | `(sAMAccountName={username})` | Searches by Windows login name | | Active Directory | `(userPrincipalName={username})` | Searches by email-style username | | Any | `(mail={username})` | Searches by email address | **Default:** If you don't specify a filter, Plane uses `(uid={username})`. **Combined filter example:**\ If you want users to sign in with either their username OR email: ``` (|(uid={username})(mail={username})) ``` * **User Attributes (optional)** List the LDAP attributes Plane should retrieve to create user profiles. Plane uses these to populate the user's display name and email in Plane. **How Plane maps attributes:** | Plane needs | LDAP provides (in order of preference) | | ------------- | ------------------------------------------------------------ | | Email address | `mail`, `userPrincipalName` | | First name | `givenName`, or first part of `cn` if `givenName` is missing | | Last name | `sn`, or last part of `cn` if `sn` is missing | **Recommended setting:** ``` mail,cn,givenName,sn,userPrincipalName,displayName ``` **Default:** If you don't specify attributes, Plane uses `mail,cn,givenName,sn`. * **Provider Name (optional)** This is the label that appears on Plane's login button. Choose something your team will recognize. **Examples:** * `Corporate Directory` * `Company SSO` * `Active Directory` **Default:** If you don't specify a name, Plane shows `LDAP`. The login button will display as: **"Sign in with \[Provider Name]"** 5. Click **Save changes** to apply your LDAP settings. Plane will validate the connection to your LDAP server. 6. Users will see **Sign in with LDAP** on Plane's login page and can use their directory credentials to sign in. ![Sign in using LDAP](/images/ldap/sign-in-ldap.webp#hero) ## How LDAP authentication works LDAP authentication in Plane works through a two-phase process. First, Plane locates the user in your directory, then it verifies their credentials. This separation is fundamental to how LDAP works and explains why you need both a service account (Bind DN) and the user's own credentials. ### The service account pattern Unlike simpler authentication systems where you might directly check a username and password against a database, LDAP uses what's called a "bind" operation. Plane needs to authenticate twice: once as itself (using the Bind DN) to search your directory, and once as the user to verify their password. This is why you configure a Bind DN and password - it's Plane's identity on your LDAP server. Think of it as Plane introducing itself before asking about your users. The Bind DN only needs read access because Plane is just looking up information, never modifying your directory. ### The authentication flow When a user tries to sign in, here's what happens behind the scenes: **Connection and service authentication**\ Plane connects to your LDAP server using the Server URI, then authenticates using the Bind DN credentials. If this fails, no users can sign in; the service account must work first. **User search**\ Now authenticated, Plane searches for the user starting from the User Search Base and applying the User Search Filter. For example, if a user enters "jsmith" and your filter is `(uid={username})`, Plane searches for `(uid=jsmith)`. The search returns the user's distinguished name (DN), their full path in the directory. **User authentication**\ Plane now attempts to bind again using the user's DN and their entered password. If the bind succeeds, the password is correct. This is why LDAP authentication is secure. Plane never stores user passwords; they go straight to your LDAP server for verification. **Profile creation**\ Once authentication succeeds, Plane retrieves the User Attributes you configured (like email, first name, last name) from the user's LDAP record. If this is the user's first time signing in, Plane creates their profile using this information. If they've signed in before, Plane updates their profile with any changes from LDAP. **Session establishment**\ Finally, Plane creates a session for the user and redirects them into the workspace. From this point on, the user's session works identically to any other Plane session. The LDAP interaction is complete. ### Why search filters matter The User Search Filter determines sign-in flexibility. A simple filter like `(uid={username})` requires exact usernames, but you can make it more flexible: `(mail={username})` lets users sign in with email `(|(uid={username})(mail={username}))` allows either username or email Filters can also restrict access: `(&(uid={username})(memberOf=cn=plane-users,ou=groups,dc=company,dc=com))` only permits specific group members. ### The role of user attributes User Attributes tell Plane which LDAP fields to retrieve after authentication. This is separate from the search filter. The filter finds the user, the attributes populate their profile. Plane specifically looks for email addresses (required) and names (optional). If your LDAP server uses different attribute names, you need to include them. --- --- url: 'https://developers.plane.so/self-hosting/govern/reset-password.html' description: >- Reset user and admin passwords for self-hosted Plane. Recover access to your instance using CLI commands or database operations. --- # Reset password Users can reset their password through the terminal of the Plane application. You need to login to backend docker container and run the below command for resetting a user’s password. 1. Get the container id for **plane-api**. ```bash docker ps ``` 2. Log in to the container. ```bash docker exec -it /bin/sh ``` 3. Run the reset password command. ```bash python manage.py reset_password ``` ::: tip The email should be of an already existing user on the Plane application. If the email is not attached to any user the command will throw an error. ::: --- --- url: 'https://developers.plane.so/self-hosting/govern/communication.html' description: >- Configure SMTP email settings for Plane. Setup email notifications and communication for your self-hosted instance. --- # Configure SMTP for email notifications Either during your set-up or sometime later, you will want to set SMTP settings to let your users get emails to reset passwords, onboard themselves right, and get notifications for changes, and receive exports of your data. ::: info Plane currently supports SMTP authentication only via email and password. OAuth-based SMTP configurations aren’t supported. ::: ## Configuration Plane offers an interface to configure Simple Mail Transfer Protocol (SMTP) and SSL for encrypted email communication. Navigate to `Email` in `/god-mode`and you will see ↓. ![](/images/instance-admin/email-settings.png) * **Host**\ The address of your SMTP server. * **Port**\ The port for outgoing emails. * **Sender email address**\ The email address you wish to use as the sender of emails. * **Email security**\ Toggle `TLS` or `SSL` as the email security layer for your emails. If you do not wish to use either of them, you can choose the `No email security` option. * **Authentication**\ You can configure the username and password to authenticate the SMTP server to send emails. It's an optional configuration, but we would advise you to provide authentication details for a secure email delivery experience. * **Username**\ Specify the username for the SMTP configuration here. * **Password**\ Specify the password for the SMTP configuration here. ::: tip **Google Workspaces** If your Plane instance is not accessible on the internet, Gmail may block profile photos or other embedded images in email notifications. This occurs because Gmail uses Google's secure image proxy to serve images for security purposes. To resolve this issue, you must configure the Image URL proxy allowlist in your Google Workspace settings to include your Plane instance's URL. Refer to Google’s documentation for instructions: [Allowlist image URLs](https://support.google.com/a/answer/3299041?hl=en). ::: ## Configuration for popular email services providers ### Amazon SES 1. Sign in to [**https://console.aws.amazon.com/ses**](https://console.aws.amazon.com/ses). 2. Navigate to **SMTP Settings** in the sidebar. 3. Click **Create My SMTP Credentials**. 4. Follow prompts to create a user in the **Create User for SMTP** dialog box, then click **Create**. 5. Select **Show User SMTP Credentials** to view the user's SMTP credentials. 6. Return to your Plane instance's `/god-mode` and enter the obtained details. Ensure to review [**email quotas**](https://docs.aws.amazon.com/ses/latest/dg/quotas.html) for your Amazon SES server. Consider managing email recipients using groups to optimize usage. --- --- url: 'https://developers.plane.so/self-hosting/govern/database-and-storage.html' description: >- Configure external database and storage for Plane. Setup PostgreSQL, Redis, and S3-compatible storage services. --- # Configure external services The Prime CLI lets you easily configure your Commercial Edition instance, providing options to customize the PostgreSQL database, Redis, external storage, and other advanced settings. ::: warning **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. ::: 1. Run the Prime CLI with ↓: `sudo prime-cli` 2. Once the CLI is running, enter `configure`, which will guide you through a step-by-step form where you can specify the following: * `Listening port`\ Define the port for the built-in reverse proxy.\ *Default*: `80` * `Max file-upload size`\ Set the maximum file size (in MB) that members can upload. *Default*: `5 MB` * `External Postgres URL`\ Provide the URL of your external PostgreSQL instance if you want to switch from the default Plane configuration. *Default*: `Postgres 15.5` in the Docker container. ::: warning Don’t use a database on your local machine. If you use `localhost` in the URL, it won’t work. Make sure to use a database hosted on a network-accessible server. Avoid using special characters in your PostgreSQL password. ::: * `External Redis URL`\ Specify the URL of your external Redis instance to override the default Redis configuration.\ *Default*: `Redis 7.2.4` * `External storage`\ Plane currently supports only S3 compatible storages.\ *Default*: `MinIO` 1. Ensure your IAM user has the following permissions on your S3 bucket. * **s3:GetObject**\ To access the objects. * **s3:PutObject**\ To upload new assets using the presigned url. 2. Configure the CORS policy on your bucket to enable presigned uploads. Use the example policy below, making sure to replace `` with your actual domain. ``` [ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET", "POST", "PUT", "DELETE", "HEAD" ], "AllowedOrigins": [ "", ], "ExposeHeaders": [ "ETag", "x-amz-server-side-encryption", "x-amz-request-id", "x-amz-id-2" ], "MaxAgeSeconds": 3000 } ] ``` 3. Switch to your external storage by providing the following values: * S3 access key ID  * S3 secret access key * S3 bucket name * S3 region  * S3 endpoint URL 3. After confirming your choices, your instance will automatically restart with the updated configuration. ::: details Community Edition To configure external Postgres, Redis, and S3 storage for the Plane Community Edition, you’ll need to adjust several environment variables in the plane.env file. Follow this guide to set up each component using the correct values for your external services. 1. Open the `plane.env` file on your server where Plane is installed. 2. In the **DB SETTINGS** section, update the variables to connect to your external Postgres instance: ```bash # DB SETTINGS PGHOST=your-external-postgres-host # Replace with the hostname or IP address of your Postgres server. PGDATABASE= # Leave blank when using external database. POSTGRES_USER=your-postgres-username # The username to access Postgres. POSTGRES_PASSWORD=your-postgres-password # Password for the Postgres user. POSTGRES_DB=your-database-name # The name of the database Plane should connect to. POSTGRES_PORT=5432 # Port where Postgres is accessible (usually 5432). PGDATA=/var/lib/postgresql/data # No need to change this for external Postgres. DATABASE_URL= # Leave this empty if you're providing values for the variables above. If you choose to use the DATABASE_URL, you can leave all the other database-related variables empty. ``` ::: warning Don’t use a database on your local machine. If you use `localhost` in the URL, it won’t work. Make sure to use a database hosted on a network-accessible server. Avoid using special characters in your PostgreSQL password. ::: 3. In the **REDIS SETTINGS** section, update the variables to connect to your external Redis instance: ```bash # REDIS SETTINGS REDIS_HOST=your-external-redis-host # Hostname or IP of the Redis server. REDIS_PORT=6379 # Port where Redis is accessible (default is 6379). REDIS_URL= # Leave this empty if you're providing values for the variables above. If you choose to use the REDIS_URL, you can leave all the other redis-related variables empty. ``` 4. In the **DATA STORE SETTINGS** section, update the variables for any S3-compatible storage: ```bash # DATA STORE SETTINGS USE_MINIO=0 # Set to 0 if using an external S3, 1 if using MinIO (default). AWS_REGION=your-s3-region # For AWS, set the region, e.g., "us-west-1". AWS_ACCESS_KEY_ID=your-s3-access-key # Access key for S3. AWS_SECRET_ACCESS_KEY=your-s3-secret-key # Secret key for S3. AWS_S3_ENDPOINT_URL=https://your-s3-endpoint # URL for S3 API endpoint (e.g., "https://s3.amazonaws.com" for AWS). AWS_S3_BUCKET_NAME=your-s3-bucket-name # Name of the S3 bucket for storing Plane data. MINIO_ROOT_USER= # Leave blank when using external S3. MINIO_ROOT_PASSWORD= # Leave blank when using external S3. BUCKET_NAME= # Leave blank when using external S3. FILE_SIZE_LIMIT=5242880 # Set maximum file upload size in bytes (5MB here). ``` ::: tip Running Plane on Amazon EKS with the Helm charts instead? You can leave the access key and secret out altogether and let Plane assume an IAM role. See [Use IRSA or EKS Pod Identity for S3 storage](/self-hosting/govern/aws-workload-identity). ::: 5. Save your changes to the `plane.env` file. 6. Restart Plane services to apply the new settings using the `setup.sh` script. ::: --- --- url: 'https://developers.plane.so/self-hosting/govern/custom-domain.html' description: >- Configure custom domain for self-hosted Plane. Setup your own domain name for your Plane instance. --- # Configure custom domain During installation, you configure a domain for your instance. If you need to change that domain later, whether you're moving to a production domain, switching to a different hostname, or updating your DNS configuration, this guide walks you through the process. :::info **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. If you're running Kubernetes or another deployment method, the environment variable names are the same, but the configuration method differs based on your setup. ::: :::warning **Plan for downtime**\ Changing domains requires restarting Plane services. Your instance will be unavailable for a few minutes during the restart. Plan accordingly or notify your users. ::: ## Check current domain configuration First, see which environment variables currently reference your old domain. This helps you identify exactly what needs updating. ```bash cat /opt/plane/plane.env | grep ``` **Example output:** ```ini DOMAIN_NAME=localhost SITE_ADDRESS=http://localhost WEB_URL=http://localhost CORS_ALLOWED_ORIGINS=http://localhost,https://localhost ``` This shows you all the variables that contain your current domain. You'll update each of these in the next step. ## Update domain in environment file 1. Open the Plane environment configuration file: ```bash vim /opt/plane/plane.env ``` 2. Find and update these environment variables with your new domain: * **DOMAIN\_NAME** Set this to your bare domain name without protocol: ```ini DOMAIN_NAME=plane.company.com ``` Don't include `http://` or `https://` here, just the hostname. * **SITE\_ADDRESS** Set this to your full domain URL: ```ini SITE_ADDRESS=https://plane.company.com ``` Include the protocol (`https://` for SSL, `http://` if you haven't set up SSL yet). * **WEB\_URL** This should match your SITE\_ADDRESS: ```ini WEB_URL=https://plane.company.com ``` Again, include the full protocol. **CORS\_ALLOWED\_ORIGINS** List all domains that should be allowed to make cross-origin requests to your Plane instance. This typically includes both HTTP and HTTPS versions of your domain: ```ini CORS_ALLOWED_ORIGINS=https://plane.company.com,http://plane.company.com ``` Separate multiple entries with commas, no spaces. If you have multiple domains or subdomains that need access, add them all here. ## Restart Plane services Apply your configuration changes by restarting Plane: ```bash sudo prime-cli restart ``` This process typically takes a few minutes. You'll see output indicating the status of each service as it restarts. ::: details Community Edition Our steps differ slightly depending on whether you are hosting on a public IP or a private/internal IP. Follow the steps listed below. #### Update configuration in .env file Open your project's `.env` file in a text editor. This file contains configuration settings for your application. Locate the following lines: ``` WEB_URL= CORS_ALLOWED_ORIGINS= ``` Replace `` with your actual domain name, including the protocol (http:// or https://). For example: ``` WEB_URL=https://example.com CORS_ALLOWED_ORIGINS=https://example.com ``` If you are hosting Plane on a public IP, then follow the steps here. However, if you are hosting Plane on an internal IP then follow these steps. #### Set DNS A record (for public IP) If your server has a public IP address, you need to configure the DNS A record to point to this IP address. This allows users to access your application using your custom domain name. Here’s how to do it: * Log in to your domain registrar's website or DNS hosting provider. * Navigate to the DNS management section. * Find the option to edit your domain's DNS records. * Add a new A record with the hostname set to `@` (or your subdomain if applicable) and the IP address set to your server's public IP address. * Save the changes. It may take some time for the DNS changes to propagate. #### Configure reverse proxy (for internal IP) If your server is behind a firewall or router and has an internal IP address, you'll need to set up a reverse proxy to route requests from your custom domain to your server. Follow these steps: * Configure a CNAME record in your domain's DNS settings that points to your reverse proxy server's hostname. This allows your domain to resolve to the reverse proxy server. * Set up reverse proxy redirection on your reverse proxy server to forward incoming requests to your server's internal IP address and port. * Depending on the reverse proxy software you're using (e.g., Nginx, Apache, etc.), the configuration process may vary. Refer to the documentation for your specific reverse proxy server for detailed instructions on setting up reverse proxy redirection. * Once the reverse proxy is properly configured, ensure that your firewall/router allows incoming traffic on the necessary ports to reach your server. By following these steps, you will be able to access your self-hosted instance of Plane using your custom domain name, whether your server has a public IP address or is behind a firewall with an internal IP address. --- --- url: 'https://developers.plane.so/self-hosting/govern/configure-ssl.html' description: >- Configure SSL/TLS certificates for Plane. Setup HTTPS encryption for secure self-hosted Plane deployment. --- # Set up SSL This guide shows you how to configure SSL/TLS certificates for your self-hosted Plane instance. Plane handles certificate provisioning and renewal automatically using Let's Encrypt. ::: info **Applies to:** Docker deployments of Plane Commercial Edition without an external reverse proxy. If you're using an external reverse proxy (nginx, Caddy, Traefik) or a load balancer, configure SSL there instead and skip this guide. ::: ## Before you begin Ensure you have: * A registered domain name pointing to your Plane server * DNS records configured (A or CNAME record pointing to your server's IP) * Ports 80 and 443 open on your server's firewall * Prime CLI installed (included with Plane Commercial Edition) ::: warning **DNS must be configured first.** Let's Encrypt validates domain ownership by making HTTP requests to your domain. Ensure your domain resolves to your server's IP address before proceeding. ::: ## Configure SSL settings ### Open the configuration file Edit your Plane environment configuration: ```bash vim /opt/plane/plane.env ``` ### Set required variables Add or update these environment variables: ```bash # SSL Configuration CERT_EMAIL=admin@yourcompany.com SITE_ADDRESS=plane.yourcompany.com WEB_URL=https://plane.yourcompany.com ``` **Variable explanations:** **CERT\_EMAIL**\ A valid email address for Let's Encrypt certificate registration. Let's Encrypt uses this to send renewal reminders and important notices about your certificates. **SITE\_ADDRESS**\ Your domain name **without** protocol. Use only the domain (e.g., `plane.company.com`), not `https://plane.company.com`. Plane's built-in proxy uses this to request certificates from Let's Encrypt. **WEB\_URL**\ Your full Plane URL **with** the `https://` protocol. This tells Plane services how to construct URLs for redirects, emails, and API responses. ### DNS provider configuration (optional) If you're using Cloudflare or another DNS provider with API access, you can use DNS validation instead of HTTP validation. This is useful if: * Your server is behind a firewall that blocks port 80 * You need wildcard certificates * HTTP validation isn't working due to network restrictions **For Cloudflare:** ```bash CERT_ACME_DNS=acme_dns cloudflare ``` Replace `` with your Cloudflare API token. Create one at **Cloudflare Dashboard** → **My Profile** → **API Tokens** with **Zone:DNS:Edit** permissions. **For other DNS providers:** Check the [acme.sh DNS API documentation](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) for provider-specific configuration. ## Apply SSL configuration Restart Plane to apply the SSL settings: ```bash sudo prime-cli restart ``` Prime CLI will: 1. Stop all Plane services 2. Request a new SSL certificate from Let's Encrypt 3. Configure the built-in proxy to use HTTPS 4. Restart all services with SSL enabled This process typically takes 30-60 seconds. ## Verify SSL is working Check that your Plane instance is accessible via HTTPS: ```bash curl -I https://plane.yourcompany.com ``` You should see a response with `HTTP/2 200` or `HTTP/1.1 200` and SSL-related headers. Visit your Plane instance in a browser at `https://plane.yourcompany.com`. You should see a secure connection (padlock icon) without certificate warnings. ## Using custom SSL certificates Custom SSL certificates (from a corporate CA or purchased certificates) are not currently supported in Plane's deployment. --- --- url: 'https://developers.plane.so/self-hosting/govern/integrations/bitbucket.html' description: >- Connect Bitbucket to your self-hosted Plane instance. Sync pull requests and commits with Plane work items for development workflow tracking. --- # Configure Bitbucket for Plane integration This guide walks you through setting up a Bitbucket application to enable Bitbucket integration for your Plane workspace on a self-hosted instance. Since self-hosted environments don't come pre-configured for Bitbucket, you'll need to create an OAuth consumer or application link, configure authentication, and set the necessary permissions to ensure seamless integration. This guide covers configuration for both: * **[Bitbucket Cloud](/self-hosting/govern/integrations/bitbucket#bitbucket-cloud)** The standard cloud-hosted Bitbucket service at bitbucket.org * **[Bitbucket Data Center](/self-hosting/govern/integrations/bitbucket#bitbucket-data-center)** Self-hosted Bitbucket instances for organizations with specific compliance or security requirements In this guide, you'll: 1. [Create and configure a Bitbucket application](/self-hosting/govern/integrations/bitbucket#create-bitbucket-application) 2. [Configure your Plane instance](/self-hosting/govern/integrations/bitbucket#configure-plane-instance) ::: warning **Activate Bitbucket integration** After creating and configuring the Bitbucket application and configuring the instance as detailed on this page, you'll need to [setup the Bitbucket integration](https://docs.plane.so/integrations/bitbucket) within Plane. ::: ## Create Bitbucket Application :::tabs key:bitbucket-edition \== Bitbucket Cloud {#bitbucket-cloud} Follow these steps to register an OAuth consumer in your Bitbucket workspace, set the callback URL and scopes, and then configure your Plane instance so it can sync pull requests and commits. #### Bitbucket Cloud 1. Log in to Bitbucket Cloud and navigate to your workspace. 2. Go to **Workspace Settings → Apps & Features → OAuth Consumers**. 3. Click **Add consumer** to begin the setup. 4. Provide a **Name** for your OAuth consumer. 5. Enter the following **Callback URL**, replacing `[YOUR_DOMAIN]` with your actual domain: ```bash https://[YOUR_DOMAIN]/silo/api/oauth/bitbucket/auth/callback ``` 6. Set permissions by selecting the required **Scopes**. The table below explains each scope: | Category | Permission | Explanation | | ------------- | ---------- | ---------------------------------------------------------------------- | | Account | `email` | Read the user's primary email address. | | Account | `read` | Read the user's account information and workspace memberships. | | Repositories | `read` | Read access to repositories, including source code and metadata. | | Repositories | `write` | Write access to repositories, required for creating and updating refs. | | Pull requests | `read` | Read pull requests, comments, and activity on repositories. | | Pull requests | `write` | Create and update pull requests and post comments. | | Projects | `read` | Read project metadata and repository associations. | | Issues | `read` | Read issues and their comments on repositories. | | Issues | `write` | Create and update issues and post comments. | | Webhooks | `read` | Read webhook subscriptions on repositories and workspaces. | | Webhooks | `write` | Create and manage webhook subscriptions. | 7. Click **Save** to finalize the setup. \== Bitbucket Data Center {#bitbucket-data-center} These instructions cover registering an application link on your self-hosted Bitbucket Data Center instance, setting the redirect URL, and assigning the required permissions for Plane to access your repositories. #### Bitbucket Data Center 1. Log in to your Bitbucket Data Center instance as an administrator. 2. Go to **Settings → Application Links**. 3. Click **Create link** to begin configuring a new application link. 4. Enter the URL of your Plane instance and click **Continue**. 5. Enter the following **Redirect URL**, replacing `[YOUR_DOMAIN]` with your actual domain: ```bash https://[YOUR_DOMAIN]/silo/api/oauth/bitbucket-dc/auth/callback ``` 6. Set the required **Application Permissions**: | Resource | Permission Level | Explanation | | ------------ | ---------------- | ------------------------------------------------------------------- | | Projects | `Admin` | Required to read project metadata and manage webhook subscriptions. | | Repositories | `Read` | Read access to repository metadata, branches, and commits. | | Repositories | `Write` | Write access to create refs and update repository content. | | Repositories | `Admin` | Required to manage repository-level webhooks. | 7. Click **Save** to create the application link. ::: ## Configure Plane instance :::tabs key:bitbucket-edition \== Bitbucket Cloud {#bitbucket-cloud} 1. Copy the **Key** and **Secret** from the newly created OAuth consumer. 2. Add these environment variables with the values to your Plane instance's `.env` file. ```bash BITBUCKET_CLIENT_ID= BITBUCKET_CLIENT_SECRET= BITBUCKET_WEBHOOK_SECRET= ``` 3. Save the file and restart the instance. 4. Once you've completed the instance configuration, [activate the Bitbucket integration in Plane](https://docs.plane.so/integrations/bitbucket?edition=bitbucket-cloud). ::: --- --- url: 'https://developers.plane.so/self-hosting/govern/integrations/github.html' description: >- Connect GitHub to your self-hosted Plane instance. Sync pull requests, commits, and branches with Plane work items for seamless development tracking. --- # Configure GitHub for Plane integration This guide walks you through setting up a GitHub App to enable GitHub integration for your Plane workspace on a self-hosted instance. Since self-hosted environments don’t come pre-configured for GitHub, you’ll need to set up the necessary authentication, permissions, and webhooks to ensure smooth integration. This guide covers configuration for both: * **[GitHub Cloud](/self-hosting/govern/integrations/github#github-cloud)** (github.com)\ The standard GitHub service, available at github.com. Covers all plans * **[GitHub Enterprise Cloud](/self-hosting/govern/integrations/github#github-enterprise-server)** (ghe.com)\ GitHub's managed enterprise service where your organization gets a dedicated subdomain * **[GitHub Enterprise Server](/self-hosting/govern/integrations/github#github-enterprise-server)** (self-hosted)\ A self-hosted GitHub instance deployed on your own infrastructure In this guide, you’ll: 1. [Create and configure a GitHub App](/self-hosting/govern/integrations/github#create-github-app) 2. [Set up permissions and events](/self-hosting/govern/integrations/github#set-up-permissions-and-events) 3. [Configure your Plane instance](/self-hosting/govern/integrations/github#configure-plane-instance) ::: warning **Activate GitHub integration** After creating and configuring the GitHub app and configuring the instance as detailed on this page, you'll need to [setup the GitHub integration](https://docs.plane.so/integrations/github) within Plane. ::: ## Create GitHub App To configure GitHub integration, you'll need to create a GitHub App within your organization. :::tabs key:github-edition \== GitHub Cloud {#github-cloud} Follow these steps to create a GitHub App, set callback URLs, and configure webhooks so Plane can sync PRs and commits from GitHub Cloud. #### GitHub Cloud 1. Go to **Settings > Developer Settings > GitHub Apps** in your GitHub organization. 2. Click **New GitHub App**. ![Create GitHub App](/images/integrations/github/create-github-app.webp#hero) 3. In the **Register new GitHub App** page, provide a **GitHub App name** and **Homepage URL**. ![App name and homepage URL](/images/integrations/github/app-name-homepage-url.webp#hero) 4. In the **Identifying and authorizing users** section, add the following **Callback URLS**. ```bash https:///silo/api/github/auth/callback https:///silo/api/github/auth/user/callback ``` These URLs allow Plane to verify and enable workspace connection with the Github App. ![Add Callback URL](/images/integrations/github/add-callback-url.webp#hero) :::warning Make sure to opt out of **Expire user authorization tokens** feature. ::: 5. In the **Post installation** section, add the below **Setup URL**. ```bash https:///silo/api/github/auth/callback ``` Redirects users to this URL after GitHub app installation. ![Add setup URL](/images/integrations/github/add-setup-url.webp#hero) 6. Turn on **Redirect on update**. 7. In the **Webhook** section, add the below **Webhook URL**. ```bash https:///silo/api/github/github-webhook ``` This allows Plane to receive updates from GitHub repositories. ![Add Webhook URL](/images/integrations/github/add-webhook-url.webp#hero) \== GitHub Enterprise Server {#github-enterprise-server} These steps cover hostname, callback URLs, and private key differences for on‑prem GitHub deployments. > \[!CAUTION] GitHub Enterprise Cloud (GHE.com) > On GHE.com enterprise, go to `https://.ghe.com/enterprises//settings/apps` and create a new GitHub App. #### GitHub Enterprise Server 1. Go to **Settings > Developer Settings > GitHub Apps** in your GitHub organization. 2. Click **New GitHub App**. ![Create GitHub App](/images/integrations/github/create-github-app.webp#hero) 3. In the **Register new GitHub App** page, provide a **GitHub App name** and **Homepage URL**. ![App name and homepage URL](/images/integrations/github/app-name-homepage-url.webp#hero) 4. In the **Identifying and authorizing users** section, add the following **Callback URLS**. **For Plane cloud instance** ```bash https://silo.plane.so/api/oauth/github-enterprise/auth/callback https://silo.plane.so/api/oauth/github-enterprise/auth/user/callback ``` **For Plane self-hosted instance** ```bash https:///silo/api/oauth/github-enterprise/auth/callback https:///silo/api/oauth/github-enterprise/auth/user/callback ``` These URLs allow Plane to verify and enable workspace connection with the Github App. ![Add Callback URL](/images/integrations/github/add-callback-url.webp#hero) :::warning Make sure to opt out of **Expire user authorization tokens** feature. ::: 5. In the **Post installation** section, add the below **Setup URL**. **For Plane cloud instance** ```bash https://silo.plane.so/api/oauth/github-enterprise/auth/callback ``` **For Plane self-hosted instance** ```bash https:///silo/api/oauth/github-enterprise/auth/callback ``` Redirects users to this URL after GitHub app installation. ![Add setup URL](/images/integrations/github/add-setup-url.webp#hero) 6. Turn on **Redirect on update**. 7. In the **Webhook** section, add the below **Webhook URL**. **For Plane cloud instance** ```bash https://silo.plane.so/api/github-enterprise/github-webhook ``` **For Plane self-hosted instance** ```bash https:///silo/api/github-enterprise/github-webhook ``` This allows Plane to receive updates from GitHub repositories. ![Add Webhook URL](/images/integrations/github/add-webhook-url.webp#hero) ::: ### Set up permissions and events 1. Add repository and account permissions by setting the **Access** dropdown next to each permission, as shown in the tables below. ![Setup permissions](/images/integrations/github/setup-permissions.webp#hero) **Repository permissions** | Permission            | Access level     | Purpose | | ---------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | | Issues | Read and write | Enables reading, creating, updating, closing, and commenting on issues within the repository. | | Metadata | Read-only | Provides read-only access to repository metadata, such as its name, description, and visibility. | | Pull requests | Read and write | Allows reading, creating, updating, merging, and commenting on pull requests. | **Account permissions** | Permission           | Access level     | Purpose | | ---------------------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------- | | Email addresses | Read-only | Grants access to users' email addresses, typically for notifications or communication. | | Profile | Read and write | Enables access to user profile details like name, username, and avatar. | 2. In the **Subscribe to events** section, turn on all the required events below. ![Subscribe to events](/images/integrations/github/subscribe-to-events.webp#hero) | Event                                             | Purpose | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Installation target | This is where the repositories or organizations where your GitHub App is installed. This determines which repositories Plane can sync with. | | Meta | Includes metadata about the app's configuration and setup. This is essential for maintaining integration stability. | | Issue comment | Triggers when a comment is added, edited, or deleted on an issue. Useful for keeping comments synced between Plane and GitHub. | | Issues | Triggers when an issue is created, updated, closed, reopened, assigned, labeled, or transferred. Ensures issue status and details remain consistent between Plane and GitHub. | | Pull request | Fires when a pull request is opened, closed, merged, edited, or labeled. Essential for tracking development progress. | | Pull request review | Activates when a review is submitted, edited, or dismissed. Keeps review activities aligned between Plane and GitHub. | | Pull request review comment | Fires when a review comment is added, modified, or removed. Ensures feedback is reflected across both platforms. | | Pull request review thread | Triggers when a review discussion thread is resolved or reopened. Helps maintain visibility on code review discussions. | | Push | Activates when new commits are pushed to a repository. Useful for tracking code updates and changes. | | Repository sub issues | Tracks issues within a repository that are linked to or managed by another issue. Ensures accurate synchronization of related issues. | 3. Click the **Create GitHub App** button at the bottom of the page. ## Configure Plane instance :::tabs key:github-edition \== GitHub Cloud {#github-cloud} 1. Go back to **Settings > Developer Settings > GitHub Apps**. 2. Click **Edit** on the GitHub you created. 3. In the **General** tab, under the **Client secrets** section, click **Generate a new client secret**. ![General tab](/images/integrations/github/general-tab.webp#hero) 4. Scroll down to the **Private keys** section. ![Private keys](/images/integrations/github/private-keys.webp#hero) 5. Click **Genereate a private key**. 6. Retrieve the following details from the **General** tab: * App ID * Client ID * Client secret * GitHub App name * Private key 7. Before adding the Private key as an environment variable, you'll need to convert it to base64. Since private keys are typically multi-line, they can cause parsing errors or issues when setting environment variables. To avoid this, run the following command to convert the key to base64: ```bash cat private_key.pem | base64 -w 0 ``` 8. Add these environment variables with the values to your Plane instance's `.env` file. ```bash GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= GITHUB_APP_NAME= GITHUB_APP_ID= GITHUB_PRIVATE_KEY= ``` 9. Save the file and restart the instance. 10. Once you've completed the instance configuration, [activate the GitHub integration in Plane](https://docs.plane.so/integrations/github). \== GitHub Enterprise Server {#github-enterprise-server} 1. Go back to **Settings > Developer Settings > GitHub Apps**. 2. Click **Edit** on the GitHub you created. 3. In the **General** tab, under the **Client secrets** section, click **Generate a new client secret**. ![General tab](/images/integrations/github/general-tab.webp#hero) 4. Scroll down to the **Private keys** section. ![Private keys](/images/integrations/github/private-keys.webp#hero) 5. Click **Generate a private key**. 6. Retrieve the following details from the **General** tab: * App ID * App Slug (You can find this in browser url) * Client ID * Client secret * Private key 7. Convert the Private key to convert it to base64. Since private keys are typically multi-line, they can cause parsing errors or issues when setting environment variables. To avoid this, run the following command to convert the key to base64: ```bash cat private_key.pem | base64 -w 0 ``` 8. Once you've created the app, [activate the GitHub Enterprise integration in Plane](https://docs.plane.so/integrations/github?edition=github-enterprise#connect-github-organization). ::: ## Troubleshooting ### Invalid private key This error usually occurs when the private key is not correctly generated. To fix this, follow the below steps. 1. Generate a new private key. 2. Convert the private key to base64. ```bash cat private_key.pem | base64 -w 0 ``` 3. Add the private key to the `.env` file. ```bash GITHUB_PRIVATE_KEY= ``` 4. Save the file and restart the instance. ### Unable to connect GitHub organization account or personal account This error usually occurs when the callback URL is not correctly configured or the GitHub App is not marked public. To fix this, follow the below steps. 1. Check if the callback URL is correctly configured. 2. Check if your GitHub App is marked public. ### Application secret value not found This error usually occurs when the application secret is not correctly configured. To fix this, follow the below steps. 1. Delete the `plane_app_details_github` key from redis cache. `del plane_app_details_github`. 2. Set the `SILO_BASE_URL` in env with plane self hosted url and restart the api server. `export SILO_BASE_URL=https://` 3. Run this command in api server shell `python manage.py reset_marketplace_app_secrets` to reset the application secrets. 4. Try to connect again to the organization account to Plane. ### Github integration suddenly stopped working after a while This error usually occurs when the GitHub integration is not correctly configured. To fix this, follow the below steps. 1. Make sure you've `opted out` of Server Token expiration and reconnect once again to the organization account to Plane. Check in Github App Settings > Optional Features --- --- url: 'https://developers.plane.so/self-hosting/govern/integrations/gitlab.html' description: >- Connect GitLab to your self-hosted Plane instance. Sync merge requests and commits with Plane work items for development workflow tracking. --- # Configure GitLab for Plane integration This guide walks you through setting up a GitLab application to enable GitLab integration for your Plane workspace on a self-hosted instance. Since self-hosted environments don’t come pre-configured for GitLab, you’ll need to create an application, configure authentication, and set the necessary permissions to ensure seamless integration. This guide covers configuration for both: * **[GitLab.com](/self-hosting/govern/integrations/gitlab#gitlab-cloud)** The standard cloud-hosted GitLab service * **[GitLab Self-managed](/self-hosting/govern/integrations/gitlab#gitlab-self-managed)** Self-hosted GitLab instances for organizations with specific compliance or security requirements In this guide, you’ll: 1. [Create and configure a GitLab Application](/self-hosting/govern/integrations/gitlab#create-gitlab-application) 2. [Configure your Plane instance](/self-hosting/govern/integrations/gitlab#configure-plane-instance) ::: warning **Activate GitLab integration** After creating and configuring the GitLab application and configuring the instance as detailed on this page, you'll need to [setup the GitLab integration](https://docs.plane.so/integrations/gitlab) within Plane. ::: ## Create GitLab Application :::tabs key:gitlab-edition \== GitLab Cloud {#gitlab-cloud} Follow these steps to register an application on the public GitLab service, set the redirect URI and scopes, and then configure your Plane instance so it can sync merge requests and commits. #### GitLab Cloud 1. On the left sidebar in GitLab, select your avatar. 2. Select **Preferences** tab. 3. Navigate to the **Applications** tab. 4. Click on **Add new application** to begin the setup. ![Add GitLab application](/images/integrations/gitlab/add-gitlab-application.webp#hero) 5. Provide a **Name** for your application. 6. Enter the following **Redirect URI**, replacing \[YOUR\_DOMAIN] with your actual domain: ```bash https://[YOUR_DOMAIN]/silo/api/gitlab/auth/callback ``` 7. Check the **Confidential** box. ![Add app details](/images/integrations/gitlab/add-app-details.webp#hero) 8. Set permissions by selecting the required **Scopes**. The table below explains each scope: | Permission | Explanation | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `api` | Grants full read/write access to the API, including all groups, projects, container registry, dependency proxy, and package registry. Required for API requests. | | `read_api` | Allows read-only access to all groups, projects, container registry, and package registry. | | `read_user` | Grants read-only access to user profiles via the /user API endpoint, including username, public email, and full name. Also provides access to /users endpoints. | | `read_repository` | Enables read-only access to repositories in private projects via Git-over-HTTP or the Repository Files API. | | `profile` | Grants read-only access to the user's profile data using OpenID Connect. | | `email` | Provides read-only access to the user's primary email address using OpenID Connect. | 9. Click **Save Application** to finalize the setup. \== GitLab Self-managed {#gitlab-self-managed} These instructions cover registering an OAuth app on your private GitLab server, using the correct callback URLs, and assigning the required scopes for Plane to access your repos and users. #### GitLab Self-managed 1. Log in to your GitLab instance. 2. Click on your profile icon in the top-right corner. 3. From the dropdown menu that appears, select **Edit profile**. 4. Look for and select the **Applications** option within this menu. 5. On the Applications page, click **Add new application** to begin configuring your OAuth application. Fill in the application details with the following configuration: * **Name** Enter a descriptive name for your application (e.g., `Plane Local Dev` or `Plane Integration`). * **Redirect URI** The redirect URI depends on your Plane deployment: **For Plane Cloud:** `https://silo.plane.so/api/oauth/gitlab-enterprise/auth/callback` **For Plane Self-Hosted:** `https:///silo/api/oauth/gitlab-enterprise/auth/callback` Replace `` with your actual Plane instance domain. * **Confidential** Keep the **Confidential** checkbox enabled. This ensures the application uses a client secret for secure authentication. * **Scopes** Select the following scopes to grant Plane the necessary permissions: * **api** - Grants complete read/write access to the API, including all groups and projects * **read\_api** - Grants read access to the API, including all groups and projects * **read\_user** - Grants read-only access to your profile information * **read\_repository** - Grants read-only access to repositories on private projects * **profile** - Grants read-only access to the user's profile data using OpenID Connect * **email** - Grants read-only access to the user's primary email address using OpenID Connect 6. Click **Save application** to create the OAuth application. ::: ## Configure Plane instance :::tabs key:gitlab-edition \== GitLab Cloud {#gitlab-cloud} 1. Copy the **Application ID** and **Secret** from the newly created application. ![Copy credentials](/images/integrations/gitlab/copy-credentials.webp#hero) 2. Add these environment variables with the values to your Plane instance's `.env` file. ```bash GITLAB_CLIENT_ID= GITLAB_CLIENT_SECRET= ``` 3. Save the file and restart the instance. 4. Once you've completed the instance configuration, [activate the GitLab integration in Plane](https://docs.plane.so/integrations/gitlab?edition=gitlab-cloud). \== GitLab Self-managed {#gitlab-self-managed} 1. Copy the **Application ID** and **Secret** from the newly created application. ![Copy credentials](/images/integrations/gitlab/copy-credentials.webp#hero) 2. Once you've created the application, [activate the GitLab Self-managed integration in Plane](https://docs.plane.so/integrations/gitlab?edition=gitlab-self-managed). ::: --- --- url: 'https://developers.plane.so/self-hosting/govern/integrations/sentry.html' description: >- Connect Sentry error monitoring to your self-hosted Plane instance. Automatically create work items from Sentry alerts and track error resolution. --- # Configure Sentry for Plane integration This guide shows you how to set up Sentry integration for your self-hosted Plane instance. Unlike Plane Cloud where Sentry comes pre-configured, self-hosted instances require you to create a custom integration in Sentry and configure your Plane deployment with the necessary credentials. ::: info **What you'll accomplish:** 1. Create a Sentry custom integration with proper permissions and webhooks 2. Configure your Plane instance with Sentry credentials 3. Enable error tracking and automatic issue creation from Sentry alerts ::: ## Before you begin You'll need: * Administrator access to your Sentry organization * Access to your Plane instance configuration files * Your Plane instance domain (e.g., `plane.yourcompany.com`) ## Create Sentry custom integration A custom integration (also called a public integration) connects your Sentry organization to Plane, enabling bidirectional communication for issue tracking and alert handling. 1. Log in to your Sentry organization. 2. Go to **Settings** → **Developer Settings** → **Custom Integrations**. 3. Click **Create New Integration**. 4. Select **Public Integration**. 5. Fill in these fields on the integration creation screen: | Field | Value | | ----------------------- | ----------------------------------------------------------- | | **Name** | `Plane` (or any name you prefer) | | **Author** | Your organization name | | **Webhook URL** | `https://[YOUR_DOMAIN]/silo/api/sentry/sentry-webhook/` | | **Redirect URL** | `https://[YOUR_DOMAIN]/silo/api/oauth/sentry/auth/callback` | | **Verify Installation** | Disabled (recommended) | | **Alert Rule Action** | Enabled | ::: tip Replace `[YOUR_DOMAIN]` with your actual Plane instance domain. For example, if your Plane instance is at `plane.company.com`, your Webhook URL would be `https://plane.company.com/silo/api/sentry/sentry-webhook/` ::: **Field explanations:** **Webhook URL**\ Sentry sends event notifications to this endpoint. Plane processes these webhooks to sync Sentry issues with Plane work items. **Redirect URL**\ After OAuth authorization, Sentry redirects users back to this URL to complete the connection. **Alert Rule Action**\ Enables automatic Plane work item creation when Sentry alert rules trigger. ### Configure integration schema The schema defines how Sentry and Plane interact—what fields appear when creating issues and how alert rules behave. Paste this schema into the **Schema** field: ```json { "elements": [ { "link": { "uri": "/silo/api/sentry/issues/link", "required_fields": [ { "uri": "/silo/api/sentry/issues", "name": "identifier", "type": "select", "label": "Issue", "skip_load_on_open": true } ] }, "type": "issue-link", "create": { "uri": "/silo/api/sentry/issues/create", "optional_fields": [ { "uri": "/silo/api/sentry/users", "name": "assignee_ids", "type": "select", "async": false, "label": "Assignees", "multiple": true, "depends_on": ["project_id"] }, { "uri": "/silo/api/sentry/priorities", "name": "priorities", "type": "select", "async": false, "label": "Priorities", "depends_on": ["project_id"] }, { "uri": "/silo/api/sentry/labels", "name": "labels", "type": "select", "async": false, "label": "Labels", "multiple": true, "depends_on": ["project_id"] }, { "uri": "/silo/api/sentry/states", "name": "state", "type": "select", "async": false, "label": "State", "depends_on": ["project_id"] }, { "uri": "/silo/api/sentry/modules", "name": "module", "type": "select", "async": false, "label": "Module", "depends_on": ["project_id"] }, { "uri": "/silo/api/sentry/cycles", "name": "cycle", "type": "select", "async": false, "label": "Cycle", "depends_on": ["project_id"] } ], "required_fields": [ { "name": "title", "type": "text", "label": "Title", "default": "issue.title" }, { "name": "description", "type": "textarea", "label": "Description", "default": "issue.description" }, { "uri": "/silo/api/sentry/projects", "name": "project_id", "type": "select", "async": false, "label": "Project" } ] } }, { "type": "alert-rule-action", "title": "Create Plane Work Item or Intake Issue", "settings": { "uri": "/silo/api/sentry/alert-rule", "type": "alert-rule-settings", "description": "Create a Plane Work Item or Intake Issue when an alert is triggered", "optional_fields": [ { "uri": "/silo/api/sentry/users", "name": "assignee_ids", "type": "select", "async": false, "label": "Assignees", "multiple": true, "depends_on": ["project_id"] }, { "uri": "/silo/api/sentry/states", "name": "state", "type": "select", "async": false, "label": "State", "depends_on": ["project_id"] }, { "uri": "/silo/api/sentry/labels", "name": "labels", "type": "select", "async": false, "label": "Labels", "multiple": true, "depends_on": ["project_id"] } ], "required_fields": [ { "name": "type", "type": "select", "label": "Type", "options": [ ["intake", "Intake"], ["work_item", "Work Item"] ] }, { "uri": "/silo/api/sentry/projects", "name": "project_id", "type": "select", "async": false, "label": "Project", "depends_on": ["type"] } ] } } ] } ``` **What this schema enables** **Work item linking**\ The first element defines how users create new Plane work items from Sentry or link existing ones. Required fields (title, description, project) ensure every work item has essential information. Optional fields (assignees, priority, labels, state, module, cycle) provide flexibility for detailed work item tracking. **Alert rule actions**\ The second element enables automatic work item creation when Sentry alerts fire. You can configure whether alerts create regular work items or intake work items for triage, and set default assignees, states, and labels. ### Set permissions Configure these permissions to allow Sentry to interact with Plane appropriately: | Permission | Access Level | Why This Matters | | ----------------- | ------------ | ------------------------------------------------------------ | | **Project** | Read | Access project details, tags, and debug files from Sentry | | **Team** | Read | Retrieve team member lists for assignee dropdowns | | **Release** | No Access | Not required for Plane integration | | **Distribution** | No Access | Not required for Plane integration | | **Issue & Event** | Read & Write | Create and link issues, sync status updates bidirectionally | | **Organization** | Read | Resolve organization IDs and retrieve repository information | | **Member** | Read | Access member details for assignee functionality | | **Alerts** | Read | Enable alert rule actions for automatic issue creation | ### Enable webhooks Webhooks keep Plane and Sentry synchronized. When issues change in Sentry, Plane receives notifications and updates accordingly. Enable the **issue** webhook with these events: | Event | Why It's Needed | | -------------- | -------------------------------------------------------------- | | **created** | Notify Plane when new Sentry issues are detected | | **resolved** | Update linked Plane work items when Sentry issues are resolved | | **assigned** | Sync assignee changes from Sentry to Plane | | **archived** | Reflect archived status in Plane | | **unresolved** | Update Plane when resolved issues reopen | ### Save and retrieve credentials After saving your integration, Sentry generates OAuth credentials: * **Client ID** - A public identifier for your integration * **Client Secret** - A private key used to authenticate API requests ::: warning **Important** The Client Secret is only displayed once immediately after creating the integration. Copy it now and store it securely. If you lose it, you'll need to regenerate the integration. ::: Copy both the Client ID and Client Secret. You'll need these in the next step. ## Configure your Plane instance Add Sentry credentials to your Plane instance so it can communicate with Sentry's API. ### Locate your configuration file **For Docker deployments:** * Edit `plane.env` in your Plane installation directory **For Kubernetes deployments:** * Edit your `custom-values.yaml` file or ConfigMap containing environment variables ### Add environment variables Add these variables to your Plane configuration: **For Docker (`plane.env`):** ```bash # Sentry Integration SENTRY_BASE_URL=https://sentry.io SENTRY_CLIENT_ID= SENTRY_CLIENT_SECRET= SENTRY_INTEGRATION_SLUG=plane ``` **For Kubernetes (`custom-values.yaml`):** ```yaml env: silo_envs: sentry_base_url: "https://sentry.io" sentry_client_id: "" sentry_client_secret: "" sentry_integration_slug: "plane" ``` Replace `` and `` with the credentials from Step 1. ### Environment variable reference | Variable | Required | Default | Description | | ------------------------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SENTRY_BASE_URL` | No | `https://sentry.io` | Base URL of your Sentry instance. For self-hosted Sentry, use your Sentry domain (e.g., `https://sentry.company.com`) | | `SENTRY_CLIENT_ID` | Yes | - | Client ID from your Sentry custom integration | | `SENTRY_CLIENT_SECRET` | Yes | - | Client Secret from your Sentry custom integration (only shown once during creation) | | `SENTRY_INTEGRATION_SLUG` | No | - | The slug identifier for your integration. Find this in your integration's URL: `https://org.sentry.io/settings/developer-settings/plane-local` (here `plane-local` is the slug) | ::: info **Using self-hosted Sentry?**\ If you're running your own Sentry instance, change `SENTRY_BASE_URL` to your Sentry domain. All other configuration remains the same. ::: ### Restart Plane Apply the configuration changes: **For Docker:** ```bash docker compose down docker compose up -d ``` **For Kubernetes:** ```bash helm upgrade plane-app plane-enterprise.tgz \ --namespace plane \ -f custom-values.yaml ``` ## Activate integration in your workspace Once you’ve completed the instance configuration, [activate the Sentry integration](https://docs.plane.so/integrations/sentry#set-up-sentry-integration) in Plane. For questions about Sentry integration, contact . --- --- url: 'https://developers.plane.so/self-hosting/govern/integrations/slack.html' description: >- Connect Slack to your self-hosted Plane instance. Get notifications for work item updates and interact with Plane from Slack channels. --- # Configure Slack for Plane integration This guide walks you through setting up a Slack App to enable Slack integration for your Plane workspace on a self-hosted instance. Since self-hosted environments don’t come pre-configured for Slack, you’ll need to set up the necessary authentication, permissions, and event subscriptions to ensure seamless communication between Plane and Slack. In this guide, you’ll: 1. [Create and configure a Slack App](/self-hosting/govern/integrations/slack#create-slack-app) 2. [Configure your Plane instance](/self-hosting/govern/integrations/slack#configure-plane-instance) ::: warning **Activate Slack integration** After creating and configuring the Slack app and configuring the instance as detailed on this page, you'll need to [set up the Slack integration](https://docs.plane.so/integrations/slack) within Plane. ::: ## Create Slack App To configure Slack integration, you'll need to create a Slack App within your organization. Follow these steps: 1. Go to [Your Apps](https://api.slack.com/apps) on Slack. 2. Click **Create an App**. ![Create Slack App](/images/integrations/slack/create-slack-app.webp#hero) 3. Choose **From a manifest**. ![Choose Manifest](/images/integrations/slack/choose-from-manifest.webp#hero) 4. Select the workspace where you want the app installed. 5. Remove the default manifest and paste the one below, making sure to update the placeholders with your actual values. ![Manifest](/images/integrations/slack/app-from-manifest.webp#hero) :::tabs key:manifest-file \== JSON {json} ```json { "display_information": { "name": "[YOUR_APP_NAME]", "description": "[YOUR_APP_DESCRIPTION]", "background_color": "#224dab" }, "features": { "bot_user": { "display_name": "[YOUR_APP_NAME]", "always_online": false }, "shortcuts": [ { "name": "Create new issue", "type": "message", "callback_id": "issue_shortcut", "description": "Create a new issue in plane" }, { "name": "Link Work Item", "type": "message", "callback_id": "link_work_item", "description": "Links thread with an existing work item" } ], "slash_commands": [ { "command": "/plane", "url": "https://[YOUR_DOMAIN]/silo/api/slack/command/", "description": "Create issue in Plane", "should_escape": false } ], "unfurl_domains": ["[YOUR_DOMAIN]"] }, "oauth_config": { "redirect_urls": [ "https://[YOUR_DOMAIN]/silo/api/slack/team/auth/callback/", "https://[YOUR_DOMAIN]/silo/api/slack/user/auth/callback/" ], "scopes": { "user": ["chat:write", "identify", "im:read", "im:write", "links:write", "links:read"], "bot": [ "channels:join", "channels:read", "users:read", "users:read.email", "chat:write", "chat:write.customize", "channels:history", "groups:history", "mpim:history", "im:history", "links:read", "links:write", "groups:read", "im:read", "mpim:read", "reactions:read", "reactions:write", "files:read", "files:write", "im:write", "commands" ] } }, "settings": { "event_subscriptions": { "request_url": "https://[YOUR_DOMAIN]/silo/api/slack/events", "bot_events": ["link_shared", "message.channels", "message.im"] }, "interactivity": { "is_enabled": true, "request_url": "https://[YOUR_DOMAIN]/silo/api/slack/action/", "message_menu_options_url": "https://[YOUR_DOMAIN]/silo/api/slack/options/" }, "org_deploy_enabled": false, "socket_mode_enabled": false, "token_rotation_enabled": true } } ``` \== YAML {yaml} ```yaml display_information: name: [YOUR_APP_NAME] description: [YOUR_APP_DESCRIPTION] background_color: "#224dab" features: bot_user: display_name: [YOUR_APP_NAME] always_online: false shortcuts: - name: Create new issue type: message callback_id: issue_shortcut description: Create a new issue in plane - name: Link Work Item type: message callback_id: link_work_item description: Links thread with an existing work item slash_commands: - command: /plane url: https://[YOUR_DOMAIN]/silo/api/slack/command/ description: Create issue in Plane should_escape: false unfurl_domains: - [YOUR_DOMAIN] oauth_config: redirect_urls: - https://[YOUR_DOMAIN]/silo/api/slack/team/auth/callback/ - https://[YOUR_DOMAIN]/silo/api/slack/user/auth/callback/ scopes: user: - chat:write - identify - im:read - im:write - links:write - links:read bot: - channels:join - channels:read - users:read - users:read.email - chat:write - chat:write.customize - channels:history - groups:history - mpim:history - im:history - links:read - links:write - groups:read - im:read - mpim:read - reactions:read - reactions:write - files:read - files:write - im:write - commands settings: event_subscriptions: request_url: https://[YOUR_DOMAIN]/silo/api/slack/events bot_events: - link_shared - message.channels - message.im interactivity: is_enabled: true request_url: https://[YOUR_DOMAIN]/silo/api/slack/action/ message_menu_options_url: https://[YOUR_DOMAIN]/silo/api/slack/options/ org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: true ``` ::: 6. Review the permissions and click **Create**. ![Review summary](/images/integrations/slack/review-summary.webp#hero) ### Manifest reference The manifest file defines the configuration for integrating Plane with Slack. It requests access to several features, enabling Plane to interact with Slack efficiently. #### Features | Feature | Explanation | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `bot_user` | Required to send thread messages while syncing issues or sending Plane notifications to Slack. | | `slack_commands` | A Slack command (`/plane`) allows users to create issues directly from Slack using a slash command. | | `shortcuts` | After activation, users can create issues from messages inside Slack. | | `unfurl_domain` | Specifies the domain where Plane is hosted. When an issue, cycle, or module link is pasted in Slack, it generates a preview of the entity. | #### Variables | Variable | Explanation | | ---------------------- | ----------------------------------------------------------------------------------------------------------- | | `YOUR_DOMAIN` | The domain where Plane is hosted. This is required for sending webhook events and authentication callbacks. | | `YOUR_APP_NAME` | The name you want to give your Slack app. "Plane" is a good default option. | | `YOUR_APP_DESCRIPTION` | A short description of your Slack app’s purpose. | #### Event subscription For thread sync and link unfurling to work, event subscriptions must be enabled. These events send relevant activity to Plane. | Bot event | Explanation | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | `link_shared` | When a link is shared in Slack and its hostname matches `unfurl_domain`, Plane receives the event and generates a preview of the entity. | | `message_channels` | When a message is posted in a channel, an event is triggered in Plane to support thread sync. | | `message_im` | When a direct message (DM) is posted, an event is triggered in Plane to support thread sync. | #### User permissions | Permission | Explanation | | ------------- | -------------------------------------------------------------------------------- | | `chat:write` | Allows the bot to send messages in channels and conversations it is a member of. | | `identify` | Allows the bot to verify its own identity and retrieve basic information. | | `im:read` | Enables the bot to view direct messages (DMs) where it has been added. | | `im:write` | Allows the bot to send direct messages (DMs) to users. | | `links:write` | Permits the bot to add, edit, and remove link unfurls. | | `links:read` | Allows the bot to view link unfurls and associated metadata. | #### Bot permissions | Permission | Explanation | | ---------------------- | -------------------------------------------------------------------------- | | `channels:join` | Allows the bot to join public channels. | | `channels:read` | Permits viewing public channel information and members. | | `users:read` | Allows viewing user information and presence status. | | `users:read.email` | Enables access to users' email addresses. | | `chat:write` | Allows sending messages in channels and conversations. | | `chat:write.customize` | Enables customization of the bot's name and profile when sending messages. | | `channels:history` | Allows viewing message history in public channels. | | `groups:history` | Permits viewing message history in private channels. | | `mpim:history` | Enables access to message history in multi-person direct messages. | | `im:history` | Allows viewing message history in direct messages. | | `links:read` | Permits viewing link unfurls and associated metadata. | | `links:write` | Allows adding, editing, and removing link unfurls. | | `groups:read` | Enables viewing private channel information and members. | | `im:read` | Allows viewing direct messages where the bot is added. | | `mpim:read` | Permits viewing multi-person direct messages. | | `reactions:read` | Enables viewing emoji reactions on messages. | | `reactions:write` | Allows adding and removing emoji reactions. | | `files:read` | Permits viewing and downloading files. | | `files:write` | Enables uploading, editing, and deleting files. | | `im:write` | Allows sending direct messages to users. | | `commands` | Enables the bot to add and respond to slash commands. | ## Configure Plane instance After creating your Slack app, follow these steps: 1. Go to the **Event Subscriptions** tab. 2. Click **Retry** to verify your event subscription URL. ![Event subscriptions](/images/integrations/slack/event-subscriptions.webp#hero) 3. Navigate to the **Basic Information** tab on Slack to find your `client_id` and `client_secret`. 4. Add these environment variables with the values to your Plane instance's `.env` file. ```bash SLACK_CLIENT_ID= SLACK_CLIENT_SECRET= ``` 5. Save the file and restart the instance. 6. Once you've completed the instance configuration, [activate the Slack integration in Plane](https://docs.plane.so/integrations/slack). --- --- url: >- https://developers.plane.so/self-hosting/govern/configure-dns-email-service.html description: >- Configure DNS MX records for Plane intake email. Route incoming emails to create work items automatically in your self-hosted instance. --- # Configure DNS for Intake Email This guide explains how to configure DNS settings to enable the [Intake Email](https://docs.plane.so/intake/intake-email) feature for your self-hosted Plane instance. These configurations enable your server to accept messages sent to your project's dedicated Intake address, which are then converted into work items in your project's Intake section. ## Prerequisites Ensure that the Plane server allows inbound traffic on the following email-related ports: `25`, `465`, and `587`. If any of these ports are currently in use, you can free them by running: ```bash fuser -k 25/tcp 465/tcp 587/tcp ``` ## Generate SSL/TLS Certificate for Email Domain ::: warning Mandatory for Docker Compose deployments only. ::: Before configuring DNS records for Intake Email, secure your email domain with an SSL/TLS certificate. This ensures encrypted communication between mail servers and improves email trust and deliverability. 1. **Install Certbot**\ Update your system and install Certbot. ```bash sudo apt update && sudo apt install certbot ``` For NGINX: ```bash sudo apt install python3-certbot-nginx ``` For Apache: ```bash sudo apt install python3-certbot-apache ``` 2. **Generate SSL Certificate**\ Choose the method that matches your web server setup: For NGINX: ```bash sudo certbot --nginx -d ``` For Apache: ```bash sudo certbot --apache -d ``` For standalone (no web server): ```bash sudo certbot certonly --standalone -d ``` 3. **Copy Certificate Files**\ Copy the generated certificate files to Plane's expected directory: ```bash sudo cp /etc/letsencrypt/live//fullchain.pem /opt/plane/data/email/tls/cert.pem sudo cp /etc/letsencrypt/live//privkey.pem /opt/plane/data/email/tls/key.pem ``` 4. **Configure Environment Variables**\ Add the following settings to your plane.env file: ```bash # If using SMTP_DOMAIN as FQDN (e.g., intake.example.com), # generate a valid SSL certificate and set these paths accordingly. SMTP_DOMAIN=intake.example.com TLS_CERT_PATH=tls/cert.pem TLS_PRIV_KEY_PATH=tls/key.pem INTAKE_EMAIL_DOMAIN=intake.example.com ``` ::: warning Important: `SMTP_DOMAIN` and `INTAKE_EMAIL_DOMAIN` must be identical. ::: ## Configure DNS records 1. **Create an A Record**\ This record points to the server running your email service. ```bash Type: A Host: # Example: plane.example.com Value: # Your server's public IP address TTL: Auto | 3600 ``` ::: tip You can alternatively use a CNAME record if you're using a cloud load balancer. ::: 2. **Add an MX Record**\ This record directs email traffic to your mail server. ```bash Type: MX Host: # Example: intake.example.com Value: # Same as your A record host Priority: 10 TTL: Auto | 3600 ``` 3. **Configure an SPF Record**\ This record helps prevent email spoofing. ```bash Type: TXT Host: # Example: intake.example.com Value: "v=spf1 ip4: -all" TTL: Auto | 3600 ``` 4. **Set Up a DMARC record**\ This record specifies how receiving mail servers should handle authentication failures. ```bash Type: TXT Host: _dmarc. # Example: _dmarc.intake.example.com Value: "v=DMARC1; p=reject; rua=mailto:" TTL: Auto | 3600 ``` ## Verify your configuration After setting up your DNS records, verify that they're correctly configured: ```bash # Verify A record dig A # Verify MX record dig MX # Verify SPF record dig TXT # Verify DMARC record dig TXT _dmarc. ``` You can also use [MXToolbox](https://mxtoolbox.com) to check for any issues with your DNS configuration. ## Test your mail server Once your DNS records have propagated, test your SMTP connections: ```bash # Test SMTP connection on standard ports telnet 25 telnet 465 telnet 587 ``` ## Troubleshooting * MX Record issues * Ensure there's a proper dot at the end of the domain. * Check that the priority number is correct (lower = higher priority). * Allow 24-48 hours for DNS changes to fully propagate. * A Record issues * Verify that the IP address is correct. * Ensure your mail subdomain matches the MX record. ## See also [Intake Email](https://docs.plane.so/intake/intake-email) --- --- url: 'https://developers.plane.so/self-hosting/govern/advanced-search.html' description: >- Enable full-text search in Plane with OpenSearch. Configure advanced search indexing for work items, projects, and pages in your self-hosted instance. --- # Configure OpenSearch for advanced search Plane uses OpenSearch to provide advanced search capabilities across your workspace. This guide walks you through setting up OpenSearch integration on your self-hosted instance. ## Before you begin You'll need: * An OpenSearch instance running version 2.19 or later (self-hosted or managed service like AWS OpenSearch). ## What you get with advanced search Once configured, advanced search provides: * **Full-text search** across work items, projects, cycles, modules, pages, and more * **Fuzzy matching** that tolerates typos and variations in spelling * **Autocomplete** with instant suggestions as you type * **Multi-entity search** that searches across all content types in a single query Users can access advanced search using the global search shortcut (Cmd/Ctrl + K) or search within specific projects and sections. ## Configure OpenSearch Set environment variables in your Plane configuration. See [Environment variables reference](/self-hosting/govern/environment-variables#opensearch) for details. ### For Docker deployments 1. **Add configuration to your environment file** Edit `/opt/plane/plane.env`. ```bash # OpenSearch Settings OPENSEARCH_ENABLED=1 OPENSEARCH_URL=https://your-opensearch-instance:9200/ OPENSEARCH_USERNAME=admin OPENSEARCH_PASSWORD=your-secure-password OPENSEARCH_INDEX_PREFIX=plane ``` 2. **Restart Plane services** ```bash prime-cli restart ``` or if managing containers directly: ```bash docker compose down docker compose up -d ``` 3. **Create search indices** Access the API container and create the necessary indices: ```bash # Access the API container docker exec -it plane-api-1 sh # Create all search indices (run once) python manage.py manage_search_index index rebuild --force ``` 4. **Index your existing data** Index all existing content into OpenSearch: ```bash # For small datasets python manage.py manage_search_index document index --force # For large datasets (recommended) python manage.py manage_search_index --background document index --force ``` The background option processes indexing through Celery workers, which is better for instances with large amounts of data. ### For Kubernetes deployments The Plane Helm chart provides auto-setup for OpenSearch. If you're using your own OpenSearch instance, configure it through Helm values. 1. **Configure Helm values** Get the current values file: ```bash helm show values plane/plane-enterprise > values.yaml ``` Edit `values.yaml` to add OpenSearch configuration: ```yaml env: # OpenSearch configuration opensearch_remote_url: "https://your-opensearch-instance:9200/" opensearch_remote_username: "admin" opensearch_remote_password: "your-secure-password" opensearch_index_prefix: "plane" ``` Refer to the [Plane Helm chart documentation](https://artifacthub.io/packages/helm/makeplane/plane-enterprise?modal=values\&path=env.opensearch_remote_url) for complete values structure. 2. **Upgrade your deployment** ```bash helm upgrade --install plane-app plane/plane-enterprise \ --create-namespace \ --namespace plane \ -f values.yaml \ --timeout 10m \ --wait \ --wait-for-jobs ``` 3. **Create search indices** Run these commands in the API pod. ```bash # Get the API pod name API_POD=$(kubectl get pods -n plane --no-headers | grep api | head -1 | awk '{print $1}') # Create all search indices (run once) kubectl exec -n plane $API_POD -- python manage.py manage_search_index index rebuild --force ``` 4. **Index your existing data** Run these commands in the API pod. ```bash # For small datasets kubectl exec -n plane $API_POD -- python manage.py manage_search_index document index --force # For large datasets (recommended) kubectl exec -n plane $API_POD -- python manage.py manage_search_index --background document index --force ``` ## Verify the setup ### Check OpenSearch connection Test that Plane can connect to your OpenSearch instance: ```bash # Access your API container or pod docker exec -it plane-api-1 sh # For Docker # OR kubectl exec -n plane $API_POD -- sh # For Kubernetes # Start Python shell python manage.py shell ``` Then run: ```python from django.conf import settings from opensearchpy import OpenSearch client = OpenSearch( hosts=[settings.OPENSEARCH_DSL['default']['hosts']], http_auth=settings.OPENSEARCH_DSL['default']['http_auth'], use_ssl=True, verify_certs=False ) # Check cluster health print(client.cluster.health()) # List indices - you should see your Plane indices print(client.cat.indices(format='json')) ``` ### Verify indices were created List all created indices: ```bash python manage.py manage_search_index list ``` You should see indices for work items, projects, cycles, modules, pages, and other searchable entities. ### Test search functionality 1. Sign in to your Plane instance. 2. Press **Cmd/Ctrl + K** to open global search. 3. Type a search query and verify results appear. 4. Test search within projects, work items, and pages. ## Maintenance ### Resync data If search results become stale or inconsistent, resync your data: ```bash python manage.py manage_search_index document index --force ``` This reindexes all content without recreating the index structure. ### Complete rebuild For a complete reset (recreates indices and reindexes all data): ```bash # Recreate all indices python manage.py manage_search_index index rebuild --force # Reindex all documents python manage.py manage_search_index document index --force ``` Use this if index structure needs updating or if you're experiencing persistent issues. ### Monitor logs Check API logs OpenSearch-related errors: **Docker:** ```bash docker compose logs api | grep -i opensearch ``` **Kubernetes:** ```bash kubectl logs -n plane -l app.kubernetes.io/component=api | grep -i opensearch ``` ## Understanding how it works Advanced search in Plane maintains search indices separately from your main database. This separation is why search can be fast even with thousands of work items - OpenSearch is purpose-built for search operations, while your database handles transactional operations. ### Why Plane uses OpenSearch Traditional database searches struggle with fuzzy matching and typos. If you search for "authentcation" (with a typo), a database won't find "authentication". OpenSearch handles this naturally because it analyzes text differently - it breaks words into tokens, normalizes variations, and understands linguistic patterns. This is why autocomplete feels instant. OpenSearch pre-processes text to match partial words, while your database would need to scan entire tables to achieve similar results. ### The synchronization challenge The trade-off with separate search indices is keeping them synchronized with your database. When someone updates a work item, that change must reach OpenSearch for search results to remain accurate. Plane solves this through an event-driven architecture. Every time data changes in your database, Django emits a signal. These signals trigger updates to OpenSearch. ### Batching for efficiency Direct, immediate updates would overwhelm both your database and OpenSearch. Imagine a user creating 50 work items in quick succession, that would mean 50 separate API calls to OpenSearch, each with network overhead. Instead, Plane batches updates through Redis. When a signal fires, the update goes into a Redis queue. A Celery worker processes this queue every 5 seconds, combining multiple updates into efficient batch operations. This is why you might notice a brief delay (up to 5 seconds) before new content appears in search results. The batching pattern also provides resilience. If OpenSearch is temporarily unavailable, updates accumulate in Redis and process once connectivity returns. This requires Redis 6.2+ which supports the LPOP count operation needed for efficient batch retrieval. ### The complete flow ![OpenSeach flow](/images/open-search/opensearch-flow.webp#hero) When you search, queries bypass this synchronization process entirely. The Plane API sends your search query directly to OpenSearch, which returns results almost instantly. Your database isn't involved in search queries at all — this is the key to search performance. ### Index organization Plane creates nine separate indices in OpenSearch, one for each searchable entity type. This separation might seem redundant - why not put everything in one index? The answer lies in how different entities need different search behaviors. Work items use fuzzy matching and field prioritization (title matches rank higher than description matches). Projects emphasize metadata filtering—status, member counts, and timelines. Pages analyze long-form content structure. Each index is optimized for its content type: | Index | Content | Search Features | | ------------------------- | ----------- | ------------------------------------------------------------------------ | | `{prefix}_issues` | Work items | Full-text search, field weighting (title > description), state filtering | | `{prefix}_issue_comments` | Comments | Comment search within work items, parent-child relationships | | `{prefix}_projects` | Projects | Project discovery, metadata filtering (dates, counts, status) | | `{prefix}_cycles` | Cycles | Cycle search, time-based filtering and aggregations | | `{prefix}_modules` | Modules | Module/sprint search, planning aggregations | | `{prefix}_pages` | Pages | Page content search, rich text analysis for long-form content | | `{prefix}_workspaces` | Workspaces | Workspace search and discovery | | `{prefix}_issue_views` | Saved views | Saved view search and filtering | | `{prefix}_teamspaces` | Teamspaces | Teamspace discovery | The `{prefix}` is whatever you configured in `OPENSEARCH_INDEX_PREFIX`, or empty if you didn't set a prefix. This prefix exists because you might run multiple Plane instances pointing to the same OpenSearch cluster. The prefix prevents different instances from accidentally sharing or conflicting with each other's indices. --- --- url: >- https://developers.plane.so/self-hosting/govern/plane-ai/configure-plane-ai.html description: >- Enable Plane AI on your self-hosted instance. Set up a dedicated database, configure OpenSearch, add an LLM API key, and connect PI to your Plane deployment. --- # Configure Plane AI Plane AI brings AI-powered features to your workspace, including natural language chat, duplicate detection, and search across work items, pages, and projects. This guide walks you through configuring Plane AI on your self-hosted instance. For an overview of what Plane AI can do, see [Plane AI](https://docs.plane.so/ai/pi-chat). ## Prerequisites Plane AI requires four things to work: 1. **An LLM API key** (OpenAI or Anthropic) - powers AI responses. 2. **OpenSearch** - An OpenSearch instance running version 2.19 or later (self-hosted or AWS OpenSearch) configured for [advanced search](/self-hosting/govern/advanced-search). Search over your workspace data (work items, pages, cycles) runs through OpenSearch indices. 3. **A dedicated database** - Plane AI must not share the main Plane application database. 4. **Read access to the main Plane database** - PI reads workspace data directly from the main Plane DB. ## Supported LLM providers ### OpenAI * GPT-5.4 * GPT-5.2 ### Anthropic * Claude Sonnet 4.5 * Claude Sonnet 4.6 You can provide API keys for both OpenAI and Anthropic, making all models available to users. If you provide only one key, users will only have access to that provider's models. :::tip Custom or self-hosted models To use Ollama, Groq, LiteLLM, AWS Bedrock, or any OpenAI-compatible endpoint, see [Custom LLM models](#custom-llm-models). ::: ## Set up databases Plane AI needs two database connections. ```bash PLANE_PI_DATABASE_URL=postgresql://user:password@host:5432/plane-pi FOLLOWER_POSTGRES_URI=postgresql://user:password@host:5432/plane ``` * **`PLANE_PI_DATABASE_URL`** - PI's own dedicated database. Must not be shared with the main Plane application database. * **`FOLLOWER_POSTGRES_URI`** - Read connection to the main Plane database. PI reads workspace data (issues, pages, projects) directly from here. Can be a read replica. Both are checked at startup - PI will not start if either is unreachable. ## Configure OpenSearch Add to `/opt/plane/plane.env`: ```bash OPENSEARCH_URL=https://your-opensearch-instance:9200/ OPENSEARCH_USERNAME=admin OPENSEARCH_PASSWORD=your-secure-password ``` If you haven't set up OpenSearch yet, see [OpenSearch for advanced search](/self-hosting/govern/advanced-search) first. ## Configure an LLM provider Add at least one to `/opt/plane/plane.env`: ```bash OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx CLAUDE_API_KEY=xxxxxxxxxxxxxxxx ``` ### Custom LLM models :::warning The custom model should have at least 1 trillion parameters for all Plane AI features to work reliably. Larger, more capable models yield better results. ::: Plane AI supports one custom LLM alongside OpenAI and Anthropic. * OpenAI-compatible - any model exposed via an OpenAI Chat Completions API, including models served by Ollama, Groq, Cerebras, and similar runtimes. * AWS Bedrock - models accessed directly through Amazon Bedrock using your AWS credentials. One custom model can be configured alongside your public provider keys. :::tip No OpenAI-compatible API? Proxy any model through - it exposes any LLM behind the OpenAI API. Then use the OpenAI-compatible setup below. If you need to use an LLM that isn't from OpenAI or Anthropic - for example, an open-source model or a regional provider for compliance reasons - you can proxy it through [LiteLLM](https://docs.litellm.ai).Then use the OpenAI-compatible setup below. ::: #### OpenAI-compatible Add to `/opt/plane/plane.env`: ```bash CUSTOM_LLM_ENABLED=true CUSTOM_LLM_PROVIDER=openai CUSTOM_LLM_MODEL_KEY=your-model-id # model ID as the endpoint expects it CUSTOM_LLM_BASE_URL=https://your-endpoint/v1 CUSTOM_LLM_API_KEY=your-api-key # use any non-empty string if no key is required CUSTOM_LLM_NAME=Your Model Name # display name shown to users CUSTOM_LLM_MAX_TOKENS=64000 # optional; max output tokens per response ``` **Examples:** ```bash # Groq CUSTOM_LLM_MODEL_KEY=llama-3.3-70b-versatile CUSTOM_LLM_BASE_URL=https://api.groq.com/openai/v1 CUSTOM_LLM_API_KEY=gsk_xxxxxxxxxxxx # Ollama (local) CUSTOM_LLM_MODEL_KEY=llama3 CUSTOM_LLM_BASE_URL=http://localhost:11434/v1 CUSTOM_LLM_API_KEY=ollama # LiteLLM proxy CUSTOM_LLM_MODEL_KEY=your-litellm-model CUSTOM_LLM_BASE_URL=http://litellm:4000/v1 CUSTOM_LLM_API_KEY=your-litellm-master-key ``` #### AWS Bedrock ##### Standard credentials Use for IAM user access with an explicit access key and secret. ```bash CUSTOM_LLM_ENABLED=true CUSTOM_LLM_PROVIDER=bedrock CUSTOM_LLM_MODEL_KEY=anthropic.claude-3-5-sonnet-20241022-v2:0 # Bedrock model ID CUSTOM_LLM_API_KEY=your-aws-secret-access-key CUSTOM_LLM_AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your-aws-access-key-id # standard AWS env var, picked up by boto3 CUSTOM_LLM_NAME=Claude via Bedrock CUSTOM_LLM_MAX_TOKENS=64000 # optional; max output tokens per response ``` :::warning IAM permission required The IAM user must have `bedrock:InvokeModel` permission on the target model. ::: #### Inference profile (IRSA / EKS Pod Identity) Use for Kubernetes deployments where the pod has an ambient IAM role. No static credentials needed. ```bash CUSTOM_LLM_ENABLED=true CUSTOM_LLM_PROVIDER=bedrock CUSTOM_LLM_MODEL_KEY=claude-sonnet-4-6 CUSTOM_LLM_AWS_REGION=us-east-1 BEDROCK_INFERENCE_PROFILE_ARN=arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/xxxx # or use BEDROCK_INFERENCE_PROFILE_ID=global.anthropic.claude-sonnet-4-6 CUSTOM_LLM_NAME=Claude via Inference Profile CUSTOM_LLM_MAX_TOKENS=64000 # optional; max output tokens per response ``` Plane AI activates inference profile mode automatically when a profile ARN or ID is set and ambient AWS credentials are present (`AWS_ROLE_ARN`, `AWS_WEB_IDENTITY_TOKEN_FILE`, `AWS_CONTAINER_CREDENTIALS_FULL_URI`, or `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE`). ## Connect Plane AI to your Plane deployment Plane AI runs as a separate service and must be wired to your Plane deployment. Two things matter here. **In the Plane API env** (`/opt/plane/plane.env`), tell Plane where PI is reachable: ```bash PI_BASE_URL=https://plane.example.com ``` Plane builds the PI URL as `PI_BASE_URL + PI_BASE_PATH` - with the default `PI_BASE_PATH=/pi` this becomes `https://plane.example.com/pi`. **In the PI env** (`/opt/plane/plane.env`), set the OAuth redirect URI to match: ```bash PLANE_OAUTH_REDIRECT_URI=https://plane.example.com/pi/api/v1/oauth/callback/ ``` ## Enable Plane AI services :::tip Other deployment methods For Coolify, Portainer, Docker Swarm, and Podman Quadlets, use the same environment variables as Docker Compose - only the replica variables differ. ::: :::tabs key:deployment-method \== Docker Compose {#docker-compose} In `/opt/plane/plane.env`, set replica counts to `1`: ```bash PI_API_REPLICAS=1 PI_BEAT_REPLICAS=1 PI_WORKER_REPLICAS=1 PI_MIGRATOR_REPLICAS=1 ``` \== Kubernetes {#kubernetes} In `values.yaml`, enable the Plane AI service: ```yaml services: pi: enabled: true ``` This activates the Plane AI API, worker, beat-worker, and migrator workloads. Configure replicas and resource limits through the [Plane AI values block](/self-hosting/methods/kubernetes#plane-ai-deployment). ::: ## Optional configuration ### Voice input Enables speech-to-text in Plane AI chat. Get a key at [console.groq.com](https://console.groq.com). ```bash GROQ_API_KEY=your-groq-api-key ``` ### File uploads Enables file attachments in Plane AI. ```bash AWS_S3_BUCKET_NAME=your-bucket-name AWS_S3_REGION=us-east-1 AWS_ACCESS_KEY_ID=your-access-key AWS_SECRET_ACCESS_KEY=your-secret-key ``` For MinIO or S3-compatible storage, also add: ```bash AWS_S3_ENDPOINT_URL=http://your-minio-host:9000 USE_MINIO=1 ``` ## Restart Plane :::tabs key:deployment-method \== Docker Compose {#docker-compose} ```bash prime-cli restart ``` Or directly: ```bash docker compose down docker compose up -d ``` \== Kubernetes {#kubernetes} ```bash helm upgrade --install plane-app plane/plane-enterprise \ --namespace plane \ -f values.yaml \ --wait ``` ::: --- --- url: >- https://developers.plane.so/self-hosting/govern/plane-ai/configure-embedding-model.html description: >- Configure an embedding model for Plane AI semantic search, duplicate detection, and vector retrieval over work items and pages. --- # Configure embedding model for semantic search Configuring the embedding model is optional. Without it, Plane AI uses BM25 keyword search. With it, you get vector similarity search, duplicate issue detection, and semantic retrieval over work items and pages. This builds on the OpenSearch connection configured in [Configure Plane AI](/self-hosting/govern/plane-ai/configure-plane-ai). Make sure `OPENSEARCH_URL`, `OPENSEARCH_USERNAME`, and `OPENSEARCH_PASSWORD` are already set before continuing. ## Supported embedding models | Provider | Model | Dimension | | --------------- | -------------------------------------- | --------- | | **Cohere** | `cohere/embed-v4.0` | 1536 | | | `cohere/embed-english-v3.0` | 1024 | | | `cohere/embed-english-v2.0` | 4096 | | **OpenAI** | `openai/text-embedding-ada-002` | 1536 | | | `openai/text-embedding-3-small` | 1536 | | | `openai/text-embedding-3-large` | 3072 | | **AWS Bedrock** | `bedrock/amazon.titan-embed-text-v1` | 1536 | | | `bedrock/amazon.titan-embed-text-v2` | 1024 | | | `bedrock/cohere.embed-english-v3` | 1024 | | | `bedrock/cohere.embed-multilingual-v3` | 1024 | ## Configure the embedding model Two options depending on your OpenSearch setup. ### Option A: Use an existing OpenSearch model ID Use this if you've already deployed an embedding model in OpenSearch - either via the [AWS OpenSearch embedding guide](/self-hosting/govern/plane-ai/aws-opensearch-embedding) or manually on self-hosted OpenSearch. ```bash EMBEDDING_MODEL=cohere/embed-v4.0 # must match the deployed model OPENSEARCH_ML_MODEL_ID= # model ID returned by OpenSearch on deploy # OPENSEARCH_EMBEDDING_DIMENSION=1024 # only if not using default 1536 ``` :::tip `OPENSEARCH_EMBEDDING_DIMENSION` must match the model's actual output dimension (see table above). It defaults to `1536` - only set it explicitly if your model uses a different value. A mismatch between the configured dimension and the model's real output breaks indexing. ::: ### Option B: Automatic deployment (self-hosted OpenSearch only) Plane AI can create and deploy the embedding model automatically when the migrator starts. Provide the model name and provider credentials - no manual OpenSearch setup needed. **Cohere:** ```bash EMBEDDING_MODEL=cohere/embed-v4.0 COHERE_API_KEY=your-cohere-api-key ``` **OpenAI:** ```bash EMBEDDING_MODEL=openai/text-embedding-3-small OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx ``` **AWS Bedrock:** ```bash EMBEDDING_MODEL=bedrock/amazon.titan-embed-text-v1 BR_AWS_ACCESS_KEY_ID=your-access-key BR_AWS_SECRET_ACCESS_KEY=your-secret-key BR_AWS_REGION=us-east-1 ``` :::warning IAM permission required for Bedrock The IAM user for `BR_AWS_ACCESS_KEY_ID` and `BR_AWS_SECRET_ACCESS_KEY` needs `bedrock:InvokeModel` permission on the Titan foundation model. Without it, embedding requests fail with a 403 error. Attach this policy to the IAM user: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "bedrock:InvokeModel", "Resource": "arn:aws:bedrock:::foundation-model/amazon.titan-embed-text-v1" } ] } ``` Replace `` with your `BR_AWS_REGION` value. ::: :::info AWS managed OpenSearch Auto-deploy requires direct ML access to OpenSearch. AWS managed OpenSearch restricts this, so deploy the model manually using the [AWS OpenSearch embedding guide](/self-hosting/govern/plane-ai/aws-opensearch-embedding), then use the model ID option above. ::: ## Restart Plane After updating `/opt/plane/plane.env`, restart Plane. The Plane AI migrator runs automatically on start and handles embedding model deployment, index creation, and pipeline setup. ## Vectorize existing data New content is indexed automatically. For existing work items and pages, run this in the API container: Generate embeddings for your existing content by running this command in the API container. **Docker:** ```bash docker exec -it plane-api-1 sh python manage.py manage_search_index --background --vectorize document index --force ``` **Kubernetes:** ```bash API_POD=$(kubectl get pods -n plane --no-headers | grep api | head -1 | awk '{print $1}') kubectl exec -n plane $API_POD -- python manage.py manage_search_index --background --vectorize document index --force ``` The `--background` flag processes vectorization through Celery workers. This is recommended for instances with large amounts of existing content. ## Changing the embedding model If you update the model or manually override the dimension size by setting `OPENSEARCH_EMBEDDING_DIMENSION`, you must recreate your search indices so they adopt the new dimension size, then reindex and revectorize your workspace. Ensure that the model associated with your `OPENSEARCH_ML_MODEL_ID` and your `EMBEDDING_MODEL` configuration share this same dimension size. ### Model only (same dimension) Update `EMBEDDING_MODEL` and provider credentials in `/opt/plane/plane.env`, restart Plane, then revectorize: ```bash docker exec -it plane-api-1 sh python manage.py manage_search_index --background --vectorize document index --force ``` ### Dimension change Update `EMBEDDING_MODEL`, `OPENSEARCH_EMBEDDING_DIMENSION`, and provider credentials in `/opt/plane/plane.env`, restart Plane, then rebuild and revectorize: ```bash # Rebuild indices with the new dimension python manage.py manage_search_index index rebuild --force # Reindex and revectorize all existing documents python manage.py manage_search_index --background --vectorize document index --force ``` `OPENSEARCH_EMBEDDING_DIMENSION` must match the actual output dimension of the model in `EMBEDDING_MODEL`. A mismatch breaks indexing. --- --- url: >- https://developers.plane.so/self-hosting/govern/plane-ai/aws-opensearch-embedding.html description: >- Step-by-step guide to deploying a Cohere embedding model on AWS OpenSearch for use with Plane AI semantic search. --- # Deploy an embedding model on AWS OpenSearch This guide walks you through deploying a Cohere embedding model on AWS OpenSearch (managed) for Plane AI semantic search. For other connector blueprints and embedding model configurations, see the [OpenSearch ML Commons remote inference blueprints](https://github.com/opensearch-project/ml-commons/tree/2.x/docs/remote_inference_blueprints). ## Before you begin Make sure you have: * An AWS OpenSearch domain with **fine-grained access control** enabled. * Admin access to OpenSearch Dashboards. * AWS CLI configured locally. * An IAM user with permissions to create roles, policies, and access Secrets Manager. * A Cohere API key. ## Create an IAM policy 1. Go to **IAM → Policies → Create Policy**. 2. Select **JSON** and paste the following: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "PassRoleAccess", "Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam:::role/plane-opensearch-access-role" }, { "Sid": "SecretManagerAccess", "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:ListSecrets" ], "Resource": "*" } ] } ``` 3. Click **Next**, name the policy `plane-opensearch-access-policy`, and click **Create Policy**. ## Create an IAM role Create an IAM role that OpenSearch can assume to access Secrets Manager. 1. Go to **IAM → Roles → Create Role**. 2. Name the role `plane-opensearch-access-role`. 3. Set this **Trust Relationship**: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" }, { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam:::user/" }, "Action": "sts:AssumeRole" } ] } ``` 4. Attach the `plane-opensearch-access-policy` you created in Step 1. 5. Click **Create Role** and note the role ARN. ## Grant ML permissions to the IAM role 1. Open **OpenSearch Dashboards**. 2. Go to **Security → Roles → `ml_full_access`**. 3. Open the **Mapped users** tab and click **Map users**. 4. Under **Backend roles**, add the role ARN: ``` arn:aws:iam:::role/plane-opensearch-access-role ``` ## Assume the role locally Run this command to get temporary credentials: ```bash aws sts assume-role \ --role-arn arn:aws:iam:::role/plane-opensearch-access-role \ --role-session-name session ``` Export the credentials from the response: ```bash export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_SESSION_TOKEN= ``` ## Store the Cohere API key in Secrets Manager 1. Go to **Secrets Manager → Store a new secret**. 2. Select **Other type of secret**. 3. Set the key to `azure_ai_foundry_key_cohere` and the value to your Cohere API key. 4. Click **Next**, name the secret `plane-ai/cohere`, and click **Store**. 5. Note the **Secret ARN** — you'll need it in the next step. ## Create a Cohere connector in OpenSearch Using the temporary credentials from Step 4, send a `POST` request to your OpenSearch cluster. **Endpoint:** `POST https:///_plugins/_ml/connectors/_create` **Request body:** ```json { "name": "Cohere", "description": "Cohere embedding connector", "version": "1", "protocol": "http", "parameters": { "endpoint": "https://azureaitrials-resource.services.ai.azure.com/models", "model": "embed-v-4-0", "api_version": "2024-05-01-preview", "input_type": "search_document", "truncate": "END" }, "credential": { "secretArn": "", "roleArn": "arn:aws:iam:::role/plane-opensearch-access-role" }, "actions": [ { "action_type": "predict", "method": "POST", "url": "${parameters.endpoint}/embeddings?api-version=${parameters.api_version}", "headers": { "api-key": "${credential.secretArn.azure_ai_foundry_key_cohere}", "x-ms-model-mesh-model-name": "embed-v-4-0" }, "request_body": "{ \"texts\": ${parameters.texts}, \"truncate\": \"${parameters.truncate}\", \"model\": \"${parameters.model}\", \"input_type\": \"${parameters.input_type}\" }", "pre_process_function": "connector.pre_process.cohere.embedding", "post_process_function": "connector.post_process.cohere.embedding" } ] } ``` Save the `connector_id` from the response. ## Configure the OpenSearch cluster Run these commands in **Dev Tools** in OpenSearch Dashboards. ### Allow the connector's external endpoints ```json PUT /_cluster/settings { "persistent": { "plugins.ml_commons.trusted_connector_endpoints_regex": [ "^https://api\\.cohere\\.ai(/.*)?$", "^https://azureaitrials-resource\\.services\\.ai\\.azure\\.com(/.*)?$" ] } } ``` ### Register the embedding model ```json POST /_plugins/_ml/models/_register { "name": "cohere_4_0_embed", "function_name": "remote", "connector_id": "", "description": "Cohere Embedding Model" } ``` Save the `model_id` from the response. ### Deploy the model ``` POST /_plugins/_ml/models//_deploy ``` ### Verify deployment status ``` GET /_plugins/_ml/models/ ``` Wait until the response shows: ```json "model_state": "DEPLOYED" ``` ### Test inference (optional) ```json POST /_plugins/_ml/models//_predict { "parameters": { "inputs": ["hello world"] } } ``` ## Configure Plane Add the deployed model ID and configuration to `/opt/plane/plane.env`: ```bash OPENSEARCH_ML_MODEL_ID= EMBEDDING_MODEL=cohere/embed-v4.0 OPENSEARCH_EMBEDDING_DIMENSION=1536 ``` Restart Plane and complete the remaining steps in [Configure embedding model](/self-hosting/govern/plane-ai/configure-embedding-model). --- --- url: 'https://developers.plane.so/self-hosting/govern/god-mode/session-expiry.html' description: >- Set how long user and admin sessions stay active before requiring re-authentication on a self-hosted Plane instance. --- # Configure session expiry Instance administrators can control how long a signed-in session stays valid before the user has to sign in again. This is set from the instance admin panel and applies to everyone on the instance. ## Where to configure it 1. Open the **instance admin panel** (God mode). 2. Go to **System Configurations**. 3. Find the **Session settings** section. ## Settings There are two independent settings. | Setting | Applies to | Unit | Default | Minimum | | ------------------------ | ---------------------------------------------------- | ----- | ------- | ------- | | **Session expiry** | Everyone signed in to the Plane app (all workspaces) | Days | 7 days | 1 day | | **Admin session expiry** | The instance admin panel (God mode) session | Hours | 1 hour | 1 hour | **Session expiry (days).** How long a user's session to the main Plane app stays valid. This is shared across the whole instance; it is not set per workspace or per user. **Admin session expiry (hours).** How long a session to the instance admin panel stays valid. This is deliberately shorter than the app session because the admin panel is more sensitive. It applies only to the God-mode admin panel, not to workspace admins or members. Enter a whole number in each field and save. Values below the minimum are rejected. ## How expiry behaves **Main app sessions are inactivity-based.** A user's session stays alive as long as they keep using Plane. Each active session's expiry is extended in the background (at most once per day) so active users are not signed out mid-work. A session only lapses after the configured number of days of inactivity. **Admin panel sessions are an absolute limit.** The admin session is not extended by activity. It expires the configured number of hours after sign-in, regardless of use, and the administrator must sign in again. In both cases, when a session expires the user is signed out and must re-authenticate to continue. ## Setting it with environment variables The same two values can also be set with environment variables, which is useful for scripted or reproducible deployments. These are specified in **seconds**. | Variable | Meaning | Default (seconds) | | -------------------------- | -------------------------- | ----------------- | | `SESSION_COOKIE_AGE` | Main app session length | `604800` (7 days) | | `ADMIN_SESSION_COOKIE_AGE` | Admin panel session length | `3600` (1 hour) | A value set in the admin panel is stored as the instance configuration and takes precedence over the environment default. ## Notes * Session expiry is **instance-wide**. Every workspace and user on the instance shares the same **Session expiry** value; there is no per-workspace or per-user override. * "Admin session" refers to the **instance admin panel** (God mode), not to workspace Admins or Owners. Workspace admins use the main app session like everyone else. * Changing these values does not sign out existing sessions immediately; the new length applies as sessions are created or refreshed. --- --- url: 'https://developers.plane.so/self-hosting/govern/opentelemetry.html' description: >- Export traces, metrics, and logs from a self-hosted Plane instance to any OpenTelemetry-compatible backend using the plane-enterprise Helm chart. --- # Configure OpenTelemetry Plane ships with built-in [OpenTelemetry](https://opentelemetry.io/) (OTel) instrumentation across its backend services. When enabled, Plane exports traces, metrics, and logs over OTLP to any OpenTelemetry-compatible backend you run — an OpenTelemetry Collector, Grafana Tempo, Datadog, Honeycomb, and others. **OpenTelemetry support is off by default and is fully opt-in.** When disabled, no OTel configuration is rendered and your deployment is unchanged. ::: info This guide covers Kubernetes deployments using the `plane-enterprise` Helm chart version **3.3.0 or later**. Plane does **not** deploy a collector for you — you bring your own OTLP endpoint. ::: ## How it works When you enable OpenTelemetry, the Helm chart: 1. Renders a ConfigMap (`-otel-vars`) with the standard `OTEL_*` environment variables built from your Helm values. 2. Renders a Secret (`-otel-secrets`) holding `OTEL_EXPORTER_OTLP_HEADERS` if you configure authentication headers, or references a secret you manage yourself. 3. Injects both into Plane's backend workloads, along with a per-service `OTEL_SERVICE_NAME`, so each service reports under its own name. The following services are instrumented: | Service | Signals exported | | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `api`, `external-api`, `worker`, `worker-importers`, `beat-worker`, `automation-consumer`, `agent-consumer`, `webhook-consumer`, `outbox-poller` | Traces and metrics over OTLP; JSON logs on stdout are enriched with trace and span IDs | | `live`, `live-exporter`, `silo` | Traces, logs, and metrics over OTLP | | `space` (server-side rendering) | Logs and request spans over OTLP/HTTP | | `pi-api`, `pi-worker`, `pi-beat` (Plane AI) | Logs over OTLP/HTTP | | `web`, `admin`, `space` (browser) | Optional browser traces and logs — see [Browser tracing](#browser-tracing) | The Python API instruments Django, Celery, PostgreSQL, Redis, and outgoing HTTP calls; trace context propagates from incoming HTTP requests through Celery tasks. The Node services (`live`, `silo`) use auto-instrumentation for HTTP, Express, PostgreSQL, Redis, and AMQP, and bridge their application logs into the OTLP logs pipeline with trace correlation. ## Prerequisites * The `plane-enterprise` Helm chart version 3.3.0 or later. See the [Kubernetes installation guide](/self-hosting/methods/kubernetes) to install or upgrade. * An OTLP endpoint reachable from the pods in your cluster — typically an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) running in the same cluster. ::: tip Choosing the protocol The Django and Node services support both OTLP/gRPC (usually port `4317`) and OTLP/HTTP (usually port `4318`). The Plane AI (`pi-*`) services and `space` server-side rendering export over OTLP/HTTP only. If you run Plane AI, expose your collector's HTTP receiver and use `protocol: http/protobuf` with the HTTP port so every service can export to the same endpoint. ::: ## Enable OpenTelemetry Enable it with two values — the toggle and the endpoint: ```bash helm upgrade plane-app plane/plane-enterprise \ --namespace plane \ --reuse-values \ --set observability.otel.enabled=true \ --set observability.otel.endpoint=http://otlp.example.com:4317 ``` Or in your values file: ```yaml observability: otel: enabled: true endpoint: http://otlp.example.com:4317 ``` If `endpoint` is left empty, the services skip OTel setup even when `enabled` is `true`, so nothing is exported. ## Configuration reference All settings live under `observability.otel` in the chart values: | Value | Default | Maps to | Description | | -------------------- | ------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------- | | `enabled` | `false` | `OTEL_ENABLED` | Master switch. When `false`, no OTel resources or environment variables are created. | | `endpoint` | `""` | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP receiver URL. An `https://` endpoint uses a secure connection. | | `protocol` | `grpc` | `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` or `http/protobuf`. | | `headers` | `""` | `OTEL_EXPORTER_OTLP_HEADERS` | Exporter headers such as ingestion credentials, in `key=value,key=value` form. Stored in a Kubernetes Secret. | | `environment` | `""` | `OTEL_ENVIRONMENT` | Sets the `deployment.environment.name` resource attribute (for example, `production`). | | `resourceAttributes` | `""` | `OTEL_RESOURCE_ATTRIBUTES` | Additional resource attributes, in `key=value,key=value` form. | | `sampler` | `always_on` | `OTEL_TRACES_SAMPLER` | Trace sampler: `always_on`, `parentbased_traceidratio`, `traceidratio`, or `always_off`. | | `samplerArg` | `"1.0"` | `OTEL_TRACES_SAMPLER_ARG` | Sampling ratio for the ratio-based samplers. Ignored by `always_on`. | | `debugConsole` | `false` | `OTEL_DEBUG_CONSOLE` | Also prints spans to stdout. For debugging only. | | `frontend.enabled` | `false` | `FRONTEND_OTEL_ENABLED` | Enables browser tracing for the web, admin, and space apps. Requires `frontend.endpoint`. | | `frontend.endpoint` | `""` | `FRONTEND_OTLP_ENDPOINT` | Browser-reachable OTLP/HTTP endpoint. This value is public — see [Browser tracing](#browser-tracing). | | `frontend.headers` | `x-otlp-browser=1` | `FRONTEND_OTLP_HEADERS` | Headers sent by the browser exporter. These are visible to anyone using the app, so never put secrets here. | A production-style example: ```yaml observability: otel: enabled: true endpoint: https://otlp.vendor.example.com:4317 protocol: grpc environment: production resourceAttributes: "cluster=eu-1" sampler: parentbased_traceidratio samplerArg: "0.25" ``` ## Authentication headers If your backend requires an ingestion key, you have two options. **Let the chart manage the secret.** Set `observability.otel.headers` and the chart creates the `-otel-secrets` Secret for you: ```bash --set observability.otel.headers='x-api-key=your_ingestion_key' ``` **Bring your own secret.** If you manage secrets externally (for example, with the [External Secrets Operator](/self-hosting/govern/external-secrets)), point the chart at a Secret you own. The chart then skips creating its own Secret and references yours instead: ```yaml external_secrets: otel_env_existingSecret: my-otel-headers ``` The Secret is injected into the pods with `envFrom`, so the data key must be named exactly like the environment variable it supplies: | Key | Required | Value | | ---------------------------- | -------- | ------------------------------------------------------------------------------------------ | | `OTEL_EXPORTER_OTLP_HEADERS` | Yes | Exporter headers in `key=value,key=value` form, for example `x-api-key=your_ingestion_key` | Created manually, it looks like this: ```bash kubectl create secret generic my-otel-headers \ --namespace plane \ --from-literal=OTEL_EXPORTER_OTLP_HEADERS='x-api-key=your_ingestion_key' ``` Or as an `ExternalSecret` target synced by the External Secrets Operator: ```yaml apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: otel-external-secret namespace: plane spec: refreshInterval: 1m secretStoreRef: name: cluster-aws-secretsmanager kind: ClusterSecretStore target: name: my-otel-headers # referenced by external_secrets.otel_env_existingSecret creationPolicy: Owner data: - secretKey: OTEL_EXPORTER_OTLP_HEADERS remoteRef: key: prod/secrets/otel property: OTEL_EXPORTER_OTLP_HEADERS ``` Keep the following in mind: * The Secret must live in the same namespace as the Plane release and is referenced as non-optional — if it doesn't exist yet (for example, ESO hasn't synced it), the instrumented pods won't start until it appears. * Because the Secret is applied with `envFrom`, any other keys in it also become environment variables on every instrumented pod. Keep it limited to the `OTEL_*` values you intend to set. ## Sampling The chart defaults to `always_on`, which exports every trace. That's the right starting point for evaluating the integration, but on a busy instance you'll likely want head sampling to control volume and cost: ```yaml observability: otel: sampler: parentbased_traceidratio samplerArg: "0.1" # keep 10% of traces ``` `parentbased_traceidratio` respects the sampling decision of an incoming trace context, so distributed traces stay complete. ## Browser tracing Backend telemetry stays inside your cluster, but you can optionally have the `web`, `admin`, and `space` apps report traces and logs from users' browsers: ```yaml observability: otel: enabled: true endpoint: http://otlp.example.com:4317 frontend: enabled: true endpoint: https://otlp-browser.example.com ``` Keep the following in mind: * The endpoint must be reachable from your users' browsers and must be an OTLP/**HTTP** receiver — browsers can't speak gRPC. The apps append `/v1/traces` and `/v1/logs` to the endpoint you configure. * The receiver must allow cross-origin requests (CORS) from your Plane domain. * `frontend.endpoint` and `frontend.headers` are served to every visitor through Plane's public instance configuration. Treat them as public values and use a dedicated, rate-limited receiver rather than credentials you care about. * Keep `frontend.headers` non-empty. A custom header forces the browser exporter to send over XHR instead of `navigator.sendBeacon` — beacon requests include credentials, which fail CORS against a wildcard `Access-Control-Allow-Origin` and silently break browser export. The default `x-otlp-browser=1` exists for exactly this reason. Browser tracing is read by the Plane API and delivered to the apps at runtime — you don't need to rebuild any images to turn it on or off. ## Verify the setup 1. Confirm the ConfigMap rendered with your values: ```bash kubectl get configmap -otel-vars -n plane -o yaml ``` 2. Check the API pod's startup logs for the confirmation line: ```bash kubectl logs deploy/-api -n plane | grep "OpenTelemetry configured" ``` You should see something like `OpenTelemetry configured: service=api, endpoint=..., protocol=grpc, sampler=always_on(1.0)`. 3. Generate some traffic in Plane and look for spans from `api` in your backend. Traces from a single request should span the API, Celery workers, and database calls. If nothing arrives, check that the endpoint is reachable from a pod in the Plane namespace, and that the protocol matches the receiver port — gRPC receivers usually listen on `4317` and HTTP receivers on `4318`. --- --- url: 'https://developers.plane.so/self-hosting/govern/external-secrets.html' description: >- Use external secrets operators with Plane on Kubernetes. Integrate AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault for secure configuration. --- # Configure external secrets for Kubernetes deployments This guide explains how to integrate Plane with external secret management solutions, enabling secure and centralized management of sensitive configuration data. The examples provided cover AWS Secrets Manager and HashiCorp Vault integrations, but you can adapt these patterns to your preferred secret management solution. ## AWS Secrets Manager 1. Create a dedicated IAM user (e.g., `external-secret-access-user`). You can uncheck **Console Access Required**. 2. Generate `ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` and keep them handy. 3. Note the user's ARN for later use (format: `arn:aws:iam:::user/`). 4. Create IAM policy (e.g., `external-secret-access-policy`) with the following JSON: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "secretsmanager:GetResourcePolicy", "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:ListSecretVersionIds" ], "Resource": ["arn:aws:secretsmanager:::secret:*"] } ] } ``` Replace `` and `` with your AWS region and account ID. 5. Create IAM role (e.g., external-secret-access-role) with the following trust relationship: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "" }, "Action": "sts:AssumeRole" } ] } ``` Replace `` with the ARN of the user created in step 1. 6. Attach the AWS IAM policy created in step 4 to the IAM role. 7. Create secrets in AWS Secrets Manager with your Plane configuration values. For example, store RabbitMQ credentials with a name like `prod/secrets/rabbitmq`. | Key | Value | | --------------------- | -------- | | RABBITMQ\_DEFAULT\_USER | plane | | RABBITMQ\_DEFAULT\_PASS | plane123 | Follow this pattern to manage all the [environment variables](/self-hosting/methods/kubernetes#external-secrets-config) in AWS Secrets Manager. 8. Create a Kubernetes secret containing AWS credentials in your application namespace: ```sh kubectl create secret generic aws-creds-secret \ --from-literal=access-key= \ --from-literal=secret-access-key= \ -n ``` 9. Apply the following YAML to create a ClusterSecretStore resource: ```yaml apiVersion: external-secrets.io/v1 kind: ClusterSecretStore metadata: name: cluster-aws-secretsmanager namespace: spec: provider: aws: service: SecretsManager role: arn:aws:iam:::role/ region: eu-west-1 auth: accessKeyIDSecretRef: name: aws-creds-secret key: access-key secretAccessKeySecretRef: name: aws-creds-secret key: secret-access-key ``` Replace `` and `` with your AWS account ID and the role name created in Step 5. 10. Create an ExternalSecret resource to fetch secrets from AWS and create a corresponding Kubernetes secret: ```yaml apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: rabbitmq-external-secrets namespace: spec: refreshInterval: 1m secretStoreRef: name: cluster-aws-secretsmanager # ClusterSecretStore name kind: ClusterSecretStore target: name: rabbitmq-secret # Target Kubernetes secret name creationPolicy: Owner data: - secretKey: RABBITMQ_DEFAULT_USER remoteRef: key: prod/secrets/rabbitmq property: RABBITMQ_DEFAULT_USER - secretKey: RABBITMQ_DEFAULT_PASS remoteRef: key: prod/secrets/rabbitmq ``` Make sure to set all [environment variables](/self-hosting/methods/kubernetes#external-secrets-config) in the AWS Secrets Manager, and then access them via ExternalSecret resources in your Kubernetes cluster. ## HashiCorp Vault 1. Access the Vault UI at `https:///`. 2. Set up a KV secrets engine if not already configured. 3. Create a secret with your Plane configuration values (e.g., `secrets/rabbitmq_secrets`). For this example, we're setting up RabbitMQ credentials: | Key | Value | | --------------------- | -------- | | RABBITMQ\_DEFAULT\_USER | plane | | RABBITMQ\_DEFAULT\_PASS | plane123 | Follow this pattern to manage all the other [environment variables](/self-hosting/methods/kubernetes#external-secrets-config) in the Vault. 4. Create a Kubernetes secret containing your Vault token in your application namespace: ```sh kubectl create secret generic vault-token -n --from-literal=token= ``` 5. Apply the following YAML to create a ClusterSecretStore resource: ```yaml apiVersion: external-secrets.io/v1 kind: ClusterSecretStore metadata: name: vault-backend namespace: spec: provider: vault: server: "https://" # the address of your vault instance path: "secrets" # path for accessing the secrets version: "v2" # Vault API version auth: tokenSecretRef: name: "vault-token" # Use a k8s secret called vault-token key: "token" # Use this key to access the vault token ``` Replace `` with your Vault server address. 6. Create an ExternalSecret resource to fetch secrets from Vault and create a corresponding Kubernetes secret: ```yaml apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: rabbitmq-external-secrets namespace: # application-namespace spec: refreshInterval: "1m" secretStoreRef: name: vault-backend # ClusterSecretStore name kind: ClusterSecretStore target: name: rabbitmq-secret # Target Kubernetes secret name creationPolicy: Owner data: - secretKey: RABBITMQ_DEFAULT_USER remoteRef: key: secrets/data/rabbitmq_secrets property: RABBITMQ_DEFAULT_USER - secretKey: RABBITMQ_DEFAULT_PASS remoteRef: key: secrets/data/rabbitmq_secrets ``` Follow this pattern to manage all the environment variables in the Vault, then access them via ExternalSecret resources in your Kubernetes cluster. --- --- url: 'https://developers.plane.so/self-hosting/govern/aws-workload-identity.html' description: >- Configure Plane on Amazon EKS to reach its S3 bucket with an IAM role instead of static access keys, using IRSA or EKS Pod Identity. --- # Use IRSA or EKS Pod Identity for S3 storage When you run Plane on Amazon EKS with an external S3 bucket, you don't have to store an access key and secret in your cluster. Plane can assume an IAM role instead, using either of the two mechanisms AWS provides: * **IRSA** (IAM Roles for Service Accounts) — the role is bound to a Kubernetes ServiceAccount through the cluster's OIDC provider, and requested by an annotation on that ServiceAccount. * **EKS Pod Identity** — the role is bound to a ServiceAccount through an EKS API object called a *pod identity association*. No annotation is involved. Either way there is no long-lived credential to rotate, leak, or commit, and access is scoped by IAM policy rather than by whoever holds the key. ## Requirements Both mechanisms are supported by both editions, but only from these chart versions onward: | Edition | Chart | Minimum version | | ---------- | ------------------ | --------------- | | Commercial | `plane-enterprise` | **3.6.0** | | Community | `plane-ce` | **1.8.0** | ::: warning Check your chart version first Earlier charts render the ServiceAccount with no `annotations` block, and Helm **silently ignores** values keys a chart doesn't define. On those versions a `serviceAccount.annotations` block is a no-op: the annotation never reaches the ServiceAccount, the IRSA webhook never fires, and uploads fail after deploy while your `values.yaml` looks correct. ::: ```bash helm repo update plane helm search repo plane/plane-enterprise --versions | head -5 helm search repo plane/plane-ce --versions | head -5 ``` ## How Plane picks up the role Plane's API builds its S3 client without passing credentials of its own, so the AWS SDK walks its **default credential chain**. On EKS that chain finds whatever the pod was given: | Mechanism | What lands in the pod | | ---------------- | ------------------------------------------------------------------------------------- | | IRSA | A projected web-identity token, plus `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE` | | EKS Pod Identity | `AWS_CONTAINER_CREDENTIALS_FULL_URI`, pointing at the node's Pod Identity Agent | Both resolve to short-lived credentials that the SDK refreshes on its own. ::: danger An empty access key is worse than no access key The chain is only consulted when `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are **absent from the environment entirely**. An *empty* value is still a value: the SDK treats it as an explicit credential, signs every request with an empty access key, and never falls back to the role. Uploads then fail with `InvalidClientTokenId` or `InvalidAccessKeyId` while the configuration looks correct. The chart handles this for you — it omits both variables when `env.aws_access_key` and `env.aws_secret_access_key` are empty. Just don't set them to `""` expecting them to be ignored, and don't set them alongside a role: **a static key always wins over the pod's identity.** ::: ## Choose a mechanism | | **EKS Pod Identity** | **IRSA** | | ----------------------------- | --------------------------------------------------------- | -------------------------------------- | | Cluster prerequisite | Pod Identity Agent add-on | IAM OIDC provider for the cluster | | How the role is bound | An EKS association (cluster + namespace + ServiceAccount) | An annotation on the ServiceAccount | | Role trust policy | One policy, reusable for every cluster | References one cluster's OIDC issuer | | Reusable across clusters | Yes | No — one trust entry per cluster | | Requires EKS | Yes | No — works on any OIDC-capable cluster | | Visible in the chart's output | No, it is configured out of band | Yes, as an annotation | Prefer **Pod Identity** on new EKS clusters: the trust policy is simpler and portable. Use **IRSA** if you are on an older cluster, already standardized on it, or running Kubernetes somewhere other than EKS. ## Step 1 — Create the IAM policy Both mechanisms need the same permissions on the bucket. Plane generates presigned URLs for browser uploads and downloads, and copies, inspects, and deletes objects as assets change. Create a policy — for example `plane-s3-access` — replacing ``: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "PlaneBucketLevel", "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": "arn:aws:s3:::" }, { "Sid": "PlaneObjectLevel", "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::/*" } ] } ``` ::: info If you plan to run the `update_bucket` command to [switch to private buckets](/self-hosting/govern/private-bucket), add `s3:PutBucketPolicy` and `s3:GetBucketPolicy` on the bucket ARN. You can remove them again once the migration is done. ::: ## Step 2 — Create the role and bind it Pick the tab that matches the mechanism you chose. In both cases the ServiceAccount name is the one the chart uses — by default `-srv-account`, so `plane-app-srv-account` for a release named `plane-app`. ::: code-group ```bash [EKS Pod Identity] # 1. Install the Pod Identity Agent add-on (once per cluster). aws eks create-addon \ --cluster-name \ --addon-name eks-pod-identity-agent # 2. Create the role with the Pod Identity trust policy (see below), then attach the policy. aws iam create-role \ --role-name plane-s3-role \ --assume-role-policy-document file://pod-identity-trust.json aws iam attach-role-policy \ --role-name plane-s3-role \ --policy-arn arn:aws:iam:::policy/plane-s3-access # 3. Associate the role with Plane's ServiceAccount. aws eks create-pod-identity-association \ --cluster-name \ --namespace plane \ --service-account plane-app-srv-account \ --role-arn arn:aws:iam:::role/plane-s3-role ``` ```bash [IRSA] # 1. Make sure the cluster has an IAM OIDC provider (once per cluster). eksctl utils associate-iam-oidc-provider \ --cluster \ --approve # 2. Create the role with the IRSA trust policy (see below), then attach the policy. aws iam create-role \ --role-name plane-s3-role \ --assume-role-policy-document file://irsa-trust.json aws iam attach-role-policy \ --role-name plane-s3-role \ --policy-arn arn:aws:iam:::policy/plane-s3-access ``` ::: ### Trust policy ::: code-group ```json [pod-identity-trust.json] { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "pods.eks.amazonaws.com" }, "Action": ["sts:AssumeRole", "sts:TagSession"] } ] } ``` ```json [irsa-trust.json] { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam:::oidc-provider/oidc.eks..amazonaws.com/id/" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks..amazonaws.com/id/:aud": "sts.amazonaws.com", "oidc.eks..amazonaws.com/id/:sub": "system:serviceaccount:plane:plane-app-srv-account" } } } ] } ``` ::: ::: warning The Pod Identity trust policy needs **both** `sts:AssumeRole` and `sts:TagSession` — EKS tags the session with the cluster and ServiceAccount, and the association fails to deliver credentials without it. For IRSA, the `sub` condition must match the namespace and ServiceAccount exactly. Get your cluster's `` with: ```bash aws eks describe-cluster --name \ --query "cluster.identity.oidc.issuer" --output text ``` ::: ::: tip `eksctl` can create the role, attach the policy, and bind it in one step: ```bash eksctl create podidentityassociation \ --cluster \ --namespace plane \ --service-account-name plane-app-srv-account \ --permission-policy-arns arn:aws:iam:::policy/plane-s3-access ``` ::: ## Step 3 — Configure the chart Point Plane at the external bucket and **leave the access key and secret unset**. The two charts differ only in where the MinIO toggle lives — `services.minio.local_setup` on Commercial, `minio.local_setup` on Community. ::: code-group ```yaml [Commercial — plane-enterprise] services: minio: local_setup: false serviceAccount: create: true # Must match the association or the trust policy. Defaults to -srv-account. name: "" annotations: # IRSA only. For EKS Pod Identity, remove this block entirely. eks.amazonaws.com/role-arn: arn:aws:iam:::role/plane-s3-role # Advisory: turns on upgrade warnings about an attached identity. cloudIdentity: true env: docstore_bucket: aws_region: # Leave all three empty so the chart omits them and the SDK uses the pod's role. aws_access_key: "" aws_secret_access_key: "" aws_s3_endpoint_url: "" ``` ```yaml [Community — plane-ce] minio: local_setup: false serviceAccount: create: true # Must match the association or the trust policy. Defaults to -srv-account. name: "" annotations: # IRSA only. For EKS Pod Identity, remove this block entirely. eks.amazonaws.com/role-arn: arn:aws:iam:::role/plane-s3-role # Advisory: turns on upgrade warnings about an attached identity. cloudIdentity: true env: docstore_bucket: aws_region: # Leave all three empty so the chart omits them and the SDK uses the pod's role. aws_access_key: "" aws_secret_access_key: "" aws_s3_endpoint_url: "" ``` ::: Then upgrade: ```bash helm upgrade --install plane-app plane/plane-enterprise \ --create-namespace \ --namespace plane \ -f values.yaml \ --timeout 10m \ --wait \ --wait-for-jobs ``` ::: warning Set `env.aws_region` Leave `aws_s3_endpoint_url` empty for AWS S3 so the SDK derives the correct regional endpoint, but do set `aws_region`. Without a region, requests are signed for the wrong scope and S3 rejects them. ::: ### Settings reference | Setting | Default | Description | | ------------------------------ | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `serviceAccount.create` | `true` | Set to `false` to run as a ServiceAccount you manage outside the chart — for example one created by `eksctl` or Terraform along with the role. | | `serviceAccount.name` | `""` | Name of the ServiceAccount every workload runs as. Defaults to `-srv-account`. This is the name the association or trust policy must match. | | `serviceAccount.annotations` | `{}` | Annotations on the ServiceAccount. Where the IRSA `eks.amazonaws.com/role-arn` binding goes. Helm values only — this is a map, so it is not offered in the Rancher UI. | | `serviceAccount.podLabels` | `{}` | Extra pod-template labels. Not needed for IRSA or Pod Identity; Azure Workload Identity requires one. | | `serviceAccount.cloudIdentity` | `false` | Advisory only, and changes nothing in the rendered output. Declares that this ServiceAccount is bound to an identity configured out of band, which lets `helm upgrade` warn about an identity's reach. | ::: danger One ServiceAccount for every workload Both charts run **all** workloads under this single ServiceAccount — Postgres, Redis, RabbitMQ, MinIO, and OpenSearch included, not just the API and workers. A role attached here is reachable from all of them. If least privilege matters, create your own ServiceAccount for the role, set `serviceAccount.create: false`, and point `serviceAccount.name` at it. ::: ### Migrating from static access keys If this instance previously ran with `env.aws_access_key` set, check that the old value is really gone after the upgrade: ```bash kubectl get secret plane-app-doc-store-secrets -n plane \ -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' ``` ::: warning On `plane-ce`, that command can still return a value. Helm cannot always remove a Secret key it has stopped rendering, and a leftover `AWS_ACCESS_KEY_ID` is found first by the credential chain — the exact failure described above, with nothing in `values.yaml` to explain it. If a value comes back, delete the Secret and re-run the upgrade so the chart recreates it: ```bash kubectl delete secret plane-app-doc-store-secrets -n plane ``` `plane-enterprise` 3.6.0 and later write this Secret in a way that removes the key for you, so no manual step is needed there. ::: ## Step 4 — Verify 1. Confirm the binding reached the ServiceAccount and the pods. ::: code-group ```bash [Pod Identity] aws eks list-pod-identity-associations \ --cluster-name \ --namespace plane # The agent injects this into the pod: kubectl exec -n plane deploy/plane-app-api -- \ printenv AWS_CONTAINER_CREDENTIALS_FULL_URI ``` ```bash [IRSA] kubectl get sa plane-app-srv-account -n plane \ -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}' # The webhook injects these into the pod: kubectl exec -n plane deploy/plane-app-api -- \ printenv AWS_ROLE_ARN AWS_WEB_IDENTITY_TOKEN_FILE ``` ::: ::: info The IRSA environment variables are injected by a mutating webhook **when the pod is created**. Annotating the ServiceAccount on an already-running deployment changes nothing until the pods restart, so roll them: `kubectl rollout restart deploy -n plane`. ::: 2. Confirm no static key is shadowing the role. Both commands should return nothing: ```bash kubectl exec -n plane deploy/plane-app-api -- printenv AWS_ACCESS_KEY_ID kubectl exec -n plane deploy/plane-app-api -- printenv AWS_SECRET_ACCESS_KEY ``` 3. Confirm the role is actually assumed and the bucket is reachable: ```bash kubectl exec -n plane deploy/plane-app-api -- \ python -c "import boto3; print(boto3.client('sts').get_caller_identity()['Arn'])" ``` The output should name `plane-s3-role`. If it names a user or a different role, the pod is picking up credentials from somewhere else — most often the node's instance profile. 4. Upload an attachment to a work item in the UI, then confirm the object exists: ```bash aws s3 ls s3:/// --recursive | tail ``` ## CORS and private buckets A role changes only *how Plane authenticates*. It doesn't change how the browser reaches S3: uploads and downloads still use presigned URLs, so the bucket still needs a CORS policy. See [Switch from public to private buckets](/self-hosting/govern/private-bucket) for the policy and for migrating existing objects to private storage. ## Troubleshoot | Symptom | Cause and fix | | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | The `role-arn` annotation never appears on the ServiceAccount | The chart predates this feature and Helm ignored the values key. Upgrade to `plane-enterprise` 3.6.0 or `plane-ce` 1.8.0 or later. | | Uploads fail with `InvalidClientTokenId` or `InvalidAccessKeyId` | An empty or leftover `AWS_ACCESS_KEY_ID` is being used as a real credential. Clear `env.aws_access_key` and `env.aws_secret_access_key`, check no `external_secrets.storage.secretName` supplies them, then see [Migrating from static access keys](#migrating-from-static-access-keys). | | `NoCredentialsError` / `Unable to locate credentials` | Nothing was injected. For IRSA the pods predate the annotation — restart them. For Pod Identity the agent add-on is missing, or the association's namespace or ServiceAccount name doesn't match. | | `AccessDenied` on `sts:AssumeRoleWithWebIdentity` | The IRSA trust policy's `sub` doesn't match. It must be exactly `system:serviceaccount::`, and the OIDC issuer must be this cluster's. | | Pod Identity association created, but no credentials arrive | The trust policy is missing `sts:TagSession`, or the Pod Identity Agent isn't running: `kubectl get pods -n kube-system -l app.kubernetes.io/name=eks-pod-identity-agent`. | | `get_caller_identity` returns the node's role | The role wasn't bound, so the SDK fell through to the node instance profile. Re-check the association or the annotation, then restart the pods. | | Presigned URLs expire earlier than expected | A presigned URL can't outlive the temporary credentials that signed it. Keep `SIGNED_URL_EXPIRATION` below the role's maximum session duration, or raise that duration on the role. | | `IllegalLocationConstraintException` or signature errors | `env.aws_region` is unset or doesn't match the bucket's region. | --- --- url: 'https://developers.plane.so/self-hosting/govern/reverse-proxy.html' description: >- Configure Nginx, Caddy, or Traefik as a reverse proxy for self-hosted Plane. Setup upstream proxying, headers, and WebSocket support. --- # Configure external reverse proxy This page provides configuration for setting up an external reverse proxy with Plane. The setup below applies to Plane Commercial Edition; for Community Edition, see [Community Edition](#community-edition). ## Plane environment setup Make sure to update the following environment variables in your plane.env file. 1. Assign free ports for Plane to listen on. Update the following variables with two different unsused ports: ```bash LISTEN_HTTP_PORT= LISTEN_HTTPS_PORT= ``` 2. Update the SITE\_ADDRESS variable to `:80` ```bash SITE_ADDRESS=:80 ``` This is required so that generated links and redirects work correctly behind the proxy: 3. After editing plane.env, restart your instance so the changes take effect: ```bash sudo prime-cli restart ``` ::: warning **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. ::: ## Proxy setup 1. Choose the appropriate [configuration template](#configuration-templates) for your reverse proxy. 2. Replace the following placeholders: * ``\ Your Plane application's domain name. * ``\ The IP address where Plane is hosted. * ``\ The port Plane listens on. 3. For Traefik, also update `your-email@example.com` with your email. Ensure that your reverse proxy setup follows the template provided, and that the forwarded headers and ports are correctly set to match the environment variable configuration. ## Configuration templates All configurations include: * Automatic HTTPS redirection * WebSocket support * Standard proxy headers * SSL/TLS certificate management * NGINX: Uses Certbot * Caddy: Handles certificates automatically * Traefik: Uses Let’s Encrypt ::: details NGINX configuration ```bash server { server_name ; location / { proxy_pass http://:/; # Set headers for proxied request proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_http_version 1.1; } client_max_body_size 10M; listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live//fullchain.pem; ssl_certificate_key /etc/letsencrypt/live//privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; } server { if ($host = ) { return 301 https://$host$request_uri; } listen 80; server_name ; return 404; } ``` ::: ::: details Caddy configuration ```bash { tls { # Caddy will automatically handle certificates } redir / https://{host}{uri} permanent reverse_proxy : { header_up X-Forwarded-Proto {scheme} header_up X-Forwarded-Host {host} header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up Host {http.request.host} header_up Upgrade {http.request.header.Upgrade} header_up Connection {http.request.header.Connection} transport http { tls_insecure_skip_verify read_buffer 4096 write_buffer 4096 } } request_body { max_size 10MB } } ``` ::: ::: details Traefik configuration ```bash entryPoints: web: address: ":80" http: redirections: entryPoint: to: websecure scheme: https permanent: true websecure: address: ":443" certificatesResolvers: letsencrypt: acme: email: your-email@example.com # Replace with your email storage: acme.json httpChallenge: entryPoint: web providers: http: routers: plane-router: rule: "Host(``)" service: plane-service entryPoints: - websecure tls: certResolver: letsencrypt services: plane-service: loadBalancer: servers: - url: "http://:" passHostHeader: true responseForwarding: flushInterval: "100ms" serversTransport: maxIdleConnsPerHost: 100 forwardingTimeouts: dialTimeout: 30s responseHeaderTimeout: 30s idleConnTimeout: 90s middlewares: headers: headers: customRequestHeaders: X-Forwarded-Proto: "https" X-Real-IP: "{{ .RemoteAddr }}" ``` ::: ## Community Edition Plane Community Edition ships with a bundled Caddy reverse proxy. To put your own reverse proxy in front of it, disable the bundled proxy and route traffic to the individual containers yourself. ### 1. Disable the bundled proxy To use your own reverse proxy, you must first disable the built-in proxy service. Open your `docker-compose.yml` file and comment out or remove the `proxy` service at the end of the file. ### 2. Connect to the Plane Docker network Your custom reverse proxy needs to resolve the internal container hostnames (like `web`, `api`, `admin`). The easiest way to achieve this is to run your reverse proxy container on the same Docker network as Plane (usually `plane-app_default`), or map all necessary ports from the individual containers to your host machine. ### 3. Route mapping Plane Community Edition relies on path-based routing to direct traffic to different services. You must map the following paths to their respective containers and internal ports: | Route path | Destination container | Internal port | | :--------------------------------------- | :-------------------- | :------------ | | `/*` (catch-all) | `web` | 3000 | | `/spaces/*` | `space` | 3000 | | `/god-mode/*` | `admin` | 3000 | | `/live/*` | `live` | 3000 | | `/api/*`, `/auth/*`, `/static/*` | `api` | 8000 | | `//*` (default: `/uploads`) | `plane-minio` | 9000 | ### 4. Configuration templates Below are example configurations for NGINX, Caddy, and Traefik, assuming your proxy runs on the same Docker network as the Plane containers. ::: details NGINX configuration ```bash server { listen 80; server_name ; # File uploads (MinIO) # Replace 'uploads' with your configured AWS_S3_BUCKET_NAME if changed location ~ ^/uploads(/.*)?$ { proxy_pass http://plane-minio:9000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # API, Auth, and Static location ~ ^/(api|auth|static)(/.*)?$ { proxy_pass http://api:8000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Live (WebSockets) location /live/ { proxy_pass http://live:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # God mode (Admin) location /god-mode/ { proxy_pass http://admin:3000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Spaces location /spaces/ { proxy_pass http://space:3000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Frontend (Catch-all) location / { proxy_pass http://web:3000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } client_max_body_size 50M; } ``` ::: ::: details Caddy configuration ```bash { request_body { max_size 50MB } redir /spaces /spaces/ permanent reverse_proxy /spaces/* space:3000 redir /god-mode /god-mode/ permanent reverse_proxy /god-mode/* admin:3000 reverse_proxy /live/* live:3000 reverse_proxy /api/* api:8000 reverse_proxy /auth/* api:8000 reverse_proxy /static/* api:8000 # Replace 'uploads' with your actual bucket name if changed reverse_proxy /uploads/* plane-minio:9000 reverse_proxy /uploads plane-minio:9000 reverse_proxy /* web:3000 } ``` ::: ::: details Traefik configuration With Traefik, you can apply labels directly in your `docker-compose.yml` to the respective containers instead of a static configuration file. Assuming Traefik is set up to read Docker labels: ```yaml services: web: # ... labels: - "traefik.enable=true" - "traefik.http.routers.plane-web.rule=Host(``)" - "traefik.http.services.plane-web.loadbalancer.server.port=3000" api: # ... labels: - "traefik.enable=true" - "traefik.http.routers.plane-api.rule=Host(``) && (Path(`/api`) || PathPrefix(`/api/`) || Path(`/auth`) || PathPrefix(`/auth/`) || Path(`/static`) || PathPrefix(`/static/`))" - "traefik.http.services.plane-api.loadbalancer.server.port=8000" admin: # ... labels: - "traefik.enable=true" - "traefik.http.routers.plane-admin.rule=Host(``) && (Path(`/god-mode`) || PathPrefix(`/god-mode/`))" - "traefik.http.services.plane-admin.loadbalancer.server.port=3000" space: # ... labels: - "traefik.enable=true" - "traefik.http.routers.plane-space.rule=Host(``) && (Path(`/spaces`) || PathPrefix(`/spaces/`))" - "traefik.http.services.plane-space.loadbalancer.server.port=3000" live: # ... labels: - "traefik.enable=true" - "traefik.http.routers.plane-live.rule=Host(``) && (Path(`/live`) || PathPrefix(`/live/`))" - "traefik.http.services.plane-live.loadbalancer.server.port=3000" plane-minio: # ... labels: - "traefik.enable=true" - "traefik.http.routers.plane-minio.rule=Host(``) && (Path(`/uploads`) || PathPrefix(`/uploads/`))" - "traefik.http.services.plane-minio.loadbalancer.server.port=9000" ``` ::: --- --- url: 'https://developers.plane.so/self-hosting/govern/private-bucket.html' description: >- Switch Plane storage from public to private S3 buckets. Secure file uploads with signed URLs and access control for commercial editions. --- # Switch from public to private buckets • Commercial Edition ::: warning Starting with v1.4.0 of the Commercial edition Plane will use private storage buckets for any file uploaded to your Plane instance. ::: ::: info New installations with default storage, which is MiniO, don't need to change anything. For S3 or S3-compatible storage, please see [this](https://developers.plane.so/self-hosting/govern/database-and-storage). ::: While you can use the current public storage paradigm that Plane has followed so far, we highly recommend you migrate to private storage buckets which ensure greater security and give you more control over how files are accessed. ::: info To keep public storage on external S3 compatible services, you still have to update your CORS policy. ::: See the instructions to switch to private storage by the provider you use below. ## For default MinIO storage Simply run the command ↓. ```bash docker exec -it python manage.py update_bucket ``` A successful run keeps any public files you already have accessible while moving you to private storage. ## For external storage • S3 or S3 compatible There are two parts to this—updating your CORS policy and then switching to private storage. ### Update bucket's CORS policy ::: warning This step is critical if you are using external storage to ensure continued functionality. ::: Here’s a sample CORS policy for your reference. Just replace `` with your actual domain and apply the policy to your bucket. ```bash [ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET", "POST", "PUT", "DELETE", "HEAD" ], "AllowedOrigins": [ "", ], "ExposeHeaders": [ "ETag", "x-amz-server-side-encryption", "x-amz-request-id", "x-amz-id-2" ], "MaxAgeSeconds": 3000 } ] ``` ### Switch to private storage ::: warning Don't start from here if you haven't updated your CORS policy. ::: To migrate from public to private bucket storage, follow the instructions below: 1. First, make sure you have the following permissions on your S3 bucket. If you don't, make changes to get those permissions on your bucket first. * **s3:GetObject**\ So you can access your public files so far To access existing objects publicly * **s3:ListBucket**\ So you can apply policies to your bucket for public access * **s3:PutObject**\ So you can create new files * **s3:PutBucketPolicy**\ So you can update your buckets' policy 2. Now, run the command ↓. ```bash docker exec -it python manage.py update_bucket ``` ::: tip 1. If the command finds the necessary permissions missing, it will generate a `permissions.json` file which you can use to update your bucket policy manually. Here’s how the `permissions.json` file should look. ```bash { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": [ "arn:aws:s3:::/", "arn:aws:s3:::/" ] } ] } ``` 2. To copy the `permissions.json` file to the local machine, run the command ↓. ```bash docker cp :/code/permissions.json . ``` ::: ## Related * [Use IRSA or EKS Pod Identity for S3 storage](/self-hosting/govern/aws-workload-identity) — grant these bucket permissions to an IAM role instead of an access key. ## Troubleshoot * [Bucket policy exceeds size limit](/self-hosting/troubleshoot/storage-errors#bucket-policy-exceeds-size-limit) --- --- url: 'https://developers.plane.so/self-hosting/govern/environment-variables.html' description: >- Configure environment variables for Plane. Complete reference of all configuration options and settings. --- # Environment variables reference This guide provides a comprehensive overview of all environment variables used in the Commercial Edition. These variables allow you to customize your Plane instance to best fit your organization's needs. ## Where to find the .env file The environment file for Plane Commercial Edition is located at: ```bash /opt/plane/plane.env ``` This is where you'll make all configuration changes. Remember to restart the instance after making changes to ensure they take effect. ## Environment variables ### General settings | Variable | Description | Default Value | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | **INSTALL\_DIR** | Directory where Plane is installed. | /opt/plane | | **DOMAIN\_NAME** | Primary domain name for your Plane instance. This determines how users will access your installation. | localhost | | **APP\_RELEASE\_VERSION** | The version of Plane Commercial Edition you're running. This helps with troubleshooting and ensures compatibility. | *Current release version* | | **WEB\_URL** | The complete base URL for the web application including protocol (e.g., `https://plane.example.com`). | http://localhost | | **CORS\_ALLOWED\_ORIGINS** | Comma-separated list of origins allowed to make cross-origin requests to your API. Usually, this should include your WEB\_URL. | http://localhost | | **DEBUG** | Toggles debug mode for more verbose logging and debugging information. | 0 (disabled) | ### Scaling and performance | Variable | Description | Default Value | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------- | | **WEB\_REPLICAS** | Number of web server replicas for load balancing. | 1 | | **SPACE\_REPLICAS** | Number of space service replicas for workspaces. | 1 | | **ADMIN\_REPLICAS** | Number of admin service replicas. | 1 | | **API\_REPLICAS** | Number of API service replicas. | 1 | | **WORKER\_REPLICAS** | Number of worker service replicas for background tasks. | 1 | | **BEAT\_WORKER\_REPLICAS** | Number of beat worker replicas for scheduled tasks. | 1 | | **LIVE\_REPLICAS** | Number of live service replicas for real-time updates. | 1 | | **GUNICORN\_WORKERS** | Number of Gunicorn workers for handling web requests. Increase for better performance on high-traffic instances. | 2 | | **SILO\_REPLICAS** | Number of Silo (integration) service replicas. | 1 | | **IFRAMELY\_REPLICAS** | Number of Iframely service replicas for link previews and embeds. | 1 | | **EMAIL\_REPLICAS** | Number of email service replicas. Set to `1` to enable the built-in email intake service. | 0 | | **AUTOMATION\_CONSUMER\_REPLICAS** | Number of automation consumer replicas for processing automation events. | 1 | | **OUTBOX\_POLLER\_REPLICAS** | Number of outbox poller replicas for event processing. | 1 | | **PI\_API\_REPLICAS** | Number of Plane Intelligence API replicas. Set to `1` to enable AI features. | 0 | | **PI\_BEAT\_REPLICAS** | Number of Plane Intelligence beat worker replicas for scheduled AI tasks. | 0 | | **PI\_WORKER\_REPLICAS** | Number of Plane Intelligence worker replicas for background AI processing. | 0 | | **PI\_MIGRATOR\_REPLICAS** | Number of Plane Intelligence migrator replicas. Set to `1` to run PI database migrations. | 0 | ### Networking and security | Variable | Description | Default Value | | ------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------- | | **LISTEN\_HTTP\_PORT** | Port for HTTP traffic. | 80 | | **LISTEN\_HTTPS\_PORT** | Port for HTTPS traffic. | 443 | | **APP\_PROTOCOL** | Protocol to be used, either `http` or `https`. | http | | **TRUSTED\_PROXIES** | CIDR notation of trusted proxies for request forwarding. Important when behind load balancers or reverse proxies. | 0.0.0.0/0 | | **SSL\_VERIFY** | Whether to verify SSL certificates for outgoing connections. Set to `0` only in development environments. | 1 | | **LISTEN\_SMTP\_PORT\_25** | Port for SMTP traffic on port 25. | 25 | | **LISTEN\_SMTP\_PORT\_465** | Port for SMTPS traffic on port 465. | 465 | | **LISTEN\_SMTP\_PORT\_587** | Port for SMTP submission traffic on port 587. | 587 | ### SSL and certificates | Variable | Description | Default Value | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | | **CERT\_EMAIL** | Email used for SSL certificate registration with Let's Encrypt or other ACME providers. | admin@example.com | | **CERT\_ACME\_CA** | ACME Certificate Authority URL for SSL certificate issuance. | https://acme-v02.api.letsencrypt.org/directory | | **CERT\_ACME\_DNS** | DNS provider configuration for SSL certificate domain validation. Format varies by provider. | | | **SITE\_ADDRESS** | The domain name and port required by Caddy for serving your Plane instance. This determines how Caddy will handle incoming requests. | localhost:80 | ### Database settings | Variable | Description | Default Value | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | **PGHOST** | Hostname or IP address of your PostgreSQL server. | plane-db | | **PGDATABASE** | Name of the PostgreSQL database Plane will use. | plane | | **POSTGRES\_USER** | Username for PostgreSQL authentication. | plane | | **POSTGRES\_PASSWORD** | Password for PostgreSQL authentication. **Critical:** Use a strong, unique password here. | plane | | **POSTGRES\_DB** | Same as PGDATABASE - the name of the PostgreSQL database. | plane | | **POSTGRES\_PORT** | TCP port your PostgreSQL server is listening on. | 5432 | | **PGDATA** | Directory path where PostgreSQL data is stored. Only relevant if you're managing PostgreSQL within the same container/system. | /var/lib/postgresql/data | | **DATABASE\_URL** | Full connection string for PostgreSQL. If provided, this takes precedence over individual connection parameters. Format: `postgresql://username:password@host:port/dbname` | | ### Redis settings | Variable | Description | Default Value | | -------------- | -------------------------------------------- | ------------- | | **REDIS\_HOST** | Hostname or IP address of your Redis server. | plane-redis | | **REDIS\_PORT** | TCP port your Redis server is listening on. | 6379 | | **REDIS\_URL** | Full connection string for Redis. | | ### RabbitMQ settings | Variable | Description | Default Value | | -------------------------- | --------------------------------------------------------------------------------------- | ------------- | | **RABBITMQ\_HOST** | Hostname or IP address of your RabbitMQ server. | plane-mq | | **RABBITMQ\_PORT** | TCP port your RabbitMQ server is listening on. | 5672 | | **RABBITMQ\_DEFAULT\_USER** | Username for RabbitMQ authentication. | plane | | **RABBITMQ\_DEFAULT\_PASS** | Password for RabbitMQ authentication. | plane | | **RABBITMQ\_DEFAULT\_VHOST** | Virtual host for RabbitMQ, providing logical separation of resources. | plane | | **RABBITMQ\_VHOST** | Virtual host name for RabbitMQ used by application services. | plane | | **AMQP\_URL** | Full connection string for RabbitMQ. Format: `amqp://username:password@host:port/vhost` | | ### Authentication and security | Variable | Description | Default Value | | -------------------------- | ------------------------------------------------------------------------------------------------------- | ------------- | | **SECRET\_KEY** | Secret key used for various cryptographic operations, including JWT token signing. | | | **MACHINE\_SIGNATURE** | Unique identifier for your instance, used for licensing and authentication. | | | **LIVE\_SERVER\_SECRET\_KEY** | Secret key for communication between the API and live (real-time) service. Must match on both services. | | | **PI\_INTERNAL\_SECRET** | Secret key for internal communication with the Plane Intelligence service. | | | **SILO\_HMAC\_SECRET\_KEY** | HMAC secret key for authenticating requests between the API and Silo (integration) service. | | | **AES\_SECRET\_KEY** | AES encryption key used for encrypting sensitive integration credentials at rest. | | ### File Storage (MinIO / S3) | Variable | Description | Default Value | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | **USE\_MINIO** | Determines whether to use MinIO for object storage. Set to `1` to enable MinIO, `0` to use configured S3 or local storage. | 1 | | **USE\_STORAGE\_PROXY** | Whether to proxy file storage requests through the application server. Set to `1` to enable. | 0 | | **AWS\_REGION** | AWS region for S3 storage services. | | | **AWS\_ACCESS\_KEY\_ID** | Access key for MinIO or AWS S3 authentication. | | | **AWS\_SECRET\_ACCESS\_KEY** | Secret key for MinIO or AWS S3 authentication. | | | **AWS\_S3\_ENDPOINT\_URL** | Custom endpoint URL for MinIO or S3-compatible storage. | http://plane-minio:9000 | | **AWS\_S3\_BUCKET\_NAME** | S3 bucket name for file storage. | uploads | | **MINIO\_ROOT\_USER** | Username for MinIO authentication. This is effectively your MinIO admin account. | access-key | | **MINIO\_ROOT\_PASSWORD** | Password for MinIO root user authentication. Keep this secure as it provides full access to your storage. | secret-key | | **BUCKET\_NAME** | S3 bucket name where all file uploads will be stored. This bucket will be automatically created if it doesn't exist. | uploads | | **FILE\_SIZE\_LIMIT** | Maximum file upload size in bytes. | 5242880 (5MB) | | **MINIO\_ENDPOINT\_SSL** | Force HTTPS for MinIO when dealing with SSL termination. Set to `1` to enable. | 0 | ### GitHub integration | Variable | Description | Default Value | | ------------------------ | ------------------------------------------------ | ------------- | | **GITHUB\_CLIENT\_ID** | OAuth client ID for GitHub integration. | | | **GITHUB\_CLIENT\_SECRET** | OAuth client secret for GitHub integration. | | | **GITHUB\_APP\_NAME** | GitHub App name for enhanced GitHub integration. | | | **GITHUB\_APP\_ID** | GitHub App ID for enhanced GitHub integration. | | | **GITHUB\_PRIVATE\_KEY** | Private key for GitHub App authentication. | | ### Slack integration | Variable | Description | Default Value | | ----------------------- | ------------------------------------------ | ------------- | | **SLACK\_CLIENT\_ID** | OAuth client ID for Slack integration. | | | **SLACK\_CLIENT\_SECRET** | OAuth client secret for Slack integration. | | ### GitLab integration | Variable | Description | Default Value | | ------------------------ | ------------------------------------------- | ------------- | | **GITLAB\_CLIENT\_ID** | OAuth client ID for GitLab integration. | | | **GITLAB\_CLIENT\_SECRET** | OAuth client secret for GitLab integration. | | ### OpenSearch | Variable | Description | Default Value | | --------------------------- | ----------------------------------------------------------- | ------------------------------------ | | **OPENSEARCH\_ENABLED** | Enable OpenSearch integration | 1 | | **OPENSEARCH\_URL** | OpenSearch endpoint URL | https://opensearch.example.com:9200/ | | **OPENSEARCH\_USERNAME** | Authentication username | admin | | **OPENSEARCH\_PASSWORD** | Authentication password | your-secure-password | | **OPENSEARCH\_INDEX\_PREFIX** | Prefix for all index names (useful for multi-tenant setups) | (empty) | ### Plane AI #### Plane AI replicas To start Plane AI services, set each replica count to `1`: | Variable | Description | Required | | ------------------------ | ---------------------------------- | -------- | | **PI\_API\_REPLICAS** | Plane AI API replica count | Yes | | **PI\_BEAT\_REPLICAS** | Plane AI Beat Worker replica count | Yes | | **PI\_WORKER\_REPLICAS** | Plane AI Worker replica count | Yes | | **PI\_MIGRATOR\_REPLICAS** | Plane AI Migrator replica count | Yes | #### Database settings ::: info Plane AI database Plane AI uses a separate PostgreSQL database. Create a new database (e.g. `plane_pi`) on your PostgreSQL server, then set **PLANE\_PI\_DATABASE\_URL** to its connection string. Example: `postgresql://user:password@host:5432/plane_pi` ::: | Variable | Description | Default Value | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | **PLANE\_PI\_DATABASE\_URL** | Connection string for the Plane AI database. A separate database used by the PI service. | postgresql://plane:plane@plane-db/plane\_pi | | **FOLLOWER\_POSTGRES\_URI** | Connection string for a Plane PostgreSQL DB read replica. Used for read-heavy operations to reduce load on the primary database. | Same as DATABASE\_URL | #### LLM provider API keys Plane AI supports multiple LLM providers. Configure one or more by adding their API keys. | Variable | Description | Required | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | | **OPENAI\_API\_KEY** | API key for OpenAI models | Optional | | **CLAUDE\_API\_KEY** | API key for Anthropic models | Optional | | **GROQ\_API\_KEY** | API key for speech-to-text features | Optional | | **CUSTOM\_LLM\_ENABLED** | Set to `true` to enable a custom LLM. Supports OpenAI-compatible endpoints and AWS Bedrock. | Optional | | **CUSTOM\_LLM\_PROVIDER** | Backend provider for the custom model. Accepted values: `openai` (default), `bedrock`. | Optional | | **CUSTOM\_LLM\_MODEL\_KEY** | Identifier key for the custom model (e.g. a model ID or name). | Optional | | **CUSTOM\_LLM\_BASE\_URL** | Base URL of the custom model's OpenAI-compatible endpoint. Required when `CUSTOM_LLM_PROVIDER=openai`. | Optional | | **CUSTOM\_LLM\_API\_KEY** | API key for authenticating with the custom endpoint. Required for `openai` provider; used as the AWS access key ID when `CUSTOM_LLM_PROVIDER=bedrock`. | Optional | | **CUSTOM\_LLM\_AWS\_REGION** | AWS region for the Bedrock model (e.g. `us-east-1`). Required when `CUSTOM_LLM_PROVIDER=bedrock`. | Optional | | **CUSTOM\_LLM\_NAME** | Display name for the custom model shown in the UI. Defaults to `Custom LLM`. | Optional | | **CUSTOM\_LLM\_MAX\_TOKENS** | Maximum token limit for the custom model. Defaults to `64000`. | Optional | #### Provider base URLs Use these when routing requests through self-hosted gateways, proxies, or compatible third-party endpoints. | Variable | Description | Default | | ------------------- | ------------------------------------------ | --------- | | **OPENAI\_BASE\_URL** | Custom base URL for OpenAI-compatible APIs | OpenAI | | **CLAUDE\_BASE\_URL** | Custom base URL for Claude-compatible APIs | Anthropic | | **COHERE\_BASE\_URL** | Custom base URL for Cohere APIs | Cohere | | **GROQ\_BASE\_URL** | Custom base URL for Groq APIs | Groq | #### Embedding model configuration These settings are required for semantic search and Plane AI Chat. Configure one of the following options. | Variable | Description | Required | | ---------------------------------- | ---------------------------------------------------------------------------------------- | ----------- | | **OPENSEARCH\_ML\_MODEL\_ID** | ID of an existing embedding model deployed in OpenSearch. | Conditional | | **EMBEDDING\_MODEL** | Model used for generating embeddings and query construction (e.g., `cohere/embed-v4.0`). | Required | | **OPENSEARCH\_EMBEDDING\_DIMENSION** | The dimension of the embedding model (e.g., `1536`). | Required | | **COHERE\_API\_KEY** | API key for Cohere embedding models | Conditional | | **BR\_AWS\_ACCESS\_KEY\_ID** | AWS access key ID for Bedrock Titan embedding | Conditional | | **BR\_AWS\_SECRET\_ACCESS\_KEY** | AWS secret access key for Bedrock Titan embedding | Conditional | | **BR\_AWS\_REGION** | AWS region for Bedrock Titan embedding | Conditional | For setup instructions, supported models, and IAM permissions, see [Configure Plane AI](/self-hosting/govern/plane-ai/configure-plane-ai). ### API settings | Variable | Description | Default Value | | ---------------------- | ----------------------------------------------------------------------- | ------------- | | **API\_KEY\_RATE\_LIMIT** | Rate limit for API requests to prevent abuse. Format: `number/timeunit` | 60/minute | ### Email settings | Variable | Description | Default Value | | ----------------------- | ----------------------------------------------------------------------------------------------- | ------------- | | **SMTP\_DOMAIN** | Domain used for the built-in SMTP email intake service. | 0.0.0.0 | | **TLS\_CERT\_PATH** | File path to the TLS certificate for email service encryption. | | | **TLS\_PRIV\_KEY\_PATH** | File path to the TLS private key for email service encryption. | | | **INTAKE\_EMAIL\_DOMAIN** | Domain name for intake email addresses (e.g., `example.com` for `issue+id@example.com` format). | example.com | ### Integration (Silo) settings | Variable | Description | Default Value | | --------------------------------- | ---------------------------------------------------------------------- | ------------- | | **SILO\_BASE\_PATH** | Base path for the Silo integration service. | /silo | | **WEBHOOK\_SECRET** | Secret key for verifying webhook payloads from external integrations. | plane-silo | | **BATCH\_SIZE** | Number of events to process in a single batch for integration syncs. | 60 | | **DEDUP\_INTERVAL** | Interval (in seconds) for deduplication of integration events. | 3 | | **MQ\_PREFETCH\_COUNT** | Number of messages the Silo service prefetches from the message queue. | 10 | | **INTEGRATION\_CALLBACK\_BASE\_URL** | Base URL for integration callbacks. Defaults to `WEB_URL` if not set. | | ### Outbox poller settings | Variable | Description | Default Value | | -------------------------------------------- | --------------------------------------------------------------------- | ------------- | | **OUTBOX\_POLLER\_MEMORY\_LIMIT\_MB** | Maximum memory usage (in MB) before the outbox poller restarts. | 512 | | **OUTBOX\_POLLER\_INTERVAL\_MIN** | Minimum polling interval (in seconds) for the outbox poller. | 0.25 | | **OUTBOX\_POLLER\_INTERVAL\_MAX** | Maximum polling interval (in seconds) for the outbox poller. | 2 | | **OUTBOX\_POLLER\_BATCH\_SIZE** | Number of outbox events to process per polling cycle. | 250 | | **OUTBOX\_POLLER\_MEMORY\_CHECK\_INTERVAL** | Interval (in seconds) between memory usage checks. | 30 | | **OUTBOX\_POLLER\_POOL\_SIZE** | Default connection pool size for the outbox poller. | 4 | | **OUTBOX\_POLLER\_POOL\_MIN\_SIZE** | Minimum number of connections in the pool. | 2 | | **OUTBOX\_POLLER\_POOL\_MAX\_SIZE** | Maximum number of connections in the pool. | 10 | | **OUTBOX\_POLLER\_POOL\_TIMEOUT** | Timeout (in seconds) for acquiring a connection from the pool. | 30.0 | | **OUTBOX\_POLLER\_POOL\_MAX\_IDLE** | Maximum idle time (in seconds) for a connection before it is closed. | 300.0 | | **OUTBOX\_POLLER\_POOL\_MAX\_LIFETIME** | Maximum lifetime (in seconds) for a connection before it is recycled. | 3600 | | **OUTBOX\_POLLER\_POOL\_RECONNECT\_TIMEOUT** | Timeout (in seconds) for reconnecting a dropped connection. | 5.0 | | **OUTBOX\_POLLER\_POOL\_HEALTH\_CHECK\_INTERVAL** | Interval (in seconds) between connection health checks. | 30 | ### Automation consumer settings | Variable | Description | Default Value | | -------------------------------------- | --------------------------------------------------------------- | ------------------------------ | | **AUTOMATION\_EVENT\_STREAM\_QUEUE\_NAME** | RabbitMQ queue name for automation event processing. | plane.event\_stream.automations | | **AUTOMATION\_EVENT\_STREAM\_PREFETCH** | Number of messages to prefetch for automation event processing. | 10 | | **AUTOMATION\_EXCHANGE\_NAME** | RabbitMQ exchange name for automation events. | plane.event\_stream | ### Plane Intelligence (PI) settings | Variable | Description | Default Value | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | **OPENAI\_API\_KEY** | API key for OpenAI services used by Plane Intelligence. | | | **OPENAI\_BASE\_URL** | Custom base URL for OpenAI-compatible API endpoints. | | | **CLAUDE\_API\_KEY** | API key for Anthropic Claude services used by Plane Intelligence. | | | **CLAUDE\_BASE\_URL** | Custom base URL for Claude API endpoints. | | | **GROQ\_API\_KEY** | API key for Groq services used by Plane Intelligence. | | | **GROQ\_BASE\_URL** | Custom base URL for Groq API endpoints. | | | **COHERE\_API\_KEY** | API key for Cohere services used by Plane Intelligence. | | | **COHERE\_BASE\_URL** | Custom base URL for Cohere API endpoints. | | | **CUSTOM\_LLM\_ENABLED** | Enable a custom OpenAI-compatible LLM provider. Set to `true` to enable. | false | | **CUSTOM\_LLM\_MODEL\_KEY** | Model key identifier for the custom LLM. | gpt-oss-120b | | **CUSTOM\_LLM\_BASE\_URL** | Base URL for the custom LLM API endpoint. | | | **CUSTOM\_LLM\_API\_KEY** | API key for the custom LLM provider. | | | **CUSTOM\_LLM\_NAME** | Display name for the custom LLM in the Plane UI. | GPT-OSS-120B | | **CUSTOM\_LLM\_DESCRIPTION** | Description of the custom LLM shown in the Plane UI. | A self-hosted OpenAI-compatible model | | **CUSTOM\_LLM\_MAX\_TOKENS** | Maximum token limit for the custom LLM. | 128000 | | **EMBEDDING\_MODEL** | Model key for generating embeddings (e.g. `cohere/embed-v4.0`). Required for PI API startup when Plane AI is enabled. | | | **OPENSEARCH\_ML\_MODEL\_ID** | OpenSearch ML model ID for the deployed embedding model. | | | **OPENSEARCH\_EMBEDDING\_DIMENSION** | Vector dimension for `knn_vector` fields; must match the embedding model and stay aligned with the API service. See [Configure embedding model](/self-hosting/govern/plane-ai/configure-embedding-model). | 1536 | | **BR\_AWS\_ACCESS\_KEY\_ID** | AWS access key for Amazon Bedrock integration. | | | **BR\_AWS\_SECRET\_ACCESS\_KEY** | AWS secret key for Amazon Bedrock integration. | | | **BR\_AWS\_SESSION\_TOKEN** | AWS session token for Amazon Bedrock integration (for temporary credentials). | | | **FASTAPI\_APP\_WORKERS** | Number of FastAPI workers for the PI service. | 1 | | **PLANE\_OAUTH\_STATE\_EXPIRY\_SECONDS** | Expiry time (in seconds) for PI OAuth state tokens. | 82800 | | **CELERY\_VECTOR\_SYNC\_ENABLED** | Enable periodic vector synchronization for AI-powered search. | 0 | | **CELERY\_VECTOR\_SYNC\_INTERVAL** | Interval (in seconds) for vector synchronization. | 3 | | **CELERY\_WORKSPACE\_PLAN\_SYNC\_ENABLED** | Enable periodic workspace plan synchronization. | 0 | | **CELERY\_WORKSPACE\_PLAN\_SYNC\_INTERVAL** | Interval (in seconds) for workspace plan synchronization. | 86400 | | **CELERY\_DOCS\_SYNC\_ENABLED** | Enable periodic documents synchronization for AI indexing. | 0 | | **CELERY\_DOCS\_SYNC\_INTERVAL** | Interval (in seconds) for documents synchronization. | 86400 | ::: details Community Edition This guide provides a comprehensive overview of all environment variables available for configuring your self-hosted Plane Community Edition. Use these variables to customize your instance to fit your deployment needs. ## Where to find the environment file The environment configuration file is located at: ```bash plane-selfhost/plane-app/plane.env ``` ## Environment Variables ### General settings | Variable | Description | Default Value | Containers | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ---------------------------------------- | | **APP\_DOMAIN** | Domain name for your Plane instance. This configures the global URLs and proxy routing via docker-compose. | localhost | docker-compose | | **APP\_RELEASE** | Release version of Plane (e.g., 'stable'). Determines the Docker image tags to pull. | stable | All containers | | **WEB\_URL** | The complete base URL for the web application including protocol. Essential for email links and integrations. | http://${APP\_DOMAIN} | api, worker, beat-worker, migrator | | **CORS\_ALLOWED\_ORIGINS** | Comma-separated list of origins allowed to make cross-origin requests to your API. | http://${APP\_DOMAIN} | live, api, worker, beat-worker, migrator | | **DEBUG** | Toggles Django API debug mode for verbose logging. Set to `1` to enable, `0` to disable. Not recommended in production as it may expose sensitive information. | 0 | api, worker, beat-worker, migrator | | **LISTEN\_HTTP\_PORT** | Port for HTTP traffic on the reverse proxy. The primary port your users will connect to. | 80 | proxy | | **LISTEN\_HTTPS\_PORT** | Port for HTTPS traffic on the reverse proxy. The primary port your users will connect to. | 443 | proxy | ### Scaling and performance | Variable | Description | Default Value | Containers | | ------------------------ | ------------------------------------------------------------------------------------------------- | ------------- | ----------- | | **WEB\_REPLICAS** | Number of web server replicas for serving the frontend UI. Increase for better load distribution. | 1 | web | | **SPACE\_REPLICAS** | Number of space service replicas handling workspace-related operations. | 1 | space | | **ADMIN\_REPLICAS** | Number of admin service replicas for administrative functions. | 1 | admin | | **API\_REPLICAS** | Number of API service replicas processing API requests. | 1 | api | | **WORKER\_REPLICAS** | Number of worker service replicas handling background tasks. | 1 | worker | | **BEAT\_WORKER\_REPLICAS** | Number of beat worker replicas for scheduled/periodic tasks. | 1 | beat-worker | | **LIVE\_REPLICAS** | Number of live service replicas for real-time updates and WebSocket connections. | 1 | live | | **GUNICORN\_WORKERS** | Number of Gunicorn workers per API instance. Increase for better request handling capacity. | 1 | api | ### API settings | Variable | Description | Default Value | Containers | | ---------------------- | ----------------------------------------------------------------------- | ------------- | ---------------------------------- | | **API\_KEY\_RATE\_LIMIT** | Rate limit for API requests to prevent abuse. Format: `number/timeunit` | 60/minute | api, worker, beat-worker, migrator | ### Database settings | Variable | Description | Default Value | Containers | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------- | | **PGHOST** | Hostname or IP address of your PostgreSQL server. Recognized by libpq and Django. | plane-db | api, worker, beat-worker, migrator | | **PGDATABASE** | Name of the PostgreSQL database Plane will use. Recognized by libpq and Django. | plane | api, worker, beat-worker, migrator | | **POSTGRES\_USER** | Username for PostgreSQL authentication. | plane | plane-db (init), api, worker, beat-worker, migrator (fallback) | | **POSTGRES\_PASSWORD** | Password for PostgreSQL authentication. Use a strong, unique password. | plane | plane-db (init), api, worker, beat-worker, migrator (fallback) | | **POSTGRES\_DB** | Same as PGDATABASE - the name of the PostgreSQL database. | plane | plane-db (init), api, worker, beat-worker, migrator (fallback) | | **POSTGRES\_PORT** | TCP port your PostgreSQL server is listening on. | 5432 | api, worker, beat-worker, migrator | | **PGDATA** | Directory path where PostgreSQL data is stored. This is specific to the internal storage path inside the container. | /var/lib/postgresql/data | plane-db | | **DATABASE\_URL** | Full connection string for PostgreSQL. If provided, overrides individual settings. Format: `postgresql://username:password@host:port/dbname` | | api, worker, beat-worker, migrator | ### Redis settings | Variable | Description | Default Value | Containers | | -------------- | ------------------------------------------------------------------------------- | ------------- | ---------------------------------------- | | **REDIS\_HOST** | Hostname or IP address of your Redis server. | plane-redis | live, api, worker, beat-worker, migrator | | **REDIS\_PORT** | TCP port your Redis server is listening on. | 6379 | live, api, worker, beat-worker, migrator | | **REDIS\_URL** | Full connection string for Redis. Format: `redis://username:password@host:port` | | live, api, worker, beat-worker, migrator | ### RabbitMQ settings | Variable | Description | Default Value | Containers | | --------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------- | -------------------------------------------------------------- | | **RABBITMQ\_HOST** | Hostname or IP address of your RabbitMQ server. | plane-mq | api, worker, beat-worker, migrator | | **RABBITMQ\_PORT** | TCP port your RabbitMQ server is listening on. | 5672 | api, worker, beat-worker, migrator | | **RABBITMQ\_USER** | Username for RabbitMQ authentication. | plane | plane-mq (init), api, worker, beat-worker, migrator (fallback) | | **RABBITMQ\_PASSWORD** | Password for RabbitMQ authentication. Use a strong, unique password. | plane | plane-mq (init), api, worker, beat-worker, migrator (fallback) | | **RABBITMQ\_VHOST** | Virtual host for RabbitMQ, providing logical separation of resources. | plane | plane-mq (init), api, worker, beat-worker, migrator (fallback) | | **AMQP\_URL** | Full connection string for RabbitMQ. If not provided, it's constructed from individual settings. | amqp://plane:plane@plane-mq:5672/plane | api, worker, beat-worker, migrator | ### File Storage (MinIO / S3) | Variable | Description | Default Value | Containers | | ------------------------- | --------------------------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------- | | **USE\_MINIO** | Whether to use MinIO for object storage. Set to `1` to enable, `0` to use other configured storage. | 1 | api, worker, beat-worker, migrator | | **MINIO\_ENDPOINT\_SSL** | Force HTTPS for MinIO when handling SSL termination. Set to `1` to enable. | 0 | api, worker, beat-worker, migrator | | **AWS\_REGION** | AWS region for S3 storage services. Applies when using S3 or MinIO. | | api, worker, beat-worker, migrator | | **AWS\_ACCESS\_KEY\_ID** | Access key for MinIO or AWS S3 authentication. | access-key | plane-minio (init admin), api, worker, beat-worker, migrator (client) | | **AWS\_SECRET\_ACCESS\_KEY** | Secret key for MinIO or AWS S3 authentication. | secret-key | plane-minio (init admin), api, worker, beat-worker, migrator (client) | | **AWS\_S3\_ENDPOINT\_URL** | Endpoint URL for MinIO or S3-compatible storage. | | api, worker, beat-worker, migrator | | **AWS\_S3\_BUCKET\_NAME** | S3 bucket name for file storage. All uploads will be stored in this bucket. | uploads | proxy, api, worker, beat-worker, migrator | | **FILE\_SIZE\_LIMIT** | Maximum file upload size in bytes. | 5242880 (5MB) | proxy, api, worker, beat-worker, migrator | ### Security settings | Variable | Description | Default Value | Containers | | -------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------- | | **SECRET\_KEY** | Secret key used for cryptographic operations like session handling and token generation. Should be a long, random string. | | api, worker, beat-worker, migrator | ::: --- --- url: 'https://developers.plane.so/self-hosting/telemetry.html' description: >- Understand what telemetry data Plane collects, how it is used, and how to opt out. Privacy-focused data collection for self-hosted instances. --- # Data collection and usage Plane collects anonymized data to enhance your user experience, ensure product stability, and drive continuous improvements. This article talks about what we collect, what we don't, and how we use collected data. ## What Plane collects With the exception of the instance admin's email address on self-hosted Plane, we don't collect any personally identifiable information (PII). All usage data is anonymized and isn't connected to individual users. ### Instance admin details Plane collects just the instance admin's name and email to communicate essential upgrade news, security alerts, and other critical notifications. ### Instance setup When an instance is created, Plane collects just the instance ID so we can track upgrades and troubleshoot paid features. ### Usage for billing Plane tracks the number of workspaces, members, and roles to bill you correctly and ensure accurate reporting in your workspace's **Billing and plans** screen and the Prime portal. ### Application data Plane tracks anonymized workspace activity, including but not limited to the number of projects, issues, cycles, modules, comments, issue types, custom properties, timesheet downloads, active importers, and integrations. This data helps us understand product usage, adoption, and build new features. ### User interaction and behavior Plane collects anonymized data for how you work with Plane when creating, updating, or deleting Plane entities, including but not restricted to projects, issues, cycles, modules, and others. This data helps us understand user journeys, pain points, and overall behavior. ### Performance data Plane collects machine configurations, environment details, OS flavors, network throughput, and stack traces to help troubleshoot and prevent performance hiccups. ## Data sample ```json { "Created At": "October 7, 2024, 1:50 PM", "Page Count": "14", "Is Telemetry Enabled": "true", "Updated At": "October 7, 2024, 1:50 PM", "User Count": "97", "Cycle Count": "62", "Cycle Issue Count": "710", "Module Issue Count": "428", "ID": "1234567890", "Updated By ID": null, "Current Version": "0.23.0", "Issue Count": "2,000", "Instance ID": "1234567890", "Latest Version": "0.23.0", "Module Count": "81", "Name": "Test", "Workspace Count": "11", "Project Count": "20" } ``` ## How we use the data * **Improving your experience**\ Analyzing real-world usage guides us in making Plane more intuitive and user-friendly. * **Detecting and fixing bugs**\ Identifying bugs and errors as they occur enables us to quickly investigate and fix issues, minimize downtime, and improve product performance. * **Enhancing security**\ Monitoring for abnormal patterns helps us identify and mitigate potential security threats or vulnerabilities, ensuring data protection and platform integrity. * **Driving product decisions**\ Understanding how you interact with Plane helps us prioritize future developments and focus on high-impact features and improvements that drive better outcomes. ## Disable telemetry As much as we'd love to understand your usage better, we agree that you should be in control of your data. If for any reason you don't want to send us that data, you can turn telemetry collection off in three clicks. 1. Go to your instance's God Mode by appending `/god-mode` to the domain you have hosted Plane on. 2. In the **General Settings** pane on the right, disable the **Telemetry** toggle button. ![Disable telemetry](/images/disable-telemetry.webp#hero) 3. Click **Save changes**. --- --- url: 'https://developers.plane.so/self-hosting/manage/upgrade-plane.html' description: >- Upgrade self-hosted Plane to the latest version. Step-by-step guide for updating your Plane installation safely. --- # Update Plane version Keeping Plane up to date ensures you’re using the latest features, improvements, and security fixes. Here’s how to upgrade your Plane installation with a single command. ::: info The upgrade process may involve a brief downtime as services are updated and restarted. ::: ## Prerequisites We recommend creating a backup of your data before any version updates. See [Backup data](/self-hosting/manage/backup-restore). ## Check version You can quickly check your Plane version by clicking the **?** icon on the sidebar. ![Check version number](https://media.docs.plane.so/product/check-version.webp#hero) ## Update version ::: warning For Commercial Edition v1.13.0, ensure you're using the **latest version of Docker Compose**. Check your Docker Compose version with `docker-compose --version` and update if needed. **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. ::: 1. Update your Prime CLI with the command ↓: ```bash sudo prime-cli update-cli ``` The latest version of the CLI ensures your Plane upgrades happen smoothly. 2. To update Plane to the latest version, run: ```bash sudo prime-cli upgrade ``` This command checks for the latest version of Plane and applies the upgrade if a new version is available. ::: details Community Edition > \[!WARNING] > This guide covers how to upgrade from version 0.14.0 and above. If you’re running version 0.13.2 or below, first follow the guide to [upgrade to version v0.14.0](/self-hosting/manage/upgrade-from-0.13.2-0.14.0) before continuing with these steps. #### Prerequisites Before starting, make a backup of your Plane instance. For detailed steps, see the [Backup data](/self-hosting/manage/backup-restore#backup-data) section. This is strongly recommended to ensure you have a safe restore point. #### Update version 1. Download the latest stable release with ↓: ```bash curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh ``` 2. Execute the setup script with ↓: ```bash ./setup.sh ``` This will bring up a menu with several options. Select option `5` to upgrade: ```bash Select a Action you want to perform: 1) Install (x86_64) 2) Start 3) Stop 4) Restart 5) Upgrade 6) View Logs 7) Backup Data 8) Exit Action [2]: 5 ``` Choosing this option stops all services and downloads the latest `docker-compose.yaml` and `variables-upgrade.env` files. The `plane.env` file won’t be overwritten, so your existing environment settings are safe. You’ll see a message indicating the services have been stopped. ![Stopped Docker services](/images/docker-compose/stopped-docker.png) 3. After the update completes, select `6` to exit the prompt. 4. After the upgrade, open `variables-upgrade.env` and compare it with your `plane.env` file. Copy any new variables from `variables-upgrade.env` to your `plane.env` file and set the correct values. This step is essential to ensure that all configuration changes are in place for the latest version. 5. Once your `plane.env` file is updated, start your Plane instance again by selecting option `2`. ::: --- --- url: >- https://developers.plane.so/self-hosting/manage/upgrade-from-0.13.2-0.14.0.html description: >- Upgrade self-hosted Plane to the latest version. Step-by-step guide for updating your Plane installation safely. --- # Mandatory checkpoint at v0.14.0 If you’re upgrading from `v0.13.2` or below, there are some additional migration steps due to significant changes in the self-hosting setup. Follow these instructions to migrate your data to the new volume structure in `v0.14.0`. 1. First, stop the running `v0.13-2` (or older) instance of Plane. If it's still running, you might hit a "ports not available" error, which will prevent the `v0.14-0` containers from starting up correctly. ```bash docker compose down ``` 2. Create a new folder for `v0.14-0` to ensure a clean installation. ```bash mkdir plane-selfhost cd plane-selfhost ``` 3. Set up the environment variable for the `RELEASE` variable, then download and prepare the installation script: ```bash export RELEASE=v0.14-dev curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/install.sh | sed -e 's@BRANCH=${BRANCH:-master}@BRANCH='"$RELEASE"'@' -e 's@APP_RELEASE="stable"@APP_RELEASE='"$RELEASE"'@' > setup.sh chmod +x setup.sh ``` 4. Execute the script to install Plane: ```bash ./setup.sh install ``` 5. Start up your new v0.14-0 Plane instance: ```bash ./setup.sh start ``` 6. Now stop the instance to initialize the new Docker volumes: ```bash ./setup.sh stop ``` 7. Download the migration script: ```bash curl -fsSL -o migrate.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/migration-0.13-0.14.sh chmod +x migrate.sh ``` 8. Run the migration script: ```bash ./migrate.sh ``` You’ll see the following instructions: ``` ****************************************************************** This script is solely for the migration purpose only. This is a 1 time migration of volume data from v0.13.2 => v0.14.x Assumption: 1. Postgres data volume name ends with _pgdata 2. Minio data volume name ends with _uploads 3. Redis data volume name ends with _redisdata Any changes to this script can break the migration. Before you proceed, make sure you run the below command to know the docker volumes docker volume ls -q | grep -i "_pgdata" docker volume ls -q | grep -i "_uploads" docker volume ls -q | grep -i "_redisdata" ******************************************************* Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata" --------------------- plane-app_redisdata v0132_redisdata Provide the Source Volume Prefix : ``` 9. Open a second terminal and run the commands shown above to identify your source and destination volume prefixes. For example, if you run `docker volume ls -q | grep -i "_pgdata"`, you might see something like: ![](/images/update-plane/docker-volumes.png) In this example, `plane-013-dev` is the prefix for `v0.13.2`, and `plane-app` is the prefix for `v0.14.0`. 10. Return to the original terminal, enter the source volume prefix `plane-013-dev` and destination volume prefix `plane-app`, and press ENTER: ```bash Provide the Source Volume Prefix : plane-013-dev Provide the Destination Volume Prefix : plane-app ``` If there are any issues, an error will appear. For a successful migration, there will be no error, and the process will exit quietly. 11. Restart the upgraded v0.14.0 instance with: ```bash ./setup.sh restart ``` 12. Login as instance admin by appending `/god-mode` to your domain. 13. Once logged in, just click **Save Changes** to finalize your setup. 14. You’re all set! Log in to your updated `v0.14-0` instance to check if all of your data has migrated successfully. 15. Now, [update to the latest version](/self-hosting/manage/upgrade-plane#update-version). --- --- url: >- https://developers.plane.so/self-hosting/manage/update-plane/airgapped-edition/update-airgapped-docker.html description: >- Upgrade your airgapped Plane instance running on Docker by cloning images, replacing configuration files, and uploading a new license. --- # Update Airgapped Edition on Docker Since airgapped instances can't pull updates from the internet, updating the version requires manually transferring the latest Docker images and configuration files from a machine with internet access. ## Prerequisites * A machine with internet access to download images and files. * Access to your airgapped Docker registry. * Your current `plane.env` file backed up. ## Update Plane 1. On a machine with internet access, pull the latest Plane images and push them to your airgapped Docker registry. Follow the guide for [cloning and pushing Plane Docker images](https://developers.plane.so/self-hosting/methods/clone-docker-images). Once complete, the latest Plane images are available in your internal registry. 2. On the same machine with internet access, download the updated `docker-compose.yml` and environment template for your target version. ```bash # Download docker-compose.yml curl -fsSL https://prime.plane.so/releases//docker-compose-airgapped.yml -o docker-compose.yml # Download environment template curl -fsSL https://prime.plane.so/releases//variables-airgapped.env -o plane.env ``` Transfer both files to your airgapped instance and replace the existing ones. Before replacing your existing `plane.env`, compare it with the new template. Copy over any custom values from your old plane.env into the new template. The new template may include additional variables required by the latest version, so always use the new file as the base and bring your existing values into it. :::info Replace `` with the version you're upgrading to (e.g., v2.6.3). Check the [release notes](https://plane.so/changelog?category=self-hosted) for the latest available release version. ::: 3. Download the latest license file for the new version from [prime.plane.so](https://prime.plane.so). Follow [this guide](https://developers.plane.so/self-hosting/manage/manage-licenses/activate-airgapped) to activate license. 4. Restart the instance to bring the instance back up with the new configuration. ```bash docker compose up -d ``` Verify the upgrade by checking the version in your Plane application. --- --- url: >- https://developers.plane.so/self-hosting/manage/update-plane/airgapped-edition/update-airgapped-kubernetes.html description: >- Upgrade your airgapped Plane instance running on Kubernetes by cloning images, updating the Helm chart, and redeploying. --- # Update Airgapped Edition on Kubernetes Since airgapped clusters can't pull updates from the internet, upgrading requires manually transferring Docker images to your private registry and updating the Helm chart. ## Prerequisites * A machine with internet access to download images and the Helm chart. * Access to your airgapped Docker registry used by the cluster. * Your current Helm `values.yaml` file backed up. ## Update Plane 1. On a machine with internet access, pull the latest Plane images and push them to your air-gapped Docker registry. Follow the guide for [cloning and pushing Plane Docker images](https://developers.plane.so/self-hosting/methods/clone-docker-images). Once complete, the latest Plane images are available in your internal registry. 2. Download the latest Plane Enterprise Helm chart. You can check the most recent version on [Artifact Hub](https://artifacthub.io/packages/helm/makeplane/plane-enterprise). ```bash # Using wget wget https://github.com/makeplane/helm-charts/releases/download/plane-enterprise-/plane-enterprise-.tgz # Using curl curl -L -O https://github.com/makeplane/helm-charts/releases/download/plane-enterprise-/plane-enterprise-.tgz ``` Transfer the `.tgz` file to a machine that can access the cluster. :::info Replace \ with the latest Helm chart version (e.g., 2.2.4). You can check the most recent version on [Artifact Hub](https://artifacthub.io/packages/helm/makeplane/plane-enterprise). ::: Before replacing your existing `values.yaml`, compare it with the new Helm chart's default values. Copy over any custom configuration from your old `values.yaml` into the new template. The new chart version may include additional or renamed fields, so always use the new default values as the base and bring your existing configuration into it. 3. In your `values.yaml`, update `planeVersion` to match the version of Plane images you pushed to the registry. ```yaml planeVersion: ``` :::info Replace `` with the version you're upgrading to (e.g., v2.6.3). Check the [release notes](https://plane.so/changelog?category=self-hosted) for the latest available release version. ::: 4. Once the Helm chart and `values.yaml` file are updated, redeploy the Helm release in your Kubernetes cluster to complete the update. Verify the upgrade by checking the version in your Plane application. --- --- url: >- https://developers.plane.so/self-hosting/manage/manage-licenses/activate-pro-and-business.html description: >- Activate your Plane Pro or Business Edition license. Upgrade from Community Edition to unlock advanced project management features. --- # Activate Pro or Business on Commercial Edition Pro and Business plan licenses are activated at the workspace level as each license is tied to a specific workspace. ## Activate license key Activate a paid plan license on your self-hosted Plane instance using a license key from the Prime portal. 1. Login to the [Prime portal](https://prime.plane.so/licenses) with the same email address you used to purchase one of our paid plans. 2. Go to [Manage licenses](https://prime.plane.so/licenses). Copy the value of the **License key** for the license you want to activate. ![Manage licenses](https://media.docs.plane.so/activate-license/copy-license-key.webp#hero) > \[!TIP] > Click the **Get more licenses** button as in the image above on the [Prime portal](https://prime.plane.so/licenses/plans) to buy more licenses. 3. On the Plane app, navigate to **Workspace Settings > Billing and plans** 4. Click the **Activate this Workspace** button. ![Activate workspace](https://media.docs.plane.so/activate-license/enter-license-key-selfhosted.webp#hero-tr) 5. Paste the license key in the **Enter license key** box. 6. Click **Activate**. You will see a confirmation. 7. That's it. To check your plan at any time and find additional details, just go to the **Billing and plans** tab in **Workspace Settings**. ![Manage subscription](https://media.docs.plane.so/activate-license/pro-activated-cloud.webp#hero-tr) ## Sync plan If you've made changes to your subscription, like renewing your license, upgrading your plan, or adjusting seats, use **Sync plan** to pull those updates into your workspace. Syncing refreshes your workspace with the latest subscription information from the Prime server, including plan type, seat count, expiration dates, and feature access. **To sync your plan:** 1. Navigate to **Workspace Settings > Billing and plans**. 2. Click **Sync plan**. 3. Wait for the sync to complete. You'll see a confirmation once your workspace is updated. Use this whenever you see a mismatch between what's in the Prime portal and what appears in your workspace, or after making any subscription changes externally. ## Delink license key Your license key is linked to both a workspace and an instance, meaning it can only be used on one workspace on one machine at a time. If you switch machines or reinstall the Commercial edition, you’ll need to reactivate your workspace. This helps prevent any misuse of the license on multiple machines or workspaces. To make it easier for you to move between machines or workspaces, we've added a new Delink feature. This lets you free up your license from its current workspace, so you can reuse it on a new machine or workspace. Here’s how to delink your license key from a workspace: 1. Head over to the **Billing and Plans** screen of the workspace that's currently using the license. 2. Click **Delink license key**. This will release the license key, making it available for use on another machine or workspace. ![Delink license key](https://media.docs.plane.so/activate-license/delink-license-key.webp#hero-tl) 3. Restart the instance using `prime-cli restart`. 4. If you’re switching machines or reinstalling the Commercial edition, see [Move Plane instance to another server](https://developers.plane.so/self-hosting/manage/migrate-plane). 5. Ensure you are connected to the internet and reactivate the new workspace using the license key you delinked earlier. --- --- url: >- https://developers.plane.so/self-hosting/manage/manage-licenses/activate-enterprise.html description: Activate your Enterprise Grid license on the Commercial Edition. --- # Activate Enterprise Grid on Commercial Edition Enterprise Grid licenses are activated at the instance level through God Mode, not through individual workspace settings. This gives instance administrators centralized control over Enterprise features across all workspaces. ## Activate license key 1. Sign in to the [Prime portal](https://prime.plane.so/licenses) with the email address you used to purchase your Enterprise Grid. 2. Navigate to **Manage licenses**. 3. Copy the license key for your Enterprise Grid. 4. Sign in to your Plane instance in [God Mode](/self-hosting/govern/instance-admin). 5. Select **Billing** from the left pane. 6. Paste your license key in the **Activate Enterprise license** field. 7. Click **Activate**. ![Activate Enterprise license](/images/activate-license/activate-enterprise-plan.webp#hero) Once activated, Enterprise features become available across all workspaces on your instance. --- --- url: >- https://developers.plane.so/self-hosting/manage/manage-licenses/activate-airgapped.html description: >- Activate and configure your Plane Airgapped Edition license. Offline license activation for air-gapped environments without internet access. --- # Activate Pro or Business on Airgapped Edition Once your air-gapped installation is running, you'll need to activate your workspace with the license file. 1. Login to the [Prime portal](https://prime.plane.so/licenses) with the same email address you used to purchase the paid plan. 2. Go to [Manage licenses](https://prime.plane.so/licenses). 3. Click **Download license** to download the license file for your Plane version. ![Download license file](/images/activate-license/download-license.webp#hero) 4. Navigate to the [Workspace Settings](https://docs.plane.so/core-concepts/workspaces/overview#workspace-settings) in the Plane application. 5. Select **Billing and plans** on the right pane. 6. Click the **Activate this workspace** button. ![Upload license file](/images/activate-license/upload-airgapped-license-file.webp#hero) 7. Upload the license file to activate your workspace. You now have Plane running in your air-gapped environment. If you run into any issues, check the logs, or reach out to our support team for assistance. --- --- url: >- https://developers.plane.so/self-hosting/manage/manage-licenses/activate-airgapped-enterprise.html description: >- Offline Enterprise Grid license activation for airgapped environments without internet access. --- # Activate Enterprise Grid on Airgapped Edition Once your air-gapped installation is running, you'll need to activate your workspace with the license file. 1. Login to the [Prime portal](https://prime.plane.so/licenses) with the same email address you used to purchase the paid plan. 2. Go to [Manage licenses](https://prime.plane.so/licenses). 3. Click **Download license** to download the license file for your Plane version. ![Download license file](/images/activate-license/download-license.webp#hero) 4. Sign in to your Plane instance in [God Mode](/self-hosting/govern/instance-admin). 5. Select **Billing** from the left pane. 6. Upload the license file to activate your instance. ![Upload license file](/images/activate-license/upload-airgapped-enterprise.webp#hero) 7. Click **Activate**. --- --- url: 'https://developers.plane.so/self-hosting/manage/backup-restore.html' description: >- Backup and restore Plane data. Complete guide for backing up database, storage, and configuration files. --- # Backup and restore data Backing up your data regularly helps prevent data loss and allows you to restore your system quickly if necessary. Follow these instructions to back up and restore your data using Plane’s command-line interface. ## For Docker Compose ### Backup data ::: warning **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. ::: Create a backup of your Plane data with ↓: ```bash sudo prime-cli backup ``` This command initiates a full backup of all critical data, storing it in the default backup location at: ```bash /opt/plane/backups ``` Each backup file will be timestamped to ensure you can easily identify the latest or a specific backup if needed. ### Backup plane.env If you need to back up only the `plane.env` file, you'll need to do it manually. Here’s how: 1. Navigate to the `/opt/plane` folder on your machine or server where Plane is installed.. 2. Locate the `plane.env` file. 3. Copy this file to a different location as a backup, so you can restore it if needed. ### Restore data You can restore your data from a previous backup with ↓: ```bash sudo prime-cli restore ``` This command prompts the restoration process, which will overwrite the current data with the data from the most recent backup file. Ensure you have selected the correct backup before running this command, as restoring will replace your current data. ::: details Community Edition #### Backup data To create a backup, start by running the setup script: ```bash ./setup.sh ``` You’ll see a menu of options—just type 7 to select "Backup Data." ``` Select an Action you want to perform: 1) Install (x86_64) 2) Start 3) Stop 4) Restart 5) Upgrade 6) View Logs 7) Backup Data 8) Exit Action [2]: 7 ``` The system will start backing up the PostgreSQL, Redis, and upload data: ``` Backing Up plane-app_pgdata Backing Up plane-app_redisdata Backing Up plane-app_uploads Backup completed successfully. Backup files are stored in /....../plane-app/backup/20240502-1120 ``` The backup files are stored locally, so you can copy them to an external storage service if needed for extra security. #### Backup plane.env If you need to back up only the `plane.env` file, you'll need to do it manually. Here’s how: 1. Navigate to the folder on your machine or server where Plane is installed.. 2. Locate the `plane.env` file. 3. Copy this file to a different location as a backup, so you can restore it if needed. *** #### Restore data Follow these steps to restore data from a backup: 1. Make sure Plane-CE is installed and started, then stop it. This ensures the necessary Docker volumes are ready. 2. Use the command ↓ to download the restore script. It’s easiest to save it in the same directory as `setup.sh`. ```bash curl -fsSL -o restore.sh https://raw.githubusercontent.com/makeplane/plane/refs/heads/preview/deployments/cli/community/restore.sh chmod +x restore.sh ``` 3. Now, run the command ↓ to restore your data, specifying the path to your backup folder (the folder with the `*.tar.gz` files): ```bash ./restore.sh ``` Here’s an example output for restoring from /opt/plane-selfhost/plane-app/backup/20240722-0914: ```bash -------------------------------------------- ____ _ ///////// | _ \| | __ _ _ __ ___ ///////// | |_) | |/ _` | '_ \ / _ \ ///// ///// | __/| | (_| | | | | __/ ///// ///// |_| |_|\__,_|_| |_|\___| //// //// -------------------------------------------- Project management tool from the future -------------------------------------------- Found /opt/plane-selfhost/plane-app/backup/20240722-0914/pgdata.tar.gz .....Restoring plane-app_pgdata .....Successfully restored volume plane-app_pgdata from pgdata.tar.gz Found /opt/plane-selfhost/plane-app/backup/20240722-0914/redisdata.tar.gz .....Restoring plane-app_redisdata .....Successfully restored volume plane-app_redisdata from redisdata.tar.gz Found /opt/plane-selfhost/plane-app/backup/20240722-0914/uploads.tar.gz .....Restoring plane-app_uploads .....Successfully restored volume plane-app_uploads from uploads.tar.gz Restore completed successfully. ``` 4. Start your Plane instance again with ↓: ```bash ./setup.sh start ``` That’s it! You’re back up and running with your restored data. ::: ## Other deployment methods For Kubernetes, or other deployment methods, use your platform's native backup tools. Plane stores data in two places that need to be backed up: | Component | What it contains | | ----------------------- | ---------------------------------------------------------------------------- | | **PostgreSQL database** | All Plane data — workspaces, projects, work items, users, comments, settings | | **Object storage** | Attachments, uploaded images, files (MinIO, S3, or S3-compatible storage) | ### Configuration files Also back up your environment configuration — this includes database connection strings, storage credentials, and other settings. * **Kubernetes:** Helm values file, ConfigMaps, and Secrets * **Other platforms:** Environment variables or configuration files specific to your setup :::tip Store backups in a separate location from your Plane installation — ideally offsite or in a different cloud region. ::: --- --- url: 'https://developers.plane.so/self-hosting/upgrade-from-community.html' description: >- Upgrade self-hosted Plane to the latest version. Step-by-step guide for updating your Plane installation safely. --- # Upgrade from Community to Commercial Edition The Commercial edition comes with the free plan and the flexibility to upgrade to a paid plan at any point. > \[!WARNING] > The instructions provided on this page are specific to installations using Docker. If you are running Plane on Kubernetes, you'll need to manually create a database dump and back up your file storage by copying the relevant volumes or storage paths. ## Prerequisites * Install the [Commercial Edition](/self-hosting/methods/docker-compose#install-plane) on a fresh machine, not the one running the Plane Community Edition. * Be sure to log in as the root user or as a user with sudo access. The `/opt` folder requires sudo or root privileges. :::tabs key:upgrade-options \== Standard setup (built-in DB & storage) {#standard-setup} This upgrade path is for installations using Plane's default PostgreSQL database and MinIO object storage. ## Back up data on Community instance 1. Download the latest version of `setup.sh`. ```bash curl -fsSL https://github.com/makeplane/plane/releases/latest/download/setup.sh -o setup.sh ``` 2. Run the setup.sh backup script to take the backup of the Community Edition instance. ```bash ./setup.sh backup ``` 3. When done, your data will be backed up to the folder shown on the screen. e.g., `/plane-selfhost/plane-app/backup/20240522-1027` This folder will contain 3 `tar.gz` files. * `pgdata.tar.gz` * `redisdata.tar.gz` * `uploads.tar.gz` 4. Copy all the three files from the server running the Community Edition to any folder on the server running the Commercial Edition. e.g., `~/ce-backup` ## Restore data on Commercial instance 1. Start any command-line interface like Terminal and go into the folder with the back-up files. ``` cd ~/ce-backup ``` 2. Copy and paste the script below on Terminal and hit Enter. ``` TARGET_DIR=/opt/plane/data sudo mkdir -p $TARGET_DIR for FILE in *.tar.gz; do if [ -e "$FILE" ]; then tar -xzvf "$FILE" -C "$TARGET_DIR" else echo "No .tar.gz files found in the current directory." exit 1 fi done # Remove destinations first, then mv sudo rm -rf $TARGET_DIR/db && mv $TARGET_DIR/pgdata $TARGET_DIR/db sudo rm -rf $TARGET_DIR/redis && mv $TARGET_DIR/redisdata $TARGET_DIR/redis mkdir -p $TARGET_DIR/minio sudo rm -rf $TARGET_DIR/minio/uploads && mv $TARGET_DIR/uploads $TARGET_DIR/minio/uploads ``` 3. This script will extract your Community Edition data and restore it to `/opt/plane/data`. \== Managed services (external DB and storage) {#managed-services} This upgrade path is for installations using external or managed database and object storage services (like AWS RDS and S3). Since your data already lives in external services, you only need to update your configuration — no backup and restore required. ## Update configuration for Commercial Edition 1. Open the `plane.env` file located at `/opt/plane/plane.env`. 2. Configure database connection. 1. Find the `DATABASE_URL` environment variable. 2. Verify it points to your external database: ```ini DATABASE_URL=postgresql://user:password@your-db-host:5432/plane ``` If you need to change it, update the value with your managed database connection string. 3. Configure object storage 1. Find the `#DATASTORE SETTINGS` section in `plane.env` 2. Update these environment variables for your external storage: ```ini USE_MINIO=0 AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_S3_ENDPOINT_URL=https://s3.amazonaws.com AWS_S3_BUCKET_NAME=plane-uploads ``` :::info Setting `USE_MINIO=0` disables the local MinIO service and enables external object storage (S3 or S3-compatible services). ::: 3. Restart Plane services to apply the configuration: ```bash prime-cli restart ``` Your Commercial Edition instance is now connected to your existing external database and storage. ::: :::details Manual backup and restore without CLI Use this method if you prefer to back up data manually or if the setup.sh script isn't working for your environment. ### What gets migrated * PostgreSQL database (all Plane data) * MinIO uploads (attachments, images, files) ### Prerequisites * Plane CE and Commercial versions should be compatible * Shell access to both servers * Docker installed on both servers ### Back up data on Community instance 1. Create backup folders: ```bash mkdir -p ~/ce-backups/db mkdir -p ~/ce-backups/minio/uploads cd ~/ce-backups ``` 2. Back up PostgreSQL data: ```bash docker cp plane-app-plane-db-1:/var/lib/postgresql/data/. db/ ``` 3. Back up MinIO uploads: ```bash docker cp plane-app-plane-minio-1:/export/uploads minio/uploads/ ``` 4. Verify backup sizes: ```bash du -sh db minio/uploads ``` Make sure sizes look reasonable (not just a few KB). 5. Transfer backup to Commercial server: ```bash scp -r ~/ce-backups user@commercial-server:/tmp/ ``` ### Restore data on Commercial instance 1. Stop Plane: ```bash prime-cli stop ``` Verify all containers are down: ```bash docker ps ``` 2. Back up existing Commercial data (safety precaution): ```bash mv /opt/plane/data/db /opt/plane/data/db.bak mv /opt/plane/data/minio/uploads /opt/plane/data/minio/uploads.bak ``` 3. Restore PostgreSQL: ```bash mv /tmp/ce-backups/db /opt/plane/data/db ``` 4. Restore MinIO uploads: ```bash mv /tmp/ce-backups/minio/uploads /opt/plane/data/minio/uploads ``` 5. Start Plane: ```bash prime-cli restart ``` ### Validate the migration * Login works * Projects are visible * Attachments open correctly ### Rollback If something fails, restore from the backup you created in step 2: ```bash prime-cli stop rm -rf /opt/plane/data/db mv /opt/plane/data/db.bak /opt/plane/data/db rm -rf /opt/plane/data/minio/uploads mv /opt/plane/data/minio/uploads.bak /opt/plane/data/minio/uploads prime-cli restart ``` ::: ## What's next * [Activate a paid plan license](/self-hosting/manage/manage-licenses/activate-pro-and-business). --- --- url: 'https://developers.plane.so/self-hosting/manage/community-to-airgapped.html' description: >- Deploy Plane in airgapped environment without internet access. Complete guide for offline Plane installation. --- # Upgrade from Community to Airgapped Edition This guide walks you through migrating your existing Plane Community Edition data to an air-gapped environment. You'll backup your current installation, transfer the data, and restore it in your air-gapped setup. ::: warning **Important**\ Make sure you already have Commercial Airgapped Edition installed on a fresh machine before starting this migration. If you haven't installed it yet, follow our [airgapped installation guide](/self-hosting/methods/airgapped-edition) first. ::: ## Prerequisites * Install the [Commercial Airgapped Edition](/self-hosting/methods/airgapped-edition) on a fresh machine, not the one running the Community Edition. * Be sure to log in as the root user or as a user with sudo access. The `/opt` folder requires sudo or root privileges. ## Backup data on Community instance 1. Download the latest version of `setup.sh`. ```bash curl -fsSL https://github.com/makeplane/plane/releases/latest/download/setup.sh -o setup.sh ``` 2. Run the setup.sh backup script to take the backup of the Community Edition instance. ```bash ./setup.sh backup ``` This will create a backup of the plane community instance in the `backup/` folder with the timestamp as the folder name. ```bash backup/ └── 20250605-0938 ├── pgdata.tar.gz ├── rabbitmq_data.tar.gz ├── redisdata.tar.gz └── uploads.tar.gz ``` ## Restore data on Airgapped instance 1. Download the latest version of `restore-airgapped.sh` ```bash curl -fsSL https://github.com/makeplane/plane/releases/latest/download/restore-airgapped.sh -o restore-airgapped.sh chmod +x restore-airgapped.sh ``` This allows you to restore the Community Edition data to the Commercial Airgapped instance. 2. Copy the `restore-airgapped.sh` script into your backup folder. 3. Move your entire backup folder to the server running the Commercial Airgapped Edition. 4. Open terminal, and execute the following command: ```bash sudo bash restore-airgapped.sh ./20250605-0938 ``` This will prompt you to enter the Commercial Airgapped Edition installation folder using whatever secure method works in your environment. 5. After the data restore is finished, start the instance. ```bash cd sudo docker compose -f docker-compose.yml --env-file plane.env up -d ``` You can now access the Commercial Airgapped instance at `http://` Once your migration is complete, verify that all your projects, issues, and team data have been successfully transferred to your air-gapped environment. --- --- url: 'https://developers.plane.so/self-hosting/manage/view-logs.html' description: >- View and debug container logs for self-hosted Plane. Monitor Docker and Kubernetes service logs to troubleshoot issues. --- # View container logs If you need to check the logs for troubleshooting or to monitor what’s happening in specific Plane services like the API or Worker, you can access them directly from the command line. To view logs, start by running the command ↓: ```bash sudo prime-cli monitor ``` This brings up a table where you can select which container logs you want to view. ![Container logs](/images/view-logs/container-logs.webp#hero) ::: warning **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. ::: ::: details Community Edition Here’s how to view logs for any service in Plane Community Edition, whether it’s the API, Worker, Redis, or others. This can be really helpful when troubleshooting or just getting insights into how each service is running. 1. Start by running the setup script: ```bash ./setup.sh ``` This will bring up the main menu with options. Select `6` to view logs. ``` Select a Action you want to perform: 1) Install (x86_64) 2) Start 3) Stop 4) Restart 5) Upgrade 6) View Logs 7) Backup Data 8) Exit Action [2]: 6 ``` 2. After choosing `6`, you’ll see a sub-menu listing all available services: ``` Select a Service you want to view the logs for: 1) Web 2) Space 3) API 4) Worker 5) Beat-Worker 6) Migrator 7) Proxy 8) Redis 9) Postgres 10) Minio 0) Back to Main Menu Service: ``` 3. Pick the service whose logs you’d like to check. For example, if you want to view the **API logs**, type `3`. After selecting a service, you’ll see the logs in real-time. Here’s an example of what API logs might look like: ``` api-1 | Waiting for database... api-1 | Database available! api-1 | Waiting for database migrations to complete... api-1 | Waiting for database migrations to complete... api-1 | Waiting for database migrations to complete... api-1 | Waiting for database migrations to complete... api-1 | Waiting for database migrations to complete... api-1 | Waiting for database migrations to complete... api-1 | Waiting for database migrations to complete... api-1 | No migrations Pending. Starting processes ... api-1 | Instance registered api-1 | ENABLE_SIGNUP loaded with value from environment variable. api-1 | ENABLE_EMAIL_PASSWORD loaded with value from environment variable. api-1 | ENABLE_MAGIC_LINK_LOGIN loaded with value from environment variable. api-1 | GOOGLE_CLIENT_ID loaded with value from environment variable. api-1 | GITHUB_CLIENT_ID loaded with value from environment variable. api-1 | GITHUB_CLIENT_SECRET loaded with value from environment variable. api-1 | EMAIL_HOST loaded with value from environment variable. api-1 | EMAIL_HOST_USER loaded with value from environment variable. api-1 | EMAIL_HOST_PASSWORD loaded with value from environment variable. api-1 | EMAIL_PORT loaded with value from environment variable. api-1 | EMAIL_FROM loaded with value from environment variable. api-1 | EMAIL_USE_TLS loaded with value from environment variable. api-1 | EMAIL_USE_SSL loaded with value from environment variable. api-1 | OPENAI_API_KEY loaded with value from environment variable. api-1 | GPT_ENGINE loaded with value from environment variable. api-1 | UNSPLASH_ACCESS_KEY loaded with value from environment variable. api-1 | Checking bucket... api-1 | Bucket 'uploads' does not exist. Creating bucket... api-1 | Bucket 'uploads' created successfully. api-1 | Public read access policy set for bucket 'uploads'. api-1 | Cache Cleared api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Starting gunicorn 21.2.0 api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker api-1 | [2024-05-02 03:56:01 +0000] [25] [INFO] Booting worker with pid: 25 api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Started server process [25] api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Waiting for application startup. api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] ASGI 'lifespan' protocol appears unsupported. api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Application startup complete. ``` 4. To exit the logs, use `CTRL+C`. This will take you back to the main menu where you can select another action or view logs from a different service. ::: --- --- url: 'https://developers.plane.so/self-hosting/manage/health-checks.html' description: >- Liveness, readiness, and health endpoints for self-hosted Plane services, and how to wire them into uptime monitors, load balancers, and Kubernetes or Docker Compose probes. --- # Health checks Self-hosted Plane services expose HTTP health endpoints so you can wire up uptime monitoring, load-balancer health checks, and container or orchestrator probes. This page documents every endpoint that ships with Plane, exactly what each one checks, and how to consume them from Kubernetes, Docker Compose, and external monitors. ::: info Edition availability The dedicated liveness, readiness, and detailed health probes documented here — `/api/live/`, `/api/ready/`, `/api/health/`, and the per-service endpoints — ship with **Commercial Edition** (Pro, Business, and Enterprise) deployments. **Community Edition** exposes only the basic root health check at `/` that returns `{ "status": "OK" }` (see [A note on the root endpoint](#a-note-on-the-root-endpoint)). ::: ## Liveness vs. readiness vs. health Plane follows the standard three-tier probe model. Knowing which one to point a given tool at matters: * **Liveness** — "is the process up?" A liveness probe answers a single question: is the service running and able to respond to HTTP at all. It performs no dependency checks. If a liveness probe fails, your orchestrator should **restart** the container. Liveness endpoints in Plane never return a failure body — if the process is alive, you get `200`; if it isn't, the request simply fails to connect. * **Readiness** — "should this instance receive traffic right now?" A readiness probe verifies that the service's critical dependencies (database, cache, Redis) are reachable. If a readiness probe fails, your orchestrator or load balancer should **stop routing traffic** to that instance until it recovers — but it should not restart it, since the process itself is fine. Readiness endpoints return `200` when ready and `503` when a dependency is unavailable. * **Health / detailed** — "what is the current state of this instance?" A detailed health endpoint returns the same up/down signal as readiness but with structured diagnostic detail (timestamps, uptime, per-dependency connection status). Use this for dashboards and debugging rather than as the gate for an orchestrator. ::: tip Why the distinction matters for self-hosting If you point a liveness probe at an endpoint that checks the database, a transient database blip will cause your orchestrator to **kill and restart healthy pods**, turning a small dependency hiccup into a restart storm. Always point liveness at the liveness endpoint and readiness at the readiness endpoint. ::: ## Primary API probes The Plane Django API is the main REST API and web application server. Its probes are the ones most operators need, and they are reachable externally through the reverse proxy under `/api/`. All three are unauthenticated `GET` requests, and all Django routes use a **trailing slash**. | Endpoint | Type | Checks | Healthy | Unhealthy | | -------------- | ----------------- | ------------------------------------- | ------- | ------------------ | | `/api/live/` | Liveness | Process running | `200` | (connection fails) | | `/api/ready/` | Readiness | Database + cache connectivity | `200` | `503` | | `/api/health/` | Health (detailed) | Database + cache connectivity, uptime | `200` | `503` | ### `/api/live/` Returns `200` as long as the process can serve HTTP. No dependency checks are performed. ```bash curl -i https://your-plane-domain.com/api/live/ ``` ```json { "alive": true } ``` ### `/api/ready/` Returns `200` when the database and cache are both reachable, and `503` if either is down. The failure response includes a sanitized `reason` field. ```bash curl -i https://your-plane-domain.com/api/ready/ ``` Healthy (`200`): ```json { "ready": true } ``` Not ready (`503`): ```json { "ready": false, "reason": "" } ``` The database check runs a `SELECT 1` query; the cache check sets and verifies a probe key. Failure reasons are **sanitized** in the response (the full exception is logged server-side only) so probes do not leak internal details. ### `/api/health/` Returns a detailed status payload. Use this for dashboards and diagnostics. It performs the same database and cache checks as readiness, but reports each dependency individually along with a timestamp and uptime. ```bash curl -i https://your-plane-domain.com/api/health/ ``` Healthy (`200`): ```json { "status": "ok", "timestamp": 1717346400000, "uptime": 12345, "database": { "connected": true }, "cache": { "connected": true } } ``` Degraded (`503`): ```json { "status": "degraded", "timestamp": 1717346400000, "uptime": 12345, "database": { "connected": false }, "cache": { "connected": false } } ``` ::: info 5-second result caching The `/api/ready/` and `/api/health/` endpoints cache their dependency-check results for **5 seconds per process** (`_RESULT_TTL_SECONDS=5`). This caps real database and cache hits at one per worker process per 5-second window, even under a flood of probe requests, so aggressive monitoring will not thrash your database or cache. The same 5-second per-worker caching applies to the Plane AI (pi) readiness check. ::: ### A note on the root endpoint The Django API also serves a minimal health indicator at the root path: | Endpoint | Type | Healthy body | | ------------- | ---------------- | ------------------------------- | | `/` | Health (minimal) | `{ "status": "OK" }` | | `/robots.txt` | Other | `User-agent: *` / `Disallow: /` | The root `/` and `/robots.txt` endpoints are mounted at the Django root level, while the detailed probes (`/api/live/`, `/api/ready/`, `/api/health/`) are mounted under `/api/`. This minimal root check is the only health endpoint present in **Community Edition** — the `/api/*` probes and the per-service endpoints below are Commercial Edition only. ::: warning Root path routing through the proxy In the reverse proxy (Caddyfile), `/` is a catch-all that routes to the web frontend (`web:3000`), not to the Django API. A request to `/` through the proxy will hit the frontend rather than the API's root health indicator. **Use the `/api/` probes for monitoring** — they route to the API service explicitly. See [Reverse proxy](/self-hosting/govern/reverse-proxy) for routing details. ::: ## All services and probe endpoints The table below lists every health-related endpoint across all Plane services. The **External path** column shows how the endpoint is reached through the reverse proxy; endpoints marked **internal only** are not exposed through the proxy and must not be made publicly reachable. | Service | Type | External path | Internal route | Checks | Success | Reachable | | ------------- | ------------------ | ------------------------- | -------------------- | ----------------------------- | -------------------- | ----------------------- | | API (Django) | Liveness | `/api/live/` | `/api/live/` | process running | `200` | External | | API (Django) | Readiness | `/api/ready/` | `/api/ready/` | database, cache | `200` (`503` fail) | External | | API (Django) | Health | `/api/health/` | `/api/health/` | database, cache, uptime | `200` (`503` fail) | External | | API (Django) | Health (min) | `/` | `/` | none | `200` | Via proxy hits frontend | | Plane AI (pi) | Liveness | `/pi/live/` | `/live/` | process running | `200` | External | | Plane AI (pi) | Readiness | `/pi/ready/` | `/ready/` | database | `200` (`503` fail) | External | | Plane AI (pi) | Health | `/pi/health/` | `/health/` | database, uptime | `200` (`503` fail) | External | | Plane AI (pi) | Other (deprecated) | `/pi/api/v1/health/` | `/api/v1/health/` | none | `200` | External | | Plane AI (pi) | Other (deprecated) | `/pi/api/v2/health/` | `/api/v2/health/` | none | `200` | External | | live | Health | `/live/health/` | `/health/` | process running, server agent | `200` | External | | live | Other (metrics) | `/live/health/memory` | `/health/memory` | memory, agent metrics | `200` (`401` no key) | External (secret-key) | | silo | Liveness | `/silo/health/` | `/health/` | process running | `201` (`500` fail) | External | | silo | Other | `/silo/health/check-hmac` | `/health/check-hmac` | API connectivity | `201` (`500` fail) | External | | silo | Readiness | `/silo/health/check-db` | `/health/check-db` | database | `200` (`500` fail) | External | | flux | Health | internal only | `/health` | redis, process running | `200` (`503` fail) | Internal | | flux | Readiness | internal only | `/ready` | redis | `200` (`503` fail) | Internal | | flux | Liveness | internal only | `/live` | process running | `200` | Internal | | node-runner | Health | internal only | `/health` | process running | `200` | Internal | | monitor | (prober) | internal only | — | watches other services | — | Internal | ::: warning Internal-only endpoints The **flux**, **node-runner**, and **monitor** services are not exposed through the reverse proxy. Their endpoints are reachable only on the internal Docker/Kubernetes network. Do not expose them publicly. The `live` memory-metrics endpoint (`/live/health/memory`) is reachable through the proxy but is secret-key protected — keep it that way. ::: ## Per-service details ### live (real-time collaboration) The `live` service is the real-time collaboration server for document editing (WebSocket via Hocuspocus), running on internal port `3000` and exposed under the `/live` proxy prefix. **Public health endpoint** — unauthenticated, suitable for probes: ```bash curl -i https://your-plane-domain.com/live/health/ ``` ```json { "status": "OK", "timestamp": "2026-06-02T00:00:00.000Z", "version": "1.0.0" } ``` This endpoint always returns `200` when the process is up; it has no failure state. Point readiness/liveness probes here. **Memory metrics endpoint** — for internal monitoring only, protected by a secret key: ```bash curl -i \ -H "live-server-secret-key: $LIVE_SERVER_SECRET_KEY" \ https://your-plane-domain.com/live/health/memory ``` ```json { "status": "ok", "timestamp": "2026-06-02T00:00:00.000Z", "memory": { "heapUsed": "120MB", "heapTotal": "256MB", "heapPercent": "47%", "rss": "300MB", "external": "5MB", "arrayBuffers": "2MB" }, "serverAgent": { "connections": 42, "recentActivity": [] }, "hocuspocus": {}, "uptime": "3600s" } ``` The `live-server-secret-key` header is checked against the `LIVE_SERVER_SECRET_KEY` environment variable; a missing or invalid key returns `401 Unauthorized`. Memory values are human-readable strings (e.g. `"120MB"`), `serverAgent.recentActivity` is truncated to the last 10 connections, and `uptime` is a string in seconds. ::: info Status-string inconsistency The `/live/health/` endpoint returns `"status": "OK"` (uppercase) while `/live/health/memory` returns `"status": "ok"` (lowercase). If you parse these programmatically, account for the case difference. ::: ### silo (integrations engine) The `silo` service is the integrations engine (ETL, OAuth, webhooks, and sync for GitHub, Jira, Linear, Asana, Slack, etc.). It runs on internal port `3000` and is exposed under the `/silo` proxy prefix. The base path is configurable via the `SILO_BASE_PATH` environment variable (default `/silo`). ```bash # Liveness — process running curl -i https://your-plane-domain.com/silo/health/ # API connectivity check (despite the name, this validates API connectivity, not HMAC) curl -i https://your-plane-domain.com/silo/health/check-hmac # Readiness — database (SELECT 1) curl -i https://your-plane-domain.com/silo/health/check-db ``` Success bodies: ```json { "message": "Welcome to Silo health check" } ``` ```json { "message": "Welcome to Silo API health check" } ``` ```json { "message": "Silo DB is up and running" } ``` A database failure on `check-db` returns `500`: ```json { "status": 500, "message": "Internal Server Error", "errors": { "message": "Database is not running" } } ``` ::: warning Non-standard status codes Silo's status codes are inconsistent with the other services. `/silo/health/` and `/silo/health/check-hmac` return **`201`** on success (not `200`), while `/silo/health/check-db` returns `200`. All three return `500` on failure. If you configure an external monitor that expects `2xx`, this works; if you expect exactly `200`, treat `201` as healthy. The `check-hmac` name is historical — it actually validates API connectivity to the Plane API, not HMAC. ::: ### Plane AI (pi) The Plane AI service (`pi`) is a FastAPI service for AI features (chat, embeddings, transcription, LLM proxy). It runs on internal port `8000` and is exposed under the `/pi` proxy prefix. Health routes are mounted at the app root; the prefix is controlled by `PI_BASE_PATH` (default empty), and the proxy adds the `/pi` prefix without stripping it. ```bash # Liveness — process running curl -i https://your-plane-domain.com/pi/live/ # Readiness — database connectivity curl -i https://your-plane-domain.com/pi/ready/ # Health (detailed) — database connectivity + uptime curl -i https://your-plane-domain.com/pi/health/ ``` Liveness (`200`): ```json { "alive": true } ``` Readiness — healthy (`200`) / not ready (`503`): ```json { "ready": true } ``` ```json { "ready": false, "reason": "database unavailable" } ``` Health — healthy (`200`) / degraded (`503`): ```json { "status": "ok", "timestamp": 1717346400000, "uptime": 12345, "database": { "connected": true } } ``` ```json { "status": "degraded", "timestamp": 1717346400000, "uptime": 12345, "database": { "connected": false } } ``` The database readiness check caches results for **5 seconds per worker** to avoid connection-pool exhaustion under probe floods. ::: info Deprecated pi endpoints `/pi/api/v1/health/` and `/pi/api/v2/health/` are **deprecated** in favor of the root-level probes above. They return a legacy payload `{ "status": "alive" }` and emit deprecation headers (per RFC 8594). Use `/pi/live/`, `/pi/ready/`, and `/pi/health/` instead. ::: ### flux (real-time event server) The `flux` service is a WebSocket server for real-time collaborative events, backed by Redis. It runs internally (default port `3004`, configurable via `PORT`) and is **internal-only** — it is not exposed through the reverse proxy. Its routes sit under the base path set by `FLUX_BASE_PATH` (default `/flux`), so they are reachable as `/flux/health`, `/flux/ready`, and `/flux/live` on the internal network: ```bash # From within the internal network / cluster (paths are relative to FLUX_BASE_PATH) curl -i http://flux:3004/flux/live # liveness — process running curl -i http://flux:3004/flux/ready # readiness — Redis connectivity curl -i http://flux:3004/flux/health # health (detailed) — Redis + connection counts ``` Liveness (`200`): ```json { "alive": true } ``` Readiness — healthy (`200`) / not ready (`503`): ```json { "ready": true } ``` ```json { "ready": false, "reason": "Redis not connected" } ``` Health — healthy (`200`) / degraded (`503`): ```json { "status": "ok", "timestamp": 1717346400000, "uptime": 12345, "connections": { "total": 5, "channels": 10 }, "redis": { "connected": true } } ``` ```json { "status": "degraded", "timestamp": 1717346400000, "uptime": 12345, "connections": { "total": 5, "channels": 10 }, "redis": { "connected": false } } ``` Liveness always returns `200`; readiness and health both gate on Redis connectivity. Note the degraded status string is `"degraded"`, not `"unhealthy"`. ### node-runner (automation execution) The `node-runner` service executes automation scripts (build, validate, sandboxed execution). It runs on internal port `3000` and is **internal-only** — not reachable through the reverse proxy. Its own health endpoint is reachable only on the internal network: ```bash curl -i http://node-runner:3000/health ``` ```json { "status": "ok" } ``` Runner health is also surfaced through the Django API at `/api/workspaces/{slug}/runnerctl/health/` (a `GET` that requires session authentication and workspace-admin permission). That endpoint checks the node-runner service and returns `{ "is_available": }` with status `200`. Use the Django-exposed endpoint if you need to observe runner health without internal network access. ### monitor (internal prober) The `monitor` service is a Go-based **prober** — it is not a service you probe. It runs internally on port `8080` (not exposed through the reverse proxy) and **does not expose any liveness/readiness/health endpoint of its own**. Instead, it periodically checks the other Plane services and reports their status. What it does: * **Health-check cron job** (default every 5 minutes, configurable via `--health-check-interval`): runs HTTP or TCP probes against the services you define via `SERVICE_*` environment variables. HTTP probes (`HTTP_TEST_METHOD`) issue `GET` requests and treat `200`–`399` as healthy; TCP probes (`TCP_TEST_METHOD`) attempt a raw connection. Each service is probed with `maxRetries=5`, `confirmTries=3`, a 5s timeout, and a 2s retry interval. Results are posted to the monitoring API at `HOST/api/service-status/`. * **Flag/license resync cron job** (default every 300 minutes, configurable via `--resync-flags-interval`): refreshes feature flags and licenses from the monitoring server. Services are declared as environment variables in the form `SERVICE__=hostname:port/path`. Examples: ```bash SERVICE_HTTP_WEB=web:3000 SERVICE_HTTP_API=api:8000 SERVICE_HTTP_LIVE=live:3000/live/health SERVICE_HTTP_PROXY=proxy:80 SERVICE_HTTP_MINIO=plane-minio:9090 SERVICE_TCP_REDIS=plane-redis:6379 SERVICE_TCP_POSTGRES=plane-db:5432 ``` For an unreachable service, monitor posts status code `500`; for a non-`2xx`/`3xx` HTTP response, it posts the actual status code. ## Using these in your infrastructure ### Kubernetes liveness and readiness probes Point `livenessProbe` at the liveness endpoint and `readinessProbe` at the readiness endpoint. The following mirrors the probe configuration Plane's own Helm charts use for the API service: ```yaml # api deployment livenessProbe: httpGet: path: /api/live/ port: 8000 initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /api/ready/ port: 8000 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 ``` The Plane AI (`pi-api`) service uses the same pattern against its own paths: ```yaml # pi-api deployment livenessProbe: httpGet: path: /pi/live/ # or {PI_BASE_PATH}/live/ port: 8000 initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /pi/ready/ # or {PI_BASE_PATH}/ready/ port: 8000 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 ``` The `live` and `silo` services use a readiness-only probe (no liveness) against their `/health` path, with a lenient `failureThreshold` to allow for slow startups: ```yaml # live deployment (silo is identical against {SILO_BASE_PATH}/health) readinessProbe: httpGet: path: /live/health # {LIVE_BASE_PATH}/health port: 3000 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 30 successThreshold: 1 ``` ::: info Timing implications With `initialDelaySeconds: 30` plus the start period, the API and pi-api pods take roughly 90 seconds before they are considered ready — this is intentional, giving migrations and warm-up time to complete. The `live`/`silo` readiness probe with `failureThreshold: 30` at a 10s period tolerates up to ~305 seconds of startup before marking the pod unhealthy. ::: ### Docker Compose healthcheck Plane's Compose deployments probe the API by calling its readiness endpoint from inside the container with Python (no extra tooling needed): ```yaml # api service healthcheck: test: [ "CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/ready/', timeout=5)", ] interval: 30s timeout: 10s retries: 5 start_period: 60s ``` The `pi-api` service follows the same approach, honoring its configurable base path: ```yaml # pi-api service healthcheck: test: [ "CMD", "python", "-c", 'import os,urllib.request; urllib.request.urlopen(f"http://localhost:8000{os.environ.get(''PI_BASE_PATH'','''')}/ready/", timeout=5)', ] interval: 30s timeout: 10s retries: 5 start_period: 60s ``` Stateful dependencies are probed with their native CLIs rather than HTTP: ```yaml # plane-db (PostgreSQL) healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 start_period: 10s # plane-mq (RabbitMQ) healthcheck: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] interval: 10s timeout: 10s retries: 5 ``` Other services then declare `depends_on` with `condition: service_healthy` so they start only after their dependencies are healthy. ### External uptime monitors and load balancers For an external uptime monitor (UptimeRobot, Better Stack, Pingdom, etc.) or a load balancer health check, target the reverse-proxy URL of the **readiness** endpoint over HTTPS: ```bash # Load balancer / uptime monitor target GET https://your-plane-domain.com/api/ready/ # Healthy: HTTP 200 Unhealthy: HTTP 503 ``` Guidelines: * Use **`/api/ready/`** as the load-balancer health check so traffic is only routed to instances whose database and cache are reachable. Treat `200` as healthy and `503` as unhealthy. * Use **`/api/health/`** for richer dashboards where you want to display per-dependency status, timestamps, and uptime. * All API probes are **unauthenticated**, so no credentials are needed. The 5-second per-worker result cache means a frequent polling interval (e.g. every 15–30 seconds) will not stress your database or cache. * Mind the **trailing slash** — Django routes require it (`/api/ready/`, not `/api/ready`). * Do not point external monitors at internal-only services (`flux`, `node-runner`, `monitor`); they are not reachable through the proxy by design. ## Troubleshooting * **`503` from `/api/ready/` or `/api/health/`** — the database or cache is unreachable. Check that PostgreSQL and Redis/Valkey are running and reachable from the API container. The `reason` field on `/api/ready/` and the per-dependency `connected` flags on `/api/health/` tell you which dependency failed. Remember the full exception is logged server-side (the probe response is sanitized), so check the API logs for details. * **`503` from `/pi/ready/`** — the Plane AI service cannot reach its database (`"reason": "database unavailable"`). Verify database connectivity from the `pi` container. * **`503` from flux `/ready` or `/health` (`"status": "degraded"`)** — Redis is not connected (`"reason": "Redis not connected"`). Check Redis availability on the internal network. * **`401` from `/live/health/memory`** — the `live-server-secret-key` header is missing or does not match `LIVE_SERVER_SECRET_KEY`. Confirm the env var is set and the header value matches. * **`500` from `/silo/health/check-db`** — Silo cannot reach its database (`"message": "Database is not running"`). Note that silo's other health endpoints return `201` on success, not `200`. * **A pod restart loop in Kubernetes** — confirm the `livenessProbe` points at a *liveness* path (`/api/live/`, `/pi/live/`), not a readiness or health path. A liveness probe that checks the database will restart healthy pods during a transient dependency outage. * **Probes failing right after deploy** — the API and pi-api need ~90 seconds before they report ready (30s initial delay plus start period). Make sure your probe `initialDelaySeconds` and `start_period` allow for startup and migrations before marking the service down. --- --- url: 'https://developers.plane.so/self-hosting/manage/migrate-plane.html' description: >- Migrate Plane between servers or environments. Guide for moving your Plane installation to new infrastructure. --- # Move Plane instance to a new server Switching to another machine is straightforward on the Commercial Edition. ## Prerequisites Before we dive in, ensure: * You’re running Plane's Commercial Edition. * You have a different machine with our standard config to migrate to. * You understand the same domain will be used to host the app as the current machine. ::: warning If you need to change your domain during migration, contact our support team for assistance. ::: ## Steps 1. **Delink licenses**\ Log in to Plane on your current server. Head to each paid workspace like Pro or Business and [delink the licenses](/self-hosting/manage/manage-licenses/activate-pro-and-business#delink-license-key). This will free up the licenses for activation on your new server. Ideally, you have just one paid workspace. 2. **Backup data**\ Create a backup of your Plane instance with ↓: ```bash prime-cli backup ``` This command will generate a backup file in the path: `/opt/plane/backups`. ::: warning **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. ::: 3. **Set up Plane on the new server**\ Follow the [installation guide](/self-hosting/methods/docker-compose#install-plane) to deploy Plane on the new instance. 4. **Transfer backup files**\ Copy the `backups` folder from the old server, created in step 2, to the new server. Place the backup in the folder `/opt/plane`. 5. **Restore data**\ On the new server, restore your data with ↓: ```bash prime-cli restore ``` Follow the prompts during the restore process to make sure everything is set up correctly. 6. **Reactivate license**\ Finally, [reactivate your license keys](/self-hosting/manage/manage-licenses/activate-pro-and-business#activate-your-license) on the new instance. This should get your Plane instance up and running on the new server. --- --- url: >- https://developers.plane.so/self-hosting/manage/migration/migrate-data-to-external-services.html description: >- Move your Plane data from the default local Postgres and MinIO containers to managed cloud services for a production ready deployment. --- # Migrate to external Postgres and storage The default Plane installation includes a local PostgreSQL container and a local MinIO container for storage. These are convenient for getting started, but not recommended for production. For production deployments, use a managed database service and an S3-compatible object store. This guide walks through moving your existing data from the local containers to your cloud-managed services. Follow these steps during a maintenance window as the migration requires Plane to be offline. ## Before you begin You need: * Docker and Docker Compose installed on the host running Plane * The PostgreSQL client `psql` and `pg_restore` installed on your local machine * Your cloud Postgres connection details: host, username, database name, and password * Your cloud storage connection details: endpoint URL, access key, secret key, and bucket name * Your cloud PostgreSQL version should ideally match your source PostgreSQL version ## Stop Plane Take Plane offline before starting. This prevents new writes during the migration. ```bash docker compose down ``` Do not stop the database and MinIO containers yet, you still need them running to export data. Start only the database and MinIO: ```bash docker compose up -d plane-db plane-minio ``` ## Export the database Run `pg_dump` inside the running database container. This creates a compressed binary dump file in your current directory. ```bash docker exec \ -e PGPASSWORD=plane \ -e PGUSER=plane \ -e PGDATABASE=plane \ plane-app-plane-db-1 \ pg_dump -Fc > plane-backup.dump ``` Verify the file was created and is not empty: ```bash ls -lh plane-backup.dump ``` ## Restore the database to your cloud Postgres Before restoring, clear the existing schema in your cloud database. ```bash psql \ -h "your-cloud-host.com" \ -U "cloud-username" \ -d "cloud-database-name" \ -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" ``` Restore the dump using `pg_restore`. The `--no-owner` flag prevents ownership errors when the source database roles do not exist in your cloud database. The `--no-privileges` flag skips grants for roles that may not exist in the target database. ```bash pg_restore \ --no-owner \ --no-privileges \ --verbose \ -h "your-cloud-host.com" \ -U "cloud-username" \ -d "cloud-database-name" \ plane-backup.dump ``` You will be prompted for your cloud database password. ### PostgreSQL version compatibility Use a `pg_restore` version that matches your target PostgreSQL version when possible. For example, if your cloud database is PostgreSQL 15, use PostgreSQL 15 client tools. If you use a newer `pg_restore` version, you may see: ```text ERROR: unrecognized configuration parameter "transaction_timeout" Command was: SET transaction_timeout = 0; ``` This warning can be ignored if the restore continues successfully. Using matching PostgreSQL client tools avoids this warning. ## Sync storage to your cloud bucket MinIO ships with the `mc` client. Use it from inside the MinIO container to sync your local uploads to your cloud storage. ### 1. Create an alias for your local MinIO Your MinIO credentials are in `plane.env` as `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD`. ```bash docker compose --env-file plane.env exec plane-minio \ mc alias set localminio http://localhost:9000 \ ``` ### 2. Create an alias for your cloud storage For AWS S3, use the S3 endpoint. Default AWS endpoint: ```bash docker compose --env-file plane.env exec plane-minio \ mc alias set cloudminio https://s3.amazonaws.com \ ``` Regional AWS S3 endpoint: ```bash docker compose --env-file plane.env exec plane-minio \ mc alias set cloudminio https://s3..amazonaws.com \ ``` For other S3-compatible providers, replace the endpoint with your provider's S3 endpoint. Verify the connection: ```bash docker compose --env-file plane.env exec plane-minio \ mc ls cloudminio ``` ### 3. Mirror local data to your cloud bucket ```bash docker compose --env-file plane.env exec plane-minio \ mc mirror localminio/uploads cloudminio/ --overwrite ``` Example: ```bash docker compose --env-file plane.env exec plane-minio \ mc mirror localminio/uploads cloudminio/my-plane-bucket --overwrite ``` This copies all files from the local uploads bucket to your cloud bucket. The `--overwrite` flag replaces existing files with the same name. Wait for the sync to complete before continuing. ## Update your environment configuration Open `plane.env` and update the database and storage settings to point to your cloud services. ### Database ```bash DATABASE_URL=postgresql://cloud-username:cloud-password@your-cloud-host.com:5432/cloud-database-name ``` ### Storage For AWS S3: ```bash AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_S3_ENDPOINT_URL=https://s3.amazonaws.com AWS_S3_BUCKET_NAME= AWS_REGION= ``` For other S3-compatible providers, replace `AWS_S3_ENDPOINT_URL` with your provider endpoint. ## Restart Plane Bring all services back up: ```bash docker compose up -d ``` Open Plane in a browser and verify that your data, attachments, and pages are intact. ## After migration Keep `plane-backup.dump` in a safe location as a point-in-time backup. --- --- url: 'https://developers.plane.so/self-hosting/manage/prime-cli.html' description: >- Use Plane Prime CLI for managing your self-hosted instance. Commands for setup, configuration, upgrades, and troubleshooting from the terminal. --- # Command line tools Our command-line tool is here to make managing your Plane instance simple. You can handle installs, upgrades, and general management without needing to be a Docker expert. ## Prime CLI The Prime CLI provides commands for common tasks like configuring services, monitoring health, managing backups, and upgrading your Plane instance. ::: warning **Prime CLI is for Docker installations only.** These commands only work on Plane instances originally installed using `prime-cli`. ::: Bring up the Prime CLI with `sudo prime-cli` from any directory on your machine. * The three operators you will use the most are: * `start` You will use this to start a service in the Docker network with the name of the service. * `stop` You will use this to stop a service in the Docker network with the name of the service. * `restart` You will use this to restart a service in the Docker network with the name of the service as a `{param or flag}`. * Often, you will want to monitor the health of your instance and see if some services are up or down. Use `monitor` to do that. * `healthcheck` is another useful utility that lets you see the status and errors, if any, of all running services * `repair` automatically diagnoses and fixes common errors in your Plane instance. This command also resets all configuration values in the plane.env file to their defaults. * `update-cli` downloads and installs the latest version of Prime CLI. ::: tip It is highly recommend to run this first before you download any Plane updates. The latest version of the CLI ensures your Plane upgrades happen smoothly. ::: For more advanced admins that want greater control over their instance, the list of additional commands available on Prime CLI follow. * `configure` Brings up a step form to let you specify the following. ::: details Steps to configure your instance * `Listening port`\ Specify the port that the built-in reverse proxy will use Default value: `80` * `Max file-upload size`\ Specify a size in MBs for how big each file uploaded to your Plane app can be Default value: `5 MB` * `External Postgres URL` Specify the URL of your own hosted Postgres if you would like to change the database your Plane app uses. Default database: Postgres 15.5 in the Docker container * `External Redis URL` Specify the URL of your own hosted REdis if you would like to change the default Redis Plane ships with. Default Redis: Redis 7.2.4 * `External storage` Specify your AWS S3 bucket's credentials in the format below to change storage from the default Plane ships with. * AWS Access Key ID * AWS Secret Access Key * AWS S3 Bucket Name Default storage: MinIO * Confirm your choices on the screen ↓. This restarts your instance with the new configs. ::: * `upgrade` checks your instance for available version upgrades and asks you for a confirmation before downloading the latest available version. 1. Typing `YES` lets the CLI automatically download's the latest version and installs it. Then it restarts the instance to load the latest app. 2. Typing `NO` cancels the upgrade. * `uninstall` uninstalls Plane. Before it goes through, it asks you for a confirmation. 1. Typing `YES` lets the CLI clean up the `/opt/plane` folder, leaving behind the `/opt/plane/data` and `/opt/plane/logs` folders. 2. Typing `NO` cancels the uninstall. ::: details Setup.sh script • Community Edition The setup script `setup.sh` provides a menu-driven interface to help you install and manage your Plane instance. #### Usage To run the setup.sh script, use the following command in your terminal from the directory where the script is located: ```bash ./setup.sh ``` This will launch an interactive menu with options to manage various aspects of your Plane instance. ```bash Select a Action you want to perform: 1) Install 2) Start 3) Stop 4) Restart 5) Upgrade 6) View Logs 7) Backup Data 8) Exit ``` #### Actions * **Install**\ Installs the Plane Community Edition on your machine. Choose this option if you are setting up Plane for the first time. * **Start**\ Starts the Plane server and all related services. * **Stop**\ Stops the Plane server and all services currently running on the machine. * **Restart**\ Restarts the Plane server and all associated services. * **Upgrade** Upgrades Plane to the latest available version. This will stop all services, update the necessary files, and then restart Plane with the latest configuration. See [Update Plane](/self-hosting/manage/upgrade-plane#prerequisites) for more info. > \[!WARNING] > It's recommended to create a backup before upgrading your instance. See [Backup and restore](/self-hosting/manage/backup-restore#backup-data). * **View Logs** Displays real-time logs of specific Plane services. See [View logs](/self-hosting/manage/view-logs) for more info. > \[!TIP] > Use **View Logs** to monitor service performance or troubleshoot issues. Press `CTRL+C` to exit the log view and return to the main menu. * **Backup Data** Creates a backup of your current Plane installation, including all data. See [Backup and restore data](/self-hosting/manage/backup-restore#backup-data) for more info. * **Exit**\ Closes the setup script and returns you to the command line. ::: ## Troubleshoot * [Failed to update Prime CLI](/self-hosting/troubleshoot/cli-errors#failed-to-update-prime-cli) --- --- url: 'https://developers.plane.so/self-hosting/manage/manage-instance-users.html' description: 'Manage instance users, invite instance admins, and control access to God Mode.' --- # Manage instance users User management lets instance admins view all users across the instance and manage their access. This is separate from workspace-level member management. ## View users Go to **God Mode → User Management** to see all users in the instance. ![User management](/images/instance-admin/user-management.webp#hero) The table displays: | Column | Description | | ------------ | ---------------------- | | Full Name | User's full name | | Display Name | User's display name | | Email | User's email address | | Account Type | User or Instance Admin | | Status | Active or Suspended | | Joining Date | When the user joined | Use the search bar to find specific users. ## Invite an instance admin Instance admins have access to God Mode but are not automatically added to any workspace. 1. Click **Invite members**. 2. Enter the user's email and password. 3. Optionally enable: * **Generate random password** — auto-create a password * **Prompt user to change password after onboarding** — require password reset on first login 4. Click **Invite**. ![Invite instance admin](/images/instance-admin/invite-instance-admin.webp#hero) :::warning No invitation email is sent. You must share the credentials with the user manually. ::: ## Manage user access Click **…** next to any user to: * **Grant admin access** — promote a user to Instance Admin. * **Remove admin access** — downgrade an Instance Admin to a regular user (loses God Mode access). * **Remove** — remove the user from the instance entirely. ![User actions](/images/instance-admin/user-actions.webp#hero) ## User status | Status | Description | | --------- | -------------------------------------------------- | | Active | User can access the instance | | Suspended | User account exists but cannot access the instance | --- --- url: 'https://developers.plane.so/self-hosting/troubleshoot/overview.html' description: Diagnose and resolve issues with your self-hosted Plane instance. --- # Troubleshooting When something goes wrong, start by identifying which service is affected, then check the relevant logs. ## Identify the service | Problem area | Service | Logs to check | | ------------------------------------------------------------ | ---------- | ------------------ | | API errors, data not saving, 500 errors | api | `plane-api` | | License activation or validation errors | monitor | `plane-monitor` | | GitHub, GitLab, Slack integrations, imports not working | silo | `plane-silo` | | SSL errors, 502/504 errors, routing issues | proxy | `plane-proxy` | | File uploads or attachments failing | minio | `plane-minio` | | Plane AI not working, AI chat errors | pi | `plane-pi` | | UI not loading, blank screens, page errors | web | `plane-web` | | Public pages or published views not working | space | `plane-space` | | Instance settings | admin | `plane-admin` | | Imports stuck, notifications delayed, file processing issues | worker | `plane-worker` | | Scheduled tasks or reminders not running | beat | `plane-beat` | | Upgrade failures, database schema errors | migrator | `plane-migrator` | | Real-time sync, live cursors, or presence not working | live | `plane-live` | | Intake Email not working | intake | `plane-intake` | | Search not returning results | opensearch | `plane-opensearch` | See [View logs](/self-hosting/manage/view-logs) for commands to access logs in Docker deployments. ## Reporting issues to support When [contacting support](https://docs.plane.so/support/get-help), include: * **Container logs** for the affected service (see table above) * **Browser Network logs** (open DevTools → Network tab → reproduce the issue → export as HAR file) This helps us diagnose the problem faster. ## Common issues * [Installation errors](/self-hosting/troubleshoot/installation-errors) * [License errors](/self-hosting/troubleshoot/license-errors) * [CLI errors](/self-hosting/troubleshoot/cli-errors) * [Storage errors](/self-hosting/troubleshoot/storage-errors) --- --- url: 'https://developers.plane.so/self-hosting/troubleshoot/installation-errors.html' description: >- Troubleshoot installation errors. Common issues, error messages, and solutions for self-hosted Plane. --- # Installation errors This guide is designed to help you resolve common issues encountered while installing Plane. Each section includes potential causes and step-by-step solutions for identified problems. ## Error during Docker Compose execution * This error typically occurs when the user doesn't have sudo or root privileges. To resolve this, ensure you're logged in as the root user or as a user with sudo access before attempting the installation again. * The issue may also be caused by using the older version of Docker Compose `docker-compose`. To fix this, install the latest version of Docker Compose `docker compose` and make sure the old version is removed. ## Migrator container exited This error typically occurs if you have configured an external database that is running on localhost. Since the connection is being attempted from inside the container, localhost won’t work, as the database is not running within the container. To resolve this issue, ensure that the database is hosted on a network-accessible server rather than localhost. Update the database URL to reflect the correct server address. See [how to configure external db](/self-hosting/govern/database-and-storage). --- --- url: 'https://developers.plane.so/self-hosting/troubleshoot/license-errors.html' description: >- Troubleshoot Plane license activation errors. Fix common issues with Pro, Business, Enterprise, and Airgapped license keys. --- # Errors related to licenses This guide is designed to help you resolve common issues encountered while activating the license key for a workspace. Each section includes potential causes and step-by-step solutions for identified problems. ## Before troubleshooting **Try syncing your plan first.** Many license issues resolve automatically when your workspace pulls the latest subscription information from the Prime server. You can sync your plan from the workspace settings in the Plane application. If the issue persists after syncing or you see other errors, continue with the specific error troubleshooting below. ## License is invalid * This issue usually occurs when your server has trouble connecting to ours to verify the license. Try running `prime-cli restart`, and it should resolve the problem. * If you migrated Plane to a new server without first [delinking the license key](/self-hosting/manage/manage-licenses/activate-pro-and-business#delink-license-key) from the old server, this error might happen. In that case, please reach out to the support team to have the license key delinked. For future reference, if you need to reinstall Plane or move to another server, follow [this guide](/self-hosting/manage/migrate-plane) to ensure a smooth transition. ## Something went wrong This error usually occurs when the license validation service is unavailable. Here's how you can troubleshoot it: 1. Confirm that your Plane instance is running the latest version. 2. If it's not up-to-date, [update to the latest version](/self-hosting/manage/upgrade-plane#prerequisites). Updating typically resolves this issue. If the problem persists, double-check your network connection and any firewall rules that might block access to the license validation service. ## Payment server is not configured This usually occurs when the environment confiuration is incorrect. The Env variable `payment_server_url` is missing in the setup. In this case, follow the below steps. 1. Backup the `plane.env` file. See [Backup plane.env](/self-hosting/manage/backup-restore#backup-plane-env). 2. Run `prime-cli repair` to allow Prime CLI to attempt automatic fixes to the `plane.env` file. 3. Try activating your workspace with the license key. 4. If needed, you can configure the instance in [God mode](/self-hosting/govern/instance-admin#settings) or adjust the environment variables directly in the new plane.env file. --- --- url: 'https://developers.plane.so/self-hosting/troubleshoot/cli-errors.html' description: >- Troubleshoot cli errors. Common issues, error messages, and solutions for self-hosted Plane. --- # CLI errors This page helps you troubleshoot common issues you might run into when using the Plane CLI tools. It covers potential causes of errors and provides straightforward steps to resolve them. ## Failed to update Prime CLI This error typically happens if you're using an older version of the Prime CLI. To fix it, follow these steps: 1. Start by taking a [data backup](/self-hosting/manage/backup-restore#backup-data) to be safe. 2. Run this command to remove the existing CLI: ```bash rm -rf /usr/bin/prime-cli ``` 3. Install the latest version of the CLI with: ```bash curl -fsSL https://prime.plane.so/install/ | sh ``` --- --- url: 'https://developers.plane.so/self-hosting/troubleshoot/storage-errors.html' description: >- Troubleshoot Plane storage and upload errors. Fix S3, MinIO, and file upload issues for self-hosted Plane instances. --- # Storage errors This guide is designed to help you resolve common issues encountered while configuring storage in Plane. Each section includes potential causes and step-by-step solutions for identified problems. ## Bucket policy exceeds size limit This error occurs when the bucket policy exceeds the 20KB size limit allowed by MinIO. It typically happens when trying to add complex policies or when unnecessary data bloats the policy size. To resolve this issue, you can define a streamlined bucket policy file and apply it correctly within the MinIO container. Follow these steps: 1. **Create a bucket policy JSON file** * On your local machine, create a file named `bucket-policy.json` with the following content: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": ["*"] }, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::uploads/*"], "Condition": { "StringEquals": { "s3:ExistingObjectTag/publicAccess": ["true"] } } } ] } ``` * Save this file in an accessible location. 2. **Set up and apply the policy**\ ::: warning **IMPORTANT**\ Make sure to execute all the `mc` commands **within the MinIO container** (either by attaching to it or using `docker exec`). ::: * Configure MinIO alias: ```bash mc alias set myminio http://plane-plane-minio:9000 ``` Replace `plane-plane-minio:9000` with your MinIO server address. * Tag existing objects: ```bash mc find myminio/uploads --exec "mc tag set {} publicAccess=true" ``` * Copy the policy file to the MinIO container: ```bash docker cp bucket-policy.json :/tmp ``` Replace `` with the actual ID of your MinIO container. You can find the container ID by running `docker ps`. * Apply the policy to the bucket: ```bash mc anonymous set-json /tmp/bucket-policy.json myminio/uploads ``` 3. **Verification** * Verify that objects are correctly tagged: ```bash mc tag list myminio/uploads/ ``` * Test public access using: ```bash curl http://:9000/uploads/ ``` ### Notes * Verify that the `access-key` and `secret-key` used for setting up the alias have adequate permissions to manage the bucket. * If your MinIO server is hosted on a different machine or address, replace `plane-plane-minio:9000` with the appropriate server URL. * After applying the policy, test the setup to confirm it is working as expected. --- --- url: 'https://developers.plane.so/api-reference/introduction.html' description: >- Complete REST API reference for Plane. Learn authentication, HTTP methods, pagination, rate limiting, and how to integrate Plane with your applications programmatically. --- # Plane API Documentation The Plane API is organized around REST. Our API has predictable resource-oriented URLs, accepts application/json request bodies, returns JSON responses, and uses standard HTTP response codes, authentication, and verbs. ## Base URL All requests to the Plane Cloud API must be made to the following base URL: ``` https://api.plane.so/ ``` This URL should be prefixed to all endpoint paths. For example, to retrieve all projects in a workspace: ``` GET https://api.plane.so/api/v1/workspaces/{workspace_slug}/projects/ ``` ::: tip If you're using a self-hosted instance of Plane, your API base URL will differ based on your custom domain and setup. ::: ## Authentication Our APIs use a key for authentication. The API key should be included in the header of each request to verify the client's identity and permissions. The key should be passed as the value of the `X-API-Key` header. ::: info You must have a Plane account or be registered to your instance to generate a key. ::: ### Generating an API Key 1. Log into your Plane account and go to **Profile Settings**. 2. Go to **Personal Access Tokens** in the list of tabs available. 3. Click `Add personal access token`. 4. Choose a title and description so you know why you are creating this token and where you will use it. 5. Choose an expiry if you want this to stop working after a point. ### Using the API Key To authenticate an API request, include your API key in the request header: ``` X-API-Key: ``` It is important to keep your API key confidential to prevent unauthorized access to your account. ### Using an OAuth Token If your application uses [OAuth](/dev-tools/build-plane-app/overview) to obtain user authorization (for example, a Plane app you've built), you can authenticate API requests with the OAuth access token. Include the token in the `Authorization` header as a Bearer token: ``` Authorization: Bearer ``` The access token is scoped to the permissions (scopes) the user granted when authorizing your app. See [OAuth scopes](/dev-tools/build-plane-app/oauth-scopes) for the full list of available scopes. ### Example of an Authenticated API Request **Using an API key:** ``` GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items/ Headers: X-API-Key: plane_api_ ``` **Using an OAuth token:** ``` GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items/ Headers: Authorization: Bearer ``` **Response:** ```json { [ ... ] } ``` ### Error Handling * **Missing API Key**: If the `X-API-Key` header is not included, the API will return an error indicating that authentication is required. * **Invalid API Key**: If the provided API key is invalid or expired, the API will return an error message indicating an authentication failure. ### Security Recommendations * **Keep the API Key Secret**: Treat your API key like a password. Do not share it or expose it in client-side code. * **Regenerate Key If Compromised**: If you suspect that your API key has been compromised, generate a new one immediately and update your applications. *** ## HTTP Methods HTTP defines a set of request methods, also known as HTTP verbs, to indicate the desired action for a given resource. | Verb | Description | Example | | ------ | --------------------------------------------------- | ------------------------------- | | GET | Requests a representation of the specified resource | Fetch all issues from a project | | POST | Submits an entity to the specified resource | Create a project | | DELETE | Deletes the specified resource | Delete a module-issue | | PATCH | Applies partial modifications to a resource | Edit a module | ## Status Codes ### Success Responses | Status Code | Description | | -------------- | --------------------------------------------------------------------------------------------------- | | 200 OK | The request succeeded, and a new resource was created, generally sent in GET or PATCH requests. | | 201 Created | The request is succeeded, and a new resource was created, generally sent in POST or PATCH requests. | | 204 No Content | The request is succeeded, and no body is sent, generally comes from the DELETE request. | ### Error Responses | Status Code | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 Bad Request | The server cannot or will not process the request due to something that is perceived to be a client error. | | 401 Unauthorized | Although the HTTP standard specifies "unauthorized", semantically, this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. | | 404 Not Found | The server cannot find the requested resource. This means the URL is not recognized. | | 429 Throttling Error | The server is processing too many requests at once and is unable to process your request. Retry the request after some time. | | 500 Internal Server Error | The server has encountered a situation it does not know how to handle. | | 502 Bad Gateway | This error response means that the server got an invalid response while working as a gateway to get a response needed to handle the request. | | 503 Service Unavailable | The server is not ready to handle the request. Common causes are a server that is down for maintenance or is overloaded. | | 504 Gateway Timeout | This error response is given when the server acts as a gateway and cannot get a timely response. | *** ## Pagination ### Overview This API implements a cursor-based pagination system, allowing clients to efficiently navigate through large datasets. The system uses a cursor parameter to manage the position and direction of pagination. ### Cursor Format The cursor is a string formatted as `value:offset:is_prev`, where: * `value` represents the page size (number of items per page). * `offset` is the current page number (starting from 0). * `is_prev` indicates whether the cursor is moving to the previous page (`1`) or to the next page (`0`). ### Request Parameters * **`per_page` (optional)**: Number of items to display per page. Defaults to 100. The maximum allowed value specified by the server is 100. * **`cursor` (optional)**: Cursor string to navigate to a specific page. If not provided, pagination starts from the first page. ### Response Fields The paginated response includes the following fields: | Field | Description | | ------------------- | -------------------------------------------------------------------- | | `next_cursor` | Cursor string for the next page. | | `prev_cursor` | Cursor string for the previous page. | | `next_page_results` | Boolean indicating if there are more results after the current page. | | `prev_page_results` | Boolean indicating if there are results before the current page. | | `count` | Total number of items on the current page. | | `total_pages` | Estimated total number of pages. | | `total_results` | Total number of items across all pages. | | `extra_stats` | Additional statistics, if any. | | `results` | Array of items for the current page. | ### Example: Fetching the First Page **Request:** ``` GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items/?per_page=20 ``` **Response:** ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 50, "total_results": 1000, "extra_stats": {}, "results": [ ... ] } ``` ### Example: Fetching the Next Page **Request:** ``` GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items?per_page=20&cursor=20:1:0 ``` **Response:** ```json { "next_cursor": "20:2:0", "prev_cursor": "20:0:1", "next_page_results": true, "prev_page_results": true, "count": 20, "total_pages": 50, "total_results": 1000, "extra_stats": {}, "results": [ ... ] } ``` *** ## Rate Limiting ### Overview To ensure fair usage and maintain the quality of service for all users, our API implements rate limiting. Rate limiting restricts the number of requests a client can make within a certain time frame. ### Rate Limit Details * **Limit**: Each client is limited to 60 requests per minute. * **Reset Interval**: The rate limit counter resets every minute. * **Scope of Limitation**: The rate limit applies to all requests made with a given API key. ### Identifying Your Rate Limit Status Rate limit status is communicated in the response headers of each API request: * **`X-RateLimit-Remaining`**: The number of requests remaining in the current rate limit window. * **`X-RateLimit-Reset`**: The time at which the current rate limit window resets (in UTC epoch seconds). ``` X-RateLimit-Remaining: 45 X-RateLimit-Reset: 1700327957 ``` *** ## Fields and Expand Query Parameters Our API provides flexible data retrieval capabilities through two powerful query parameters: `fields` and `expand`. These parameters allow clients to tailor the response data to their specific needs, optimizing both the payload size and the clarity of the response. ### Fields Parameter The `fields` parameter enables clients to selectively retrieve only a subset of fields for a given resource. This is particularly useful for minimizing response size and bandwidth consumption, especially when the client requires only specific pieces of data. **Usage:** The `fields` parameter accepts a comma-separated list of field names that the client wants to be included in the response. ``` GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items/?fields=id,name,description ``` In this example, the API will return only the `id`, `name`, and `description` fields of the resource. ### Expand Parameter The `expand` parameter allows clients to request additional related information to be included in the response. This is useful for retrieving detailed information about nested resources without making separate API calls. **Usage:** The `expand` parameter can be used to include details of related resources or nested objects in the response. ``` GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items/?expand=assignees,state ``` This request will return the resource data along with expanded information about the `assignees` and `state`. ### Error Handling * Invalid or unrecognized field names passed in the `fields` parameter will result in an error response, indicating which fields are invalid. * Similarly, if `expand` is used on fields that cannot be expanded, an appropriate error message will be returned. --- --- url: 'https://developers.plane.so/api-reference/project/overview.html' description: >- Plane Project API overview. Learn about endpoints, request/response format, and how to work with project via REST API. --- # Overview Projects organize your team's work within a workspace. Each project contains work items, cycles, modules, and other resources. [Learn more about Projects](https://docs.plane.so/core-concepts/projects/overview) ### The Project Object ### Attributes * `name` *string* (**required**) Name of the project * `identifier` *string* (**required**) Unique Identifier of project for the workspace * `description` *string* Project description * `total_members` *integer* Total members present in the project. * `total_cycles` *integer* Total number of cycles present in the project. * `total_modules` *integer* Total number of modules present in the project. * `is_member` *boolean* The current requesting user is a member of the project or not * `member_role` *integer* The current requesting users role in the project. * `is_deployed` *integer* Represents if the project is deployed and publicly visible. * `created_at` *timestamp* The timestamp of the time when the project was created * `updated_at` *timestamp* The timestamp of the time when the project was last updated * `network` *integer* Is the project public or secret it takes in two values either (0,2) * **0 - Secret** * **2 - Public** * `emoji` *string* HTML emoji DEX code without the `&#` * `icon_prop` *json* saves the data of the project icon * `module_view` *bool* Enable disable module for the project in the UI * `cycle_view` *bool* Enable disable cycle for the project in the UI * `inbox_view` *bool* Enable disable intake for the project in the UI * `page_view` *bool* Enable disable pages for the project in the UI * `issue_views_view` *bool* Enable disable project views for the project in the UI * `cover_image` *url* URL for the image for the project cover * `archive_in` *integer* Months in which the issue should be automatically archived can take values between (0,12) * `close_in` *integer* Months in which the issue should be auto closed can take values between (0,12) * `created_by` , `updated_by` *uuid* This values are auto saved and represent the id of the user that created or the updated the project * `workspace` *uuid* The workspace uuid where the project is created saved automatically * `default_assignee` *uuid* The uuid of the user who is a workspace member that have issues assigned automatically if the issue does not have any assignee * `project_lead` *uuid* The uuid of the user who is a workspace member that leads the project * `estimate` *uuid* UUID of the estimate of the project * `default_state` Default state which will be used when the issues will be auto closed * `template_id` *uuid* UUID of the project template used to create this project. ```json { "id": "00918ea1-52f7-48bd-abe3-d3efe76ff7dd", "total_members": 1, "total_cycles": 0, "total_modules": 0, "is_member": true, "member_role": 20, "is_deployed": false, "created_at": "2023-11-19T10:40:15.426652Z", "updated_at": "2023-11-19T10:40:15.426672Z", "name": "Project X", "description": "", "description_text": null, "description_html": null, "network": 2, "identifier": "PROJX", "emoji": null, "icon_prop": null, "module_view": true, "cycle_view": true, "issue_views_view": true, "page_view": true, "inbox_view": false, "cover_image": null, "archive_in": 0, "close_in": 0, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "default_assignee": null, "project_lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "estimate": null, "default_state": null } ``` --- --- url: 'https://developers.plane.so/api-reference/project/add-project.html' description: >- Create a project via Plane API. HTTP request format, parameters, scopes, and example responses for create a project. --- # Create a project Create a new project in the workspace with default states and member assignments. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Project lead. Default assignee. Identifier. Icon prop. Emoji. Cover image. Module view. Cycle view. Issue views view. Page view. Intake view. Guest view all features. Archive in. Close in. * `Africa/Abidjan` - Africa/Abidjan * `Africa/Accra` - Africa/Accra * `Africa/Addis_Ababa` - Africa/Addis\_Ababa * `Africa/Algiers` - Africa/Algiers * `Africa/Asmara` - Africa/Asmara * `Africa/Bamako` - Africa/Bamako * `Africa/Bangui` - Africa/Bangui * `Africa/Banjul` - Africa/Banjul * `Africa/Bissau` - Africa/Bissau * `Africa/Blantyre` - Africa/Blantyre * `Africa/Brazzaville` - Africa/Brazzaville * `Africa/Bujumbura` - Africa/Bujumbura * `Africa/Cairo` - Africa/Cairo * `Africa/Casablanca` - Africa/Casablanca * `Africa/Ceuta` - Africa/Ceuta * `Africa/Conakry` - Africa/Conakry * `Africa/Dakar` - Africa/Dakar * `Africa/Dar_es_Salaam` - Africa/Dar\_es\_Salaam * `Africa/Djibouti` - Africa/Djibouti * `Africa/Douala` - Africa/Douala * `Africa/El_Aaiun` - Africa/El\_Aaiun * `Africa/Freetown` - Africa/Freetown * `Africa/Gaborone` - Africa/Gaborone * `Africa/Harare` - Africa/Harare * `Africa/Johannesburg` - Africa/Johannesburg * `Africa/Juba` - Africa/Juba * `Africa/Kampala` - Africa/Kampala * `Africa/Khartoum` - Africa/Khartoum * `Africa/Kigali` - Africa/Kigali * `Africa/Kinshasa` - Africa/Kinshasa * `Africa/Lagos` - Africa/Lagos * `Africa/Libreville` - Africa/Libreville * `Africa/Lome` - Africa/Lome * `Africa/Luanda` - Africa/Luanda * `Africa/Lubumbashi` - Africa/Lubumbashi * `Africa/Lusaka` - Africa/Lusaka * `Africa/Malabo` - Africa/Malabo * `Africa/Maputo` - Africa/Maputo * `Africa/Maseru` - Africa/Maseru * `Africa/Mbabane` - Africa/Mbabane * `Africa/Mogadishu` - Africa/Mogadishu * `Africa/Monrovia` - Africa/Monrovia * `Africa/Nairobi` - Africa/Nairobi * `Africa/Ndjamena` - Africa/Ndjamena * `Africa/Niamey` - Africa/Niamey * `Africa/Nouakchott` - Africa/Nouakchott * `Africa/Ouagadougou` - Africa/Ouagadougou * `Africa/Porto-Novo` - Africa/Porto-Novo * `Africa/Sao_Tome` - Africa/Sao\_Tome * `Africa/Tripoli` - Africa/Tripoli * `Africa/Tunis` - Africa/Tunis * `Africa/Windhoek` - Africa/Windhoek * `America/Adak` - America/Adak * `America/Anchorage` - America/Anchorage * `America/Anguilla` - America/Anguilla * `America/Antigua` - America/Antigua * `America/Araguaina` - America/Araguaina * `America/Argentina/Buenos_Aires` - America/Argentina/Buenos\_Aires * `America/Argentina/Catamarca` - America/Argentina/Catamarca * `America/Argentina/Cordoba` - America/Argentina/Cordoba * `America/Argentina/Jujuy` - America/Argentina/Jujuy * `America/Argentina/La_Rioja` - America/Argentina/La\_Rioja * `America/Argentina/Mendoza` - America/Argentina/Mendoza * `America/Argentina/Rio_Gallegos` - America/Argentina/Rio\_Gallegos * `America/Argentina/Salta` - America/Argentina/Salta * `America/Argentina/San_Juan` - America/Argentina/San\_Juan * `America/Argentina/San_Luis` - America/Argentina/San\_Luis * `America/Argentina/Tucuman` - America/Argentina/Tucuman * `America/Argentina/Ushuaia` - America/Argentina/Ushuaia * `America/Aruba` - America/Aruba * `America/Asuncion` - America/Asuncion * `America/Atikokan` - America/Atikokan * `America/Bahia` - America/Bahia * `America/Bahia_Banderas` - America/Bahia\_Banderas * `America/Barbados` - America/Barbados * `America/Belem` - America/Belem * `America/Belize` - America/Belize * `America/Blanc-Sablon` - America/Blanc-Sablon * `America/Boa_Vista` - America/Boa\_Vista * `America/Bogota` - America/Bogota * `America/Boise` - America/Boise * `America/Cambridge_Bay` - America/Cambridge\_Bay * `America/Campo_Grande` - America/Campo\_Grande * `America/Cancun` - America/Cancun * `America/Caracas` - America/Caracas * `America/Cayenne` - America/Cayenne * `America/Cayman` - America/Cayman * `America/Chicago` - America/Chicago * `America/Chihuahua` - America/Chihuahua * `America/Ciudad_Juarez` - America/Ciudad\_Juarez * `America/Costa_Rica` - America/Costa\_Rica * `America/Creston` - America/Creston * `America/Cuiaba` - America/Cuiaba * `America/Curacao` - America/Curacao * `America/Danmarkshavn` - America/Danmarkshavn * `America/Dawson` - America/Dawson * `America/Dawson_Creek` - America/Dawson\_Creek * `America/Denver` - America/Denver * `America/Detroit` - America/Detroit * `America/Dominica` - America/Dominica * `America/Edmonton` - America/Edmonton * `America/Eirunepe` - America/Eirunepe * `America/El_Salvador` - America/El\_Salvador * `America/Fort_Nelson` - America/Fort\_Nelson * `America/Fortaleza` - America/Fortaleza * `America/Glace_Bay` - America/Glace\_Bay * `America/Goose_Bay` - America/Goose\_Bay * `America/Grand_Turk` - America/Grand\_Turk * `America/Grenada` - America/Grenada * `America/Guadeloupe` - America/Guadeloupe * `America/Guatemala` - America/Guatemala * `America/Guayaquil` - America/Guayaquil * `America/Guyana` - America/Guyana * `America/Halifax` - America/Halifax * `America/Havana` - America/Havana * `America/Hermosillo` - America/Hermosillo * `America/Indiana/Indianapolis` - America/Indiana/Indianapolis * `America/Indiana/Knox` - America/Indiana/Knox * `America/Indiana/Marengo` - America/Indiana/Marengo * `America/Indiana/Petersburg` - America/Indiana/Petersburg * `America/Indiana/Tell_City` - America/Indiana/Tell\_City * `America/Indiana/Vevay` - America/Indiana/Vevay * `America/Indiana/Vincennes` - America/Indiana/Vincennes * `America/Indiana/Winamac` - America/Indiana/Winamac * `America/Inuvik` - America/Inuvik * `America/Iqaluit` - America/Iqaluit * `America/Jamaica` - America/Jamaica * `America/Juneau` - America/Juneau * `America/Kentucky/Louisville` - America/Kentucky/Louisville * `America/Kentucky/Monticello` - America/Kentucky/Monticello * `America/Kralendijk` - America/Kralendijk * `America/La_Paz` - America/La\_Paz * `America/Lima` - America/Lima * `America/Los_Angeles` - America/Los\_Angeles * `America/Lower_Princes` - America/Lower\_Princes * `America/Maceio` - America/Maceio * `America/Managua` - America/Managua * `America/Manaus` - America/Manaus * `America/Marigot` - America/Marigot * `America/Martinique` - America/Martinique * `America/Matamoros` - America/Matamoros * `America/Mazatlan` - America/Mazatlan * `America/Menominee` - America/Menominee * `America/Merida` - America/Merida * `America/Metlakatla` - America/Metlakatla * `America/Mexico_City` - America/Mexico\_City * `America/Miquelon` - America/Miquelon * `America/Moncton` - America/Moncton * `America/Monterrey` - America/Monterrey * `America/Montevideo` - America/Montevideo * `America/Montserrat` - America/Montserrat * `America/Nassau` - America/Nassau * `America/New_York` - America/New\_York * `America/Nome` - America/Nome * `America/Noronha` - America/Noronha * `America/North_Dakota/Beulah` - America/North\_Dakota/Beulah * `America/North_Dakota/Center` - America/North\_Dakota/Center * `America/North_Dakota/New_Salem` - America/North\_Dakota/New\_Salem * `America/Nuuk` - America/Nuuk * `America/Ojinaga` - America/Ojinaga * `America/Panama` - America/Panama * `America/Paramaribo` - America/Paramaribo * `America/Phoenix` - America/Phoenix * `America/Port-au-Prince` - America/Port-au-Prince * `America/Port_of_Spain` - America/Port\_of\_Spain * `America/Porto_Velho` - America/Porto\_Velho * `America/Puerto_Rico` - America/Puerto\_Rico * `America/Punta_Arenas` - America/Punta\_Arenas * `America/Rankin_Inlet` - America/Rankin\_Inlet * `America/Recife` - America/Recife * `America/Regina` - America/Regina * `America/Resolute` - America/Resolute * `America/Rio_Branco` - America/Rio\_Branco * `America/Santarem` - America/Santarem * `America/Santiago` - America/Santiago * `America/Santo_Domingo` - America/Santo\_Domingo * `America/Sao_Paulo` - America/Sao\_Paulo * `America/Scoresbysund` - America/Scoresbysund * `America/Sitka` - America/Sitka * `America/St_Barthelemy` - America/St\_Barthelemy * `America/St_Johns` - America/St\_Johns * `America/St_Kitts` - America/St\_Kitts * `America/St_Lucia` - America/St\_Lucia * `America/St_Thomas` - America/St\_Thomas * `America/St_Vincent` - America/St\_Vincent * `America/Swift_Current` - America/Swift\_Current * `America/Tegucigalpa` - America/Tegucigalpa * `America/Thule` - America/Thule * `America/Tijuana` - America/Tijuana * `America/Toronto` - America/Toronto * `America/Tortola` - America/Tortola * `America/Vancouver` - America/Vancouver * `America/Whitehorse` - America/Whitehorse * `America/Winnipeg` - America/Winnipeg * `America/Yakutat` - America/Yakutat * `Antarctica/Casey` - Antarctica/Casey * `Antarctica/Davis` - Antarctica/Davis * `Antarctica/DumontDUrville` - Antarctica/DumontDUrville * `Antarctica/Macquarie` - Antarctica/Macquarie * `Antarctica/Mawson` - Antarctica/Mawson * `Antarctica/McMurdo` - Antarctica/McMurdo * `Antarctica/Palmer` - Antarctica/Palmer * `Antarctica/Rothera` - Antarctica/Rothera * `Antarctica/Syowa` - Antarctica/Syowa * `Antarctica/Troll` - Antarctica/Troll * `Antarctica/Vostok` - Antarctica/Vostok * `Arctic/Longyearbyen` - Arctic/Longyearbyen * `Asia/Aden` - Asia/Aden * `Asia/Almaty` - Asia/Almaty * `Asia/Amman` - Asia/Amman * `Asia/Anadyr` - Asia/Anadyr * `Asia/Aqtau` - Asia/Aqtau * `Asia/Aqtobe` - Asia/Aqtobe * `Asia/Ashgabat` - Asia/Ashgabat * `Asia/Atyrau` - Asia/Atyrau * `Asia/Baghdad` - Asia/Baghdad * `Asia/Bahrain` - Asia/Bahrain * `Asia/Baku` - Asia/Baku * `Asia/Bangkok` - Asia/Bangkok * `Asia/Barnaul` - Asia/Barnaul * `Asia/Beirut` - Asia/Beirut * `Asia/Bishkek` - Asia/Bishkek * `Asia/Brunei` - Asia/Brunei * `Asia/Chita` - Asia/Chita * `Asia/Choibalsan` - Asia/Choibalsan * `Asia/Colombo` - Asia/Colombo * `Asia/Damascus` - Asia/Damascus * `Asia/Dhaka` - Asia/Dhaka * `Asia/Dili` - Asia/Dili * `Asia/Dubai` - Asia/Dubai * `Asia/Dushanbe` - Asia/Dushanbe * `Asia/Famagusta` - Asia/Famagusta * `Asia/Gaza` - Asia/Gaza * `Asia/Hebron` - Asia/Hebron * `Asia/Ho_Chi_Minh` - Asia/Ho\_Chi\_Minh * `Asia/Hong_Kong` - Asia/Hong\_Kong * `Asia/Hovd` - Asia/Hovd * `Asia/Irkutsk` - Asia/Irkutsk * `Asia/Jakarta` - Asia/Jakarta * `Asia/Jayapura` - Asia/Jayapura * `Asia/Jerusalem` - Asia/Jerusalem * `Asia/Kabul` - Asia/Kabul * `Asia/Kamchatka` - Asia/Kamchatka * `Asia/Karachi` - Asia/Karachi * `Asia/Kathmandu` - Asia/Kathmandu * `Asia/Khandyga` - Asia/Khandyga * `Asia/Kolkata` - Asia/Kolkata * `Asia/Krasnoyarsk` - Asia/Krasnoyarsk * `Asia/Kuala_Lumpur` - Asia/Kuala\_Lumpur * `Asia/Kuching` - Asia/Kuching * `Asia/Kuwait` - Asia/Kuwait * `Asia/Macau` - Asia/Macau * `Asia/Magadan` - Asia/Magadan * `Asia/Makassar` - Asia/Makassar * `Asia/Manila` - Asia/Manila * `Asia/Muscat` - Asia/Muscat * `Asia/Nicosia` - Asia/Nicosia * `Asia/Novokuznetsk` - Asia/Novokuznetsk * `Asia/Novosibirsk` - Asia/Novosibirsk * `Asia/Omsk` - Asia/Omsk * `Asia/Oral` - Asia/Oral * `Asia/Phnom_Penh` - Asia/Phnom\_Penh * `Asia/Pontianak` - Asia/Pontianak * `Asia/Pyongyang` - Asia/Pyongyang * `Asia/Qatar` - Asia/Qatar * `Asia/Qostanay` - Asia/Qostanay * `Asia/Qyzylorda` - Asia/Qyzylorda * `Asia/Riyadh` - Asia/Riyadh * `Asia/Sakhalin` - Asia/Sakhalin * `Asia/Samarkand` - Asia/Samarkand * `Asia/Seoul` - Asia/Seoul * `Asia/Shanghai` - Asia/Shanghai * `Asia/Singapore` - Asia/Singapore * `Asia/Srednekolymsk` - Asia/Srednekolymsk * `Asia/Taipei` - Asia/Taipei * `Asia/Tashkent` - Asia/Tashkent * `Asia/Tbilisi` - Asia/Tbilisi * `Asia/Tehran` - Asia/Tehran * `Asia/Thimphu` - Asia/Thimphu * `Asia/Tokyo` - Asia/Tokyo * `Asia/Tomsk` - Asia/Tomsk * `Asia/Ulaanbaatar` - Asia/Ulaanbaatar * `Asia/Urumqi` - Asia/Urumqi * `Asia/Ust-Nera` - Asia/Ust-Nera * `Asia/Vientiane` - Asia/Vientiane * `Asia/Vladivostok` - Asia/Vladivostok * `Asia/Yakutsk` - Asia/Yakutsk * `Asia/Yangon` - Asia/Yangon * `Asia/Yekaterinburg` - Asia/Yekaterinburg * `Asia/Yerevan` - Asia/Yerevan * `Atlantic/Azores` - Atlantic/Azores * `Atlantic/Bermuda` - Atlantic/Bermuda * `Atlantic/Canary` - Atlantic/Canary * `Atlantic/Cape_Verde` - Atlantic/Cape\_Verde * `Atlantic/Faroe` - Atlantic/Faroe * `Atlantic/Madeira` - Atlantic/Madeira * `Atlantic/Reykjavik` - Atlantic/Reykjavik * `Atlantic/South_Georgia` - Atlantic/South\_Georgia * `Atlantic/St_Helena` - Atlantic/St\_Helena * `Atlantic/Stanley` - Atlantic/Stanley * `Australia/Adelaide` - Australia/Adelaide * `Australia/Brisbane` - Australia/Brisbane * `Australia/Broken_Hill` - Australia/Broken\_Hill * `Australia/Darwin` - Australia/Darwin * `Australia/Eucla` - Australia/Eucla * `Australia/Hobart` - Australia/Hobart * `Australia/Lindeman` - Australia/Lindeman * `Australia/Lord_Howe` - Australia/Lord\_Howe * `Australia/Melbourne` - Australia/Melbourne * `Australia/Perth` - Australia/Perth * `Australia/Sydney` - Australia/Sydney * `Canada/Atlantic` - Canada/Atlantic * `Canada/Central` - Canada/Central * `Canada/Eastern` - Canada/Eastern * `Canada/Mountain` - Canada/Mountain * `Canada/Newfoundland` - Canada/Newfoundland * `Canada/Pacific` - Canada/Pacific * `Europe/Amsterdam` - Europe/Amsterdam * `Europe/Andorra` - Europe/Andorra * `Europe/Astrakhan` - Europe/Astrakhan * `Europe/Athens` - Europe/Athens * `Europe/Belgrade` - Europe/Belgrade * `Europe/Berlin` - Europe/Berlin * `Europe/Bratislava` - Europe/Bratislava * `Europe/Brussels` - Europe/Brussels * `Europe/Bucharest` - Europe/Bucharest * `Europe/Budapest` - Europe/Budapest * `Europe/Busingen` - Europe/Busingen * `Europe/Chisinau` - Europe/Chisinau * `Europe/Copenhagen` - Europe/Copenhagen * `Europe/Dublin` - Europe/Dublin * `Europe/Gibraltar` - Europe/Gibraltar * `Europe/Guernsey` - Europe/Guernsey * `Europe/Helsinki` - Europe/Helsinki * `Europe/Isle_of_Man` - Europe/Isle\_of\_Man * `Europe/Istanbul` - Europe/Istanbul * `Europe/Jersey` - Europe/Jersey * `Europe/Kaliningrad` - Europe/Kaliningrad * `Europe/Kirov` - Europe/Kirov * `Europe/Kyiv` - Europe/Kyiv * `Europe/Lisbon` - Europe/Lisbon * `Europe/Ljubljana` - Europe/Ljubljana * `Europe/London` - Europe/London * `Europe/Luxembourg` - Europe/Luxembourg * `Europe/Madrid` - Europe/Madrid * `Europe/Malta` - Europe/Malta * `Europe/Mariehamn` - Europe/Mariehamn * `Europe/Minsk` - Europe/Minsk * `Europe/Monaco` - Europe/Monaco * `Europe/Moscow` - Europe/Moscow * `Europe/Oslo` - Europe/Oslo * `Europe/Paris` - Europe/Paris * `Europe/Podgorica` - Europe/Podgorica * `Europe/Prague` - Europe/Prague * `Europe/Riga` - Europe/Riga * `Europe/Rome` - Europe/Rome * `Europe/Samara` - Europe/Samara * `Europe/San_Marino` - Europe/San\_Marino * `Europe/Sarajevo` - Europe/Sarajevo * `Europe/Saratov` - Europe/Saratov * `Europe/Simferopol` - Europe/Simferopol * `Europe/Skopje` - Europe/Skopje * `Europe/Sofia` - Europe/Sofia * `Europe/Stockholm` - Europe/Stockholm * `Europe/Tallinn` - Europe/Tallinn * `Europe/Tirane` - Europe/Tirane * `Europe/Ulyanovsk` - Europe/Ulyanovsk * `Europe/Vaduz` - Europe/Vaduz * `Europe/Vatican` - Europe/Vatican * `Europe/Vienna` - Europe/Vienna * `Europe/Vilnius` - Europe/Vilnius * `Europe/Volgograd` - Europe/Volgograd * `Europe/Warsaw` - Europe/Warsaw * `Europe/Zagreb` - Europe/Zagreb * `Europe/Zurich` - Europe/Zurich * `GMT` - GMT * `Indian/Antananarivo` - Indian/Antananarivo * `Indian/Chagos` - Indian/Chagos * `Indian/Christmas` - Indian/Christmas * `Indian/Cocos` - Indian/Cocos * `Indian/Comoro` - Indian/Comoro * `Indian/Kerguelen` - Indian/Kerguelen * `Indian/Mahe` - Indian/Mahe * `Indian/Maldives` - Indian/Maldives * `Indian/Mauritius` - Indian/Mauritius * `Indian/Mayotte` - Indian/Mayotte * `Indian/Reunion` - Indian/Reunion * `Pacific/Apia` - Pacific/Apia * `Pacific/Auckland` - Pacific/Auckland * `Pacific/Bougainville` - Pacific/Bougainville * `Pacific/Chatham` - Pacific/Chatham * `Pacific/Chuuk` - Pacific/Chuuk * `Pacific/Easter` - Pacific/Easter * `Pacific/Efate` - Pacific/Efate * `Pacific/Fakaofo` - Pacific/Fakaofo * `Pacific/Fiji` - Pacific/Fiji * `Pacific/Funafuti` - Pacific/Funafuti * `Pacific/Galapagos` - Pacific/Galapagos * `Pacific/Gambier` - Pacific/Gambier * `Pacific/Guadalcanal` - Pacific/Guadalcanal * `Pacific/Guam` - Pacific/Guam * `Pacific/Honolulu` - Pacific/Honolulu * `Pacific/Kanton` - Pacific/Kanton * `Pacific/Kiritimati` - Pacific/Kiritimati * `Pacific/Kosrae` - Pacific/Kosrae * `Pacific/Kwajalein` - Pacific/Kwajalein * `Pacific/Majuro` - Pacific/Majuro * `Pacific/Marquesas` - Pacific/Marquesas * `Pacific/Midway` - Pacific/Midway * `Pacific/Nauru` - Pacific/Nauru * `Pacific/Niue` - Pacific/Niue * `Pacific/Norfolk` - Pacific/Norfolk * `Pacific/Noumea` - Pacific/Noumea * `Pacific/Pago_Pago` - Pacific/Pago\_Pago * `Pacific/Palau` - Pacific/Palau * `Pacific/Pitcairn` - Pacific/Pitcairn * `Pacific/Pohnpei` - Pacific/Pohnpei * `Pacific/Port_Moresby` - Pacific/Port\_Moresby * `Pacific/Rarotonga` - Pacific/Rarotonga * `Pacific/Saipan` - Pacific/Saipan * `Pacific/Tahiti` - Pacific/Tahiti * `Pacific/Tarawa` - Pacific/Tarawa * `Pacific/Tongatapu` - Pacific/Tongatapu * `Pacific/Wake` - Pacific/Wake * `Pacific/Wallis` - Pacific/Wallis * `US/Alaska` - US/Alaska * `US/Arizona` - US/Arizona * `US/Central` - US/Central * `US/Eastern` - US/Eastern * `US/Hawaii` - US/Hawaii * `US/Mountain` - US/Mountain * `US/Pacific` - US/Pacific * `UTC` - UTC External source. External id. Is issue type enabled. Is time tracking enabled. ### Scopes `projects:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "project_lead": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "project_lead": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/projects/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", identifier: "PROJ-123", project_lead: "550e8400-e29b-41d4-a716-446655440000", }), }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2, "project_lead": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/project/create-project-with-template.html description: >- Create a project from an existing project template via Plane API. HTTP request format, parameters, scopes, and example responses for create project with template. --- # Create project with template Create a new project from an existing project template. The template's states, labels, estimates, modules, and work items are copied into the new project. Fields provided in the request body override the template defaults. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters The ID of the project template to instantiate. Must belong to the same workspace. Name of the new project. Overrides the template default. Short identifier for the project (e.g. `MAR`). Overrides the template default. Description of the new project. Overrides the template default. Network visibility of the project. `0` for secret, `2` for public. Overrides the template default. User ID of the project lead. The lead is added as a project admin. Overrides the template default. ### Scopes `write` or `projects:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/templates/use/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "template_id": "7a2d3972-80a5-4ac5-8bb5-03026671826a", "name": "Mobile App Revamp", "identifier": "MAR", "description": "Project created from the Agile Project Setup template", "network": 2, "project_lead": "0d8d8869-3ed1-4fb4-b5c4-ff672888f5e2" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/templates/use/", headers={"X-API-Key": "your-api-key"}, json={ "template_id": "7a2d3972-80a5-4ac5-8bb5-03026671826a", "name": "Mobile App Revamp", "identifier": "MAR", "description": "Project created from the Agile Project Setup template", "network": 2, "project_lead": "0d8d8869-3ed1-4fb4-b5c4-ff672888f5e2" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/templates/use/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ template_id: "7a2d3972-80a5-4ac5-8bb5-03026671826a", name: "Mobile App Revamp", identifier: "MAR", description: "Project created from the Agile Project Setup template", network: 2, project_lead: "0d8d8869-3ed1-4fb4-b5c4-ff672888f5e2", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Mobile App Revamp", "description": "Project created from the Agile Project Setup template", "identifier": "MAR", "network": 2, "project_lead": "0d8d8869-3ed1-4fb4-b5c4-ff672888f5e2", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/project/list-projects.html' description: >- List all projects via Plane API. HTTP request format, parameters, scopes, and example responses for list all projects. --- # List all projects Retrieve all projects in a workspace or get details of a specific project. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2 } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/project/get-project-detail.html' description: >- Retrieve a project via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a project. --- # Retrieve a project Retrieve details of a specific project. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2, "project_lead": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/project/update-project-detail.html' description: >- Update a project via Plane API. HTTP request format, parameters, scopes, and example responses for update a project. --- # Update a project Partially update an existing project's properties like name, description, or settings. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Project lead. Default assignee. Identifier. Icon prop. Emoji. Cover image. Module view. Cycle view. Issue views view. Page view. Intake view. Guest view all features. Archive in. Close in. * `Africa/Abidjan` - Africa/Abidjan * `Africa/Accra` - Africa/Accra * `Africa/Addis_Ababa` - Africa/Addis\_Ababa * `Africa/Algiers` - Africa/Algiers * `Africa/Asmara` - Africa/Asmara * `Africa/Bamako` - Africa/Bamako * `Africa/Bangui` - Africa/Bangui * `Africa/Banjul` - Africa/Banjul * `Africa/Bissau` - Africa/Bissau * `Africa/Blantyre` - Africa/Blantyre * `Africa/Brazzaville` - Africa/Brazzaville * `Africa/Bujumbura` - Africa/Bujumbura * `Africa/Cairo` - Africa/Cairo * `Africa/Casablanca` - Africa/Casablanca * `Africa/Ceuta` - Africa/Ceuta * `Africa/Conakry` - Africa/Conakry * `Africa/Dakar` - Africa/Dakar * `Africa/Dar_es_Salaam` - Africa/Dar\_es\_Salaam * `Africa/Djibouti` - Africa/Djibouti * `Africa/Douala` - Africa/Douala * `Africa/El_Aaiun` - Africa/El\_Aaiun * `Africa/Freetown` - Africa/Freetown * `Africa/Gaborone` - Africa/Gaborone * `Africa/Harare` - Africa/Harare * `Africa/Johannesburg` - Africa/Johannesburg * `Africa/Juba` - Africa/Juba * `Africa/Kampala` - Africa/Kampala * `Africa/Khartoum` - Africa/Khartoum * `Africa/Kigali` - Africa/Kigali * `Africa/Kinshasa` - Africa/Kinshasa * `Africa/Lagos` - Africa/Lagos * `Africa/Libreville` - Africa/Libreville * `Africa/Lome` - Africa/Lome * `Africa/Luanda` - Africa/Luanda * `Africa/Lubumbashi` - Africa/Lubumbashi * `Africa/Lusaka` - Africa/Lusaka * `Africa/Malabo` - Africa/Malabo * `Africa/Maputo` - Africa/Maputo * `Africa/Maseru` - Africa/Maseru * `Africa/Mbabane` - Africa/Mbabane * `Africa/Mogadishu` - Africa/Mogadishu * `Africa/Monrovia` - Africa/Monrovia * `Africa/Nairobi` - Africa/Nairobi * `Africa/Ndjamena` - Africa/Ndjamena * `Africa/Niamey` - Africa/Niamey * `Africa/Nouakchott` - Africa/Nouakchott * `Africa/Ouagadougou` - Africa/Ouagadougou * `Africa/Porto-Novo` - Africa/Porto-Novo * `Africa/Sao_Tome` - Africa/Sao\_Tome * `Africa/Tripoli` - Africa/Tripoli * `Africa/Tunis` - Africa/Tunis * `Africa/Windhoek` - Africa/Windhoek * `America/Adak` - America/Adak * `America/Anchorage` - America/Anchorage * `America/Anguilla` - America/Anguilla * `America/Antigua` - America/Antigua * `America/Araguaina` - America/Araguaina * `America/Argentina/Buenos_Aires` - America/Argentina/Buenos\_Aires * `America/Argentina/Catamarca` - America/Argentina/Catamarca * `America/Argentina/Cordoba` - America/Argentina/Cordoba * `America/Argentina/Jujuy` - America/Argentina/Jujuy * `America/Argentina/La_Rioja` - America/Argentina/La\_Rioja * `America/Argentina/Mendoza` - America/Argentina/Mendoza * `America/Argentina/Rio_Gallegos` - America/Argentina/Rio\_Gallegos * `America/Argentina/Salta` - America/Argentina/Salta * `America/Argentina/San_Juan` - America/Argentina/San\_Juan * `America/Argentina/San_Luis` - America/Argentina/San\_Luis * `America/Argentina/Tucuman` - America/Argentina/Tucuman * `America/Argentina/Ushuaia` - America/Argentina/Ushuaia * `America/Aruba` - America/Aruba * `America/Asuncion` - America/Asuncion * `America/Atikokan` - America/Atikokan * `America/Bahia` - America/Bahia * `America/Bahia_Banderas` - America/Bahia\_Banderas * `America/Barbados` - America/Barbados * `America/Belem` - America/Belem * `America/Belize` - America/Belize * `America/Blanc-Sablon` - America/Blanc-Sablon * `America/Boa_Vista` - America/Boa\_Vista * `America/Bogota` - America/Bogota * `America/Boise` - America/Boise * `America/Cambridge_Bay` - America/Cambridge\_Bay * `America/Campo_Grande` - America/Campo\_Grande * `America/Cancun` - America/Cancun * `America/Caracas` - America/Caracas * `America/Cayenne` - America/Cayenne * `America/Cayman` - America/Cayman * `America/Chicago` - America/Chicago * `America/Chihuahua` - America/Chihuahua * `America/Ciudad_Juarez` - America/Ciudad\_Juarez * `America/Costa_Rica` - America/Costa\_Rica * `America/Creston` - America/Creston * `America/Cuiaba` - America/Cuiaba * `America/Curacao` - America/Curacao * `America/Danmarkshavn` - America/Danmarkshavn * `America/Dawson` - America/Dawson * `America/Dawson_Creek` - America/Dawson\_Creek * `America/Denver` - America/Denver * `America/Detroit` - America/Detroit * `America/Dominica` - America/Dominica * `America/Edmonton` - America/Edmonton * `America/Eirunepe` - America/Eirunepe * `America/El_Salvador` - America/El\_Salvador * `America/Fort_Nelson` - America/Fort\_Nelson * `America/Fortaleza` - America/Fortaleza * `America/Glace_Bay` - America/Glace\_Bay * `America/Goose_Bay` - America/Goose\_Bay * `America/Grand_Turk` - America/Grand\_Turk * `America/Grenada` - America/Grenada * `America/Guadeloupe` - America/Guadeloupe * `America/Guatemala` - America/Guatemala * `America/Guayaquil` - America/Guayaquil * `America/Guyana` - America/Guyana * `America/Halifax` - America/Halifax * `America/Havana` - America/Havana * `America/Hermosillo` - America/Hermosillo * `America/Indiana/Indianapolis` - America/Indiana/Indianapolis * `America/Indiana/Knox` - America/Indiana/Knox * `America/Indiana/Marengo` - America/Indiana/Marengo * `America/Indiana/Petersburg` - America/Indiana/Petersburg * `America/Indiana/Tell_City` - America/Indiana/Tell\_City * `America/Indiana/Vevay` - America/Indiana/Vevay * `America/Indiana/Vincennes` - America/Indiana/Vincennes * `America/Indiana/Winamac` - America/Indiana/Winamac * `America/Inuvik` - America/Inuvik * `America/Iqaluit` - America/Iqaluit * `America/Jamaica` - America/Jamaica * `America/Juneau` - America/Juneau * `America/Kentucky/Louisville` - America/Kentucky/Louisville * `America/Kentucky/Monticello` - America/Kentucky/Monticello * `America/Kralendijk` - America/Kralendijk * `America/La_Paz` - America/La\_Paz * `America/Lima` - America/Lima * `America/Los_Angeles` - America/Los\_Angeles * `America/Lower_Princes` - America/Lower\_Princes * `America/Maceio` - America/Maceio * `America/Managua` - America/Managua * `America/Manaus` - America/Manaus * `America/Marigot` - America/Marigot * `America/Martinique` - America/Martinique * `America/Matamoros` - America/Matamoros * `America/Mazatlan` - America/Mazatlan * `America/Menominee` - America/Menominee * `America/Merida` - America/Merida * `America/Metlakatla` - America/Metlakatla * `America/Mexico_City` - America/Mexico\_City * `America/Miquelon` - America/Miquelon * `America/Moncton` - America/Moncton * `America/Monterrey` - America/Monterrey * `America/Montevideo` - America/Montevideo * `America/Montserrat` - America/Montserrat * `America/Nassau` - America/Nassau * `America/New_York` - America/New\_York * `America/Nome` - America/Nome * `America/Noronha` - America/Noronha * `America/North_Dakota/Beulah` - America/North\_Dakota/Beulah * `America/North_Dakota/Center` - America/North\_Dakota/Center * `America/North_Dakota/New_Salem` - America/North\_Dakota/New\_Salem * `America/Nuuk` - America/Nuuk * `America/Ojinaga` - America/Ojinaga * `America/Panama` - America/Panama * `America/Paramaribo` - America/Paramaribo * `America/Phoenix` - America/Phoenix * `America/Port-au-Prince` - America/Port-au-Prince * `America/Port_of_Spain` - America/Port\_of\_Spain * `America/Porto_Velho` - America/Porto\_Velho * `America/Puerto_Rico` - America/Puerto\_Rico * `America/Punta_Arenas` - America/Punta\_Arenas * `America/Rankin_Inlet` - America/Rankin\_Inlet * `America/Recife` - America/Recife * `America/Regina` - America/Regina * `America/Resolute` - America/Resolute * `America/Rio_Branco` - America/Rio\_Branco * `America/Santarem` - America/Santarem * `America/Santiago` - America/Santiago * `America/Santo_Domingo` - America/Santo\_Domingo * `America/Sao_Paulo` - America/Sao\_Paulo * `America/Scoresbysund` - America/Scoresbysund * `America/Sitka` - America/Sitka * `America/St_Barthelemy` - America/St\_Barthelemy * `America/St_Johns` - America/St\_Johns * `America/St_Kitts` - America/St\_Kitts * `America/St_Lucia` - America/St\_Lucia * `America/St_Thomas` - America/St\_Thomas * `America/St_Vincent` - America/St\_Vincent * `America/Swift_Current` - America/Swift\_Current * `America/Tegucigalpa` - America/Tegucigalpa * `America/Thule` - America/Thule * `America/Tijuana` - America/Tijuana * `America/Toronto` - America/Toronto * `America/Tortola` - America/Tortola * `America/Vancouver` - America/Vancouver * `America/Whitehorse` - America/Whitehorse * `America/Winnipeg` - America/Winnipeg * `America/Yakutat` - America/Yakutat * `Antarctica/Casey` - Antarctica/Casey * `Antarctica/Davis` - Antarctica/Davis * `Antarctica/DumontDUrville` - Antarctica/DumontDUrville * `Antarctica/Macquarie` - Antarctica/Macquarie * `Antarctica/Mawson` - Antarctica/Mawson * `Antarctica/McMurdo` - Antarctica/McMurdo * `Antarctica/Palmer` - Antarctica/Palmer * `Antarctica/Rothera` - Antarctica/Rothera * `Antarctica/Syowa` - Antarctica/Syowa * `Antarctica/Troll` - Antarctica/Troll * `Antarctica/Vostok` - Antarctica/Vostok * `Arctic/Longyearbyen` - Arctic/Longyearbyen * `Asia/Aden` - Asia/Aden * `Asia/Almaty` - Asia/Almaty * `Asia/Amman` - Asia/Amman * `Asia/Anadyr` - Asia/Anadyr * `Asia/Aqtau` - Asia/Aqtau * `Asia/Aqtobe` - Asia/Aqtobe * `Asia/Ashgabat` - Asia/Ashgabat * `Asia/Atyrau` - Asia/Atyrau * `Asia/Baghdad` - Asia/Baghdad * `Asia/Bahrain` - Asia/Bahrain * `Asia/Baku` - Asia/Baku * `Asia/Bangkok` - Asia/Bangkok * `Asia/Barnaul` - Asia/Barnaul * `Asia/Beirut` - Asia/Beirut * `Asia/Bishkek` - Asia/Bishkek * `Asia/Brunei` - Asia/Brunei * `Asia/Chita` - Asia/Chita * `Asia/Choibalsan` - Asia/Choibalsan * `Asia/Colombo` - Asia/Colombo * `Asia/Damascus` - Asia/Damascus * `Asia/Dhaka` - Asia/Dhaka * `Asia/Dili` - Asia/Dili * `Asia/Dubai` - Asia/Dubai * `Asia/Dushanbe` - Asia/Dushanbe * `Asia/Famagusta` - Asia/Famagusta * `Asia/Gaza` - Asia/Gaza * `Asia/Hebron` - Asia/Hebron * `Asia/Ho_Chi_Minh` - Asia/Ho\_Chi\_Minh * `Asia/Hong_Kong` - Asia/Hong\_Kong * `Asia/Hovd` - Asia/Hovd * `Asia/Irkutsk` - Asia/Irkutsk * `Asia/Jakarta` - Asia/Jakarta * `Asia/Jayapura` - Asia/Jayapura * `Asia/Jerusalem` - Asia/Jerusalem * `Asia/Kabul` - Asia/Kabul * `Asia/Kamchatka` - Asia/Kamchatka * `Asia/Karachi` - Asia/Karachi * `Asia/Kathmandu` - Asia/Kathmandu * `Asia/Khandyga` - Asia/Khandyga * `Asia/Kolkata` - Asia/Kolkata * `Asia/Krasnoyarsk` - Asia/Krasnoyarsk * `Asia/Kuala_Lumpur` - Asia/Kuala\_Lumpur * `Asia/Kuching` - Asia/Kuching * `Asia/Kuwait` - Asia/Kuwait * `Asia/Macau` - Asia/Macau * `Asia/Magadan` - Asia/Magadan * `Asia/Makassar` - Asia/Makassar * `Asia/Manila` - Asia/Manila * `Asia/Muscat` - Asia/Muscat * `Asia/Nicosia` - Asia/Nicosia * `Asia/Novokuznetsk` - Asia/Novokuznetsk * `Asia/Novosibirsk` - Asia/Novosibirsk * `Asia/Omsk` - Asia/Omsk * `Asia/Oral` - Asia/Oral * `Asia/Phnom_Penh` - Asia/Phnom\_Penh * `Asia/Pontianak` - Asia/Pontianak * `Asia/Pyongyang` - Asia/Pyongyang * `Asia/Qatar` - Asia/Qatar * `Asia/Qostanay` - Asia/Qostanay * `Asia/Qyzylorda` - Asia/Qyzylorda * `Asia/Riyadh` - Asia/Riyadh * `Asia/Sakhalin` - Asia/Sakhalin * `Asia/Samarkand` - Asia/Samarkand * `Asia/Seoul` - Asia/Seoul * `Asia/Shanghai` - Asia/Shanghai * `Asia/Singapore` - Asia/Singapore * `Asia/Srednekolymsk` - Asia/Srednekolymsk * `Asia/Taipei` - Asia/Taipei * `Asia/Tashkent` - Asia/Tashkent * `Asia/Tbilisi` - Asia/Tbilisi * `Asia/Tehran` - Asia/Tehran * `Asia/Thimphu` - Asia/Thimphu * `Asia/Tokyo` - Asia/Tokyo * `Asia/Tomsk` - Asia/Tomsk * `Asia/Ulaanbaatar` - Asia/Ulaanbaatar * `Asia/Urumqi` - Asia/Urumqi * `Asia/Ust-Nera` - Asia/Ust-Nera * `Asia/Vientiane` - Asia/Vientiane * `Asia/Vladivostok` - Asia/Vladivostok * `Asia/Yakutsk` - Asia/Yakutsk * `Asia/Yangon` - Asia/Yangon * `Asia/Yekaterinburg` - Asia/Yekaterinburg * `Asia/Yerevan` - Asia/Yerevan * `Atlantic/Azores` - Atlantic/Azores * `Atlantic/Bermuda` - Atlantic/Bermuda * `Atlantic/Canary` - Atlantic/Canary * `Atlantic/Cape_Verde` - Atlantic/Cape\_Verde * `Atlantic/Faroe` - Atlantic/Faroe * `Atlantic/Madeira` - Atlantic/Madeira * `Atlantic/Reykjavik` - Atlantic/Reykjavik * `Atlantic/South_Georgia` - Atlantic/South\_Georgia * `Atlantic/St_Helena` - Atlantic/St\_Helena * `Atlantic/Stanley` - Atlantic/Stanley * `Australia/Adelaide` - Australia/Adelaide * `Australia/Brisbane` - Australia/Brisbane * `Australia/Broken_Hill` - Australia/Broken\_Hill * `Australia/Darwin` - Australia/Darwin * `Australia/Eucla` - Australia/Eucla * `Australia/Hobart` - Australia/Hobart * `Australia/Lindeman` - Australia/Lindeman * `Australia/Lord_Howe` - Australia/Lord\_Howe * `Australia/Melbourne` - Australia/Melbourne * `Australia/Perth` - Australia/Perth * `Australia/Sydney` - Australia/Sydney * `Canada/Atlantic` - Canada/Atlantic * `Canada/Central` - Canada/Central * `Canada/Eastern` - Canada/Eastern * `Canada/Mountain` - Canada/Mountain * `Canada/Newfoundland` - Canada/Newfoundland * `Canada/Pacific` - Canada/Pacific * `Europe/Amsterdam` - Europe/Amsterdam * `Europe/Andorra` - Europe/Andorra * `Europe/Astrakhan` - Europe/Astrakhan * `Europe/Athens` - Europe/Athens * `Europe/Belgrade` - Europe/Belgrade * `Europe/Berlin` - Europe/Berlin * `Europe/Bratislava` - Europe/Bratislava * `Europe/Brussels` - Europe/Brussels * `Europe/Bucharest` - Europe/Bucharest * `Europe/Budapest` - Europe/Budapest * `Europe/Busingen` - Europe/Busingen * `Europe/Chisinau` - Europe/Chisinau * `Europe/Copenhagen` - Europe/Copenhagen * `Europe/Dublin` - Europe/Dublin * `Europe/Gibraltar` - Europe/Gibraltar * `Europe/Guernsey` - Europe/Guernsey * `Europe/Helsinki` - Europe/Helsinki * `Europe/Isle_of_Man` - Europe/Isle\_of\_Man * `Europe/Istanbul` - Europe/Istanbul * `Europe/Jersey` - Europe/Jersey * `Europe/Kaliningrad` - Europe/Kaliningrad * `Europe/Kirov` - Europe/Kirov * `Europe/Kyiv` - Europe/Kyiv * `Europe/Lisbon` - Europe/Lisbon * `Europe/Ljubljana` - Europe/Ljubljana * `Europe/London` - Europe/London * `Europe/Luxembourg` - Europe/Luxembourg * `Europe/Madrid` - Europe/Madrid * `Europe/Malta` - Europe/Malta * `Europe/Mariehamn` - Europe/Mariehamn * `Europe/Minsk` - Europe/Minsk * `Europe/Monaco` - Europe/Monaco * `Europe/Moscow` - Europe/Moscow * `Europe/Oslo` - Europe/Oslo * `Europe/Paris` - Europe/Paris * `Europe/Podgorica` - Europe/Podgorica * `Europe/Prague` - Europe/Prague * `Europe/Riga` - Europe/Riga * `Europe/Rome` - Europe/Rome * `Europe/Samara` - Europe/Samara * `Europe/San_Marino` - Europe/San\_Marino * `Europe/Sarajevo` - Europe/Sarajevo * `Europe/Saratov` - Europe/Saratov * `Europe/Simferopol` - Europe/Simferopol * `Europe/Skopje` - Europe/Skopje * `Europe/Sofia` - Europe/Sofia * `Europe/Stockholm` - Europe/Stockholm * `Europe/Tallinn` - Europe/Tallinn * `Europe/Tirane` - Europe/Tirane * `Europe/Ulyanovsk` - Europe/Ulyanovsk * `Europe/Vaduz` - Europe/Vaduz * `Europe/Vatican` - Europe/Vatican * `Europe/Vienna` - Europe/Vienna * `Europe/Vilnius` - Europe/Vilnius * `Europe/Volgograd` - Europe/Volgograd * `Europe/Warsaw` - Europe/Warsaw * `Europe/Zagreb` - Europe/Zagreb * `Europe/Zurich` - Europe/Zurich * `GMT` - GMT * `Indian/Antananarivo` - Indian/Antananarivo * `Indian/Chagos` - Indian/Chagos * `Indian/Christmas` - Indian/Christmas * `Indian/Cocos` - Indian/Cocos * `Indian/Comoro` - Indian/Comoro * `Indian/Kerguelen` - Indian/Kerguelen * `Indian/Mahe` - Indian/Mahe * `Indian/Maldives` - Indian/Maldives * `Indian/Mauritius` - Indian/Mauritius * `Indian/Mayotte` - Indian/Mayotte * `Indian/Reunion` - Indian/Reunion * `Pacific/Apia` - Pacific/Apia * `Pacific/Auckland` - Pacific/Auckland * `Pacific/Bougainville` - Pacific/Bougainville * `Pacific/Chatham` - Pacific/Chatham * `Pacific/Chuuk` - Pacific/Chuuk * `Pacific/Easter` - Pacific/Easter * `Pacific/Efate` - Pacific/Efate * `Pacific/Fakaofo` - Pacific/Fakaofo * `Pacific/Fiji` - Pacific/Fiji * `Pacific/Funafuti` - Pacific/Funafuti * `Pacific/Galapagos` - Pacific/Galapagos * `Pacific/Gambier` - Pacific/Gambier * `Pacific/Guadalcanal` - Pacific/Guadalcanal * `Pacific/Guam` - Pacific/Guam * `Pacific/Honolulu` - Pacific/Honolulu * `Pacific/Kanton` - Pacific/Kanton * `Pacific/Kiritimati` - Pacific/Kiritimati * `Pacific/Kosrae` - Pacific/Kosrae * `Pacific/Kwajalein` - Pacific/Kwajalein * `Pacific/Majuro` - Pacific/Majuro * `Pacific/Marquesas` - Pacific/Marquesas * `Pacific/Midway` - Pacific/Midway * `Pacific/Nauru` - Pacific/Nauru * `Pacific/Niue` - Pacific/Niue * `Pacific/Norfolk` - Pacific/Norfolk * `Pacific/Noumea` - Pacific/Noumea * `Pacific/Pago_Pago` - Pacific/Pago\_Pago * `Pacific/Palau` - Pacific/Palau * `Pacific/Pitcairn` - Pacific/Pitcairn * `Pacific/Pohnpei` - Pacific/Pohnpei * `Pacific/Port_Moresby` - Pacific/Port\_Moresby * `Pacific/Rarotonga` - Pacific/Rarotonga * `Pacific/Saipan` - Pacific/Saipan * `Pacific/Tahiti` - Pacific/Tahiti * `Pacific/Tarawa` - Pacific/Tarawa * `Pacific/Tongatapu` - Pacific/Tongatapu * `Pacific/Wake` - Pacific/Wake * `Pacific/Wallis` - Pacific/Wallis * `US/Alaska` - US/Alaska * `US/Arizona` - US/Arizona * `US/Central` - US/Central * `US/Eastern` - US/Eastern * `US/Hawaii` - US/Hawaii * `US/Mountain` - US/Mountain * `US/Pacific` - US/Pacific * `UTC` - UTC External source. External id. Is issue type enabled. Is time tracking enabled. Default state. Estimate. ### Scopes `projects:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "project_lead": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "project_lead": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", identifier: "PROJ-123", project_lead: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2, "project_lead": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/project/archive-project.html' description: >- Archive project via Plane API. HTTP request format, parameters, scopes, and example responses for archive project. --- # Archive project Move a project to archived status, hiding it from active project lists. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archive/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archive/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archive/", { method: "POST", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/project/unarchive-project.html' description: >- Unarchive project via Plane API. HTTP request format, parameters, scopes, and example responses for unarchive project. --- # Unarchive project Restore an archived project to active status, making it available in regular workflows. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archive/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archive/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archive/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/project/delete-project.html' description: >- Delete a project via Plane API. HTTP request format, parameters, scopes, and example responses for delete a project. --- # Delete a project Permanently remove a project and all its associated data from the workspace. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/project-features/overview.html' description: >- Plane Project Features API overview. Learn how to inspect and update project feature flags with the Plane API. --- # Overview Project features control which project-level capabilities are enabled for an individual project. [Learn more about Projects](https://docs.plane.so/core-concepts/projects/overview) ## The Project Feature Object ### Attributes * `epics` *boolean* Epics. * `modules` *boolean* Modules. * `cycles` *boolean* Cycles. * `views` *boolean* Views. * `pages` *boolean* Pages. * `intakes` *boolean* Intakes. * `work_item_types` *boolean* Work item types. ```json { "epics": true, "modules": true, "cycles": true, "views": true, "pages": true, "intakes": true, "work_item_types": true } ``` --- --- url: >- https://developers.plane.so/api-reference/project-features/get-project-features.html description: >- Get project features via Plane API. HTTP request format, parameters, scopes, and example responses for get project features. --- # Get project features Get the features of a project ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.features:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/features/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/features/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/features/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "epics": true, "modules": true, "cycles": true, "views": true, "pages": true, "intakes": true, "work_item_types": true } ``` --- --- url: >- https://developers.plane.so/api-reference/project-features/update-project-features.html description: >- Update project features via Plane API. HTTP request format, parameters, scopes, and example responses for update project features. --- # Update project features Update the features of a project ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Epics. Modules. Cycles. Views. Pages. Intakes. Work item types. ### Scopes `projects.features:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/features/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "epics": true, "modules": true, "cycles": true, "views": true, "pages": true, "intakes": true, "work_item_types": true }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/features/", headers={"X-API-Key": "your-api-key"}, json={ "epics": true, "modules": true, "cycles": true, "views": true, "pages": true, "intakes": true, "work_item_types": true } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/features/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ epics: true, modules: true, cycles: true, views: true, pages: true, intakes: true, work_item_types: true, }), }, ); const data = await response.json(); ``` ```json { "epics": true, "modules": true, "cycles": true, "views": true, "pages": true, "intakes": true, "work_item_types": true } ``` --- --- url: 'https://developers.plane.so/api-reference/project-labels/overview.html' description: >- Plane Project Labels API overview. Learn how to manage workspace-level project labels with the Plane API. --- # Overview Project labels define reusable classifications that can be attached to projects across a workspace. [Learn more about Projects](https://developers.plane.so/api-reference/project/overview) ## The Project Label Object ### Attributes * `id` *string* Id. * `name` *string* Name. * `description` *string* Description. * `color` *string* Color. * `sort_order` *number* Sort order. * `workspace` *string* Workspace. * `created_at` *string* Created at. * `updated_at` *string* Updated at. * `created_by` *string* Created by. * `updated_by` *string* Updated by. ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#f39c12", "description": "Example description", "sort_order": 65535, "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/project-labels/add-project-label.html description: >- Create project label via Plane API. HTTP request format, parameters, scopes, and example responses for create project label. --- # Create project label Create a new project label in the workspace with name, color, and description. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Color. Sort order. ### Scopes `projects.labels:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "color": "#ff0000", "description": "Example description" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "color": "#ff0000", "description": "Example description" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", color: "#ff0000", description: "Example description", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#f39c12", "description": "Example description", "sort_order": 65535, "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/project-labels/list-project-labels.html description: >- List project labels via Plane API. HTTP request format, parameters, scopes, and example responses for list project labels. --- # List project labels Retrieve all project labels in a workspace. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/project-labels/get-project-label-detail.html description: >- Get project label via Plane API. HTTP request format, parameters, scopes, and example responses for get project label. --- # Get project label Retrieve details of a specific project label. ### Path Parameters The unique identifier of the label. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#f39c12", "description": "Example description", "sort_order": 65535, "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/project-labels/update-project-label-detail.html description: >- Update project label via Plane API. HTTP request format, parameters, scopes, and example responses for update project label. --- # Update project label Partially update an existing project label's properties like name, color, or description. ### Path Parameters The unique identifier of the label. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Color. Sort order. ### Scopes `projects.labels:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "color": "#00ff00", "description": "Example description" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "color": "#00ff00", "description": "Example description" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", color: "#00ff00", description: "Example description", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#f39c12", "description": "Example description", "sort_order": 65535, "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/project-labels/delete-project-label.html description: >- Delete project label via Plane API. HTTP request format, parameters, scopes, and example responses for delete project label. --- # Delete project label Permanently delete an existing project label from the workspace. ### Path Parameters The unique identifier of the label. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.labels:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/project-labels/label-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/issue/overview.html' description: >- Plane Issue API overview. Learn about endpoints, request/response format, and how to work with issue via REST API. --- # Overview Work items are the fundamental unit of work in Plane. They represent tasks that need to be accomplished — assignable, trackable, and actionable to-dos in your project management workflow. [Learn more about Work Items](https://docs.plane.so/core-concepts/issues/overview) ## The Work Item object ### Attributes * `name` *string* **(required)** Name of the work item * `created_at` , `updated_at` *timestamp* Timestamp of the work item when it was created and when it was last updated * `estimate_point` *integer* or *null* Total estimate points for the work item takes value between (0,7). * `description_html` *string* HTML description of the work item * `description_stripped` *string* Stripped version of the html description auto generated using the application. * `priority` *string* Priority of the work item takes in 5 values * none * urgent * high * medium * low * `start_date` *date* Start date of the work item * `target_date` *date* Target date of the work item * `sequence_id` *integer* Auto generated from the system the unique identifier of the work item * `sort_order` *decimal* Auto generated from the system during creation used for ordering * `completed_at` *timestamp* or *null* Timestamp when the work item is moved to any completed group state * `created_by` & `updated_by` This values are auto saved and represent the id of the user that created or the updated the project. * `project` *uuid* The project which the work item is part of auto generated from backend * `workspace` *uuid* The workspace which the work item is part of auto generated from backend * `parent` *uuid* The uuid of the parent work item which should be part of the same workspace * `state` *uuid* The uuid of the state which is present in the project where the work item is being created. * `assignees` - *\[uuid,]* The array of uuids of the users who are part of the project where the work item is being created or updated. * `labels` - *\[uuid,]* The array of uuids of the labels which are present in the project where the work item is being created or updated. * `type` *uuid* The uuid of the work item type for the work item. * `module` *uuid* The uuid of the module the work item belongs to. * `is_draft` *boolean* Whether the work item is a draft. * `archived_at` *timestamp* or *null* Timestamp when the work item was archived. * `description_binary` *string* Binary description of the work item. **Expandable Fields** The following fields can be expanded when retrieving a work item by including them in the `expand` query parameter: * `type` - Expands to full WorkItemType object * `module` - Expands to full Module object * `labels` - Expands to array of full Label objects * `assignees` - Expands to array of full User objects * `state` - Expands to full State object * `project` - Expands to full Project object ```json { "id": "e1c25c66-5bb8-465e-a818-92a483423443", "created_at": "2023-11-19T11:56:55.176802Z", "updated_at": "2023-11-19T11:56:55.176809Z", "estimate_point": null, "name": "First Work Item", "description_html": "

", "description_stripped": "", "priority": "none", "start_date": "2023-09-01", "target_date": "2023-10-04", "sequence_id": 421, "sort_order": 265535.0, "completed_at": null, "archived_at": null, "is_draft": false, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "parent": null, "state": "f3f045db-7e74-49f2-b3b2-0b7dee4635ae", "assignees": ["797b5aea-3f40-4199-be84-5f94e0d04501"], "labels": [] } ``` --- --- url: 'https://developers.plane.so/api-reference/issue/add-issue.html' description: >- Create a work item via Plane API. HTTP request format, parameters, scopes, and example responses for create a work item. --- # Create a work item Create a new work item in the specified project with the provided details. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Assignees. Labels. Type id. Parent. Deleted at. Point. Name. Description html. Description stripped. * `urgent` - Urgent * `high` - High * `medium` - Medium * `low` - Low * `none` - None Start date. Target date. Sequence id. Sort order. Completed at. Archived at. Last activity at. Is draft. External source. External id. Created by. State. Estimate point. Type. ### Scopes `projects.work_items:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "priority": "medium", "state": "550e8400-e29b-41d4-a716-446655440000", "assignees": [ "550e8400-e29b-41d4-a716-446655440000" ], "labels": [ "550e8400-e29b-41d4-a716-446655440000" ], "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "priority": "medium", "state": "550e8400-e29b-41d4-a716-446655440000", "assignees": [ "550e8400-e29b-41d4-a716-446655440000" ], "labels": [ "550e8400-e29b-41d4-a716-446655440000" ], "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", priority: "medium", state: "550e8400-e29b-41d4-a716-446655440000", assignees: ["550e8400-e29b-41d4-a716-446655440000"], labels: ["550e8400-e29b-41d4-a716-446655440000"], external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "sequence_id": 1, "priority": "high", "assignees": ["550e8400-e29b-41d4-a716-446655440000"], "labels": ["550e8400-e29b-41d4-a716-446655440000"], "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/issue/list-issues.html' description: >- List all work items via Plane API. HTTP request format, parameters, scopes, and example responses for list all work items. --- # List all work items Retrieve a paginated list of all work items in a project. Supports filtering, ordering, and field selection through query parameters. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response External system identifier for filtering or lookup External system source name for filtering or lookup Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.work_items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "priority": "high", "sequence_id": 123, "state": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "group": "started" }, "assignees": [], "labels": [], "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/issue/get-issue-detail.html' description: >- Retrieve a work item by ID via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a work item by id. --- # Retrieve a work item by ID Retrieve details of a specific work item. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Comma-separated list of related fields to expand in response External system identifier for filtering or lookup External system source name for filtering or lookup Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order ### Scopes `projects.work_items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/?expand=assignees&external_id=1234567890" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/?expand=assignees&external_id=1234567890", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/?expand=assignees&external_id=1234567890", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "sequence_id": 1, "priority": "high", "assignees": ["550e8400-e29b-41d4-a716-446655440000"], "labels": ["550e8400-e29b-41d4-a716-446655440000"], "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/issue/get-issue-sequence-id.html' description: >- Retrieve a work item by identifier via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a work item by identifier. --- # Retrieve a work item by identifier Retrieve a specific work item using workspace slug, project identifier, and issue identifier. ### Path Parameters The numeric issue identifier. The project identifier key. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/PROJ-123/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/PROJ-123/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/PROJ-123/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "sequence_id": 1, "priority": "high", "assignees": ["550e8400-e29b-41d4-a716-446655440000"], "labels": ["550e8400-e29b-41d4-a716-446655440000"], "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/issue/search-issues.html' description: >- Search work items via Plane API. HTTP request format, parameters, scopes, and example responses for search work items. --- # Search work items Perform semantic search across issue names, sequence IDs, and project identifiers. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Maximum number of results to return Project ID for filtering results within a specific project Search query to filter results by name, description, or identifier Whether to search across entire workspace or within specific project ### Scopes `projects.work_items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/search/?limit=10&project_id=project-uuid" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/search/?limit=10&project_id=project-uuid", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/search/?limit=10&project_id=project-uuid", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "issues": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "sequence_id": 123, "project__identifier": "MAB", "project_id": "550e8400-e29b-41d4-a716-446655440000", "workspace__slug": "my-workspace" }, { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "sequence_id": 124, "project__identifier": "MAB", "project_id": "550e8400-e29b-41d4-a716-446655440000", "workspace__slug": "my-workspace" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/issue/advanced-search-work-items.html description: >- Advanced search work items via Plane API. HTTP request format, parameters, scopes, and example responses for advanced search work items. --- # Advanced search work items Search for work items with advanced filters and search query. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Search query string for text-based search across issue fields Filter JSON passed through to IssueFilterSet for validation and application Maximum number of results to return Whether to search across all projects in the workspace Optional project ID to filter results to a specific project ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/advanced-search/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "login", "project_id": "550e8400-e29b-41d4-a716-446655440000", "limit": 10 }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/advanced-search/", headers={"X-API-Key": "your-api-key"}, json={ "query": "login", "project_id": "550e8400-e29b-41d4-a716-446655440000", "limit": 10 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/work-items/advanced-search/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ query: "login", project_id: "550e8400-e29b-41d4-a716-446655440000", limit: 10, }), }, ); const data = await response.json(); ``` ```json [ [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "sequence_id": 102, "project_identifier": "WEB", "project_id": "550e8400-e29b-41d4-a716-446655440000", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "type_id": "550e8400-e29b-41d4-a716-446655440000", "state_id": "550e8400-e29b-41d4-a716-446655440000", "priority": "high", "target_date": "2024-01-01", "start_date": "2024-01-01" }, { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "sequence_id": 245, "project_identifier": "API", "project_id": "550e8400-e29b-41d4-a716-446655440000", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "type_id": "550e8400-e29b-41d4-a716-446655440000", "state_id": "550e8400-e29b-41d4-a716-446655440000", "priority": "medium", "target_date": null, "start_date": "2024-01-01" } ] ] ``` --- --- url: 'https://developers.plane.so/api-reference/issue/update-issue-detail.html' description: >- Update a work item via Plane API. HTTP request format, parameters, scopes, and example responses for update a work item. --- # Update a work item Partially update an existing work item with the provided fields. Supports external ID validation to prevent conflicts. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Assignees. Labels. Type id. Parent. Deleted at. Point. Name. Description html. Description stripped. * `urgent` - Urgent * `high` - High * `medium` - Medium * `low` - Low * `none` - None Start date. Target date. Sequence id. Sort order. Completed at. Archived at. Last activity at. Is draft. External source. External id. Created by. State. Estimate point. Type. ### Scopes `projects.work_items:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "priority": "medium", "state": "550e8400-e29b-41d4-a716-446655440000", "assignees": [ "550e8400-e29b-41d4-a716-446655440000" ], "labels": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "priority": "medium", "state": "550e8400-e29b-41d4-a716-446655440000", "assignees": [ "550e8400-e29b-41d4-a716-446655440000" ], "labels": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", priority: "medium", state: "550e8400-e29b-41d4-a716-446655440000", assignees: ["550e8400-e29b-41d4-a716-446655440000"], labels: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "sequence_id": 1, "priority": "high", "assignees": ["550e8400-e29b-41d4-a716-446655440000"], "labels": ["550e8400-e29b-41d4-a716-446655440000"], "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/issue/delete-issue.html' description: >- Delete a work item via Plane API. HTTP request format, parameters, scopes, and example responses for delete a work item. --- # Delete a work item Permanently delete an existing work item from the project. Only admins or the item creator can perform this action. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/state/overview.html' description: >- Plane State API overview. Learn about endpoints, request/response format, and how to work with state via REST API. --- # Overview States represent the current status of a work item in your project workflow. [Learn more about States](https://docs.plane.so/core-concepts/work-items/states) ## The State Object ### Attributes * `name` *string* **( required )** Name of the state * `created_at` , `updated_at` *timestamp* Timestamp of the issue when it was created and when it was last updated * `description` *string* Description of the state * `color` *string* **(required)** String code of the color * `workspace_slug` *string* Slugified name of the state auto generated from the system * `sequence` *string* Auto generated sequence of the state for ordering. * `group` *string* **(required)** Group to which the state belongs can only take values * backlog * unstarted * started * completed * cancelled * `default` *boolean* Is it the default state in which if the issues are not assigned any states all the issues are created in this state. * `created_by` & `updated_by` This values are auto saved and represent the id of the user that created or the updated the project. * `project` *uuid* The project which the issue is part of auto generated from backend * `workspace` *uuid* The workspace which the issue is part of auto generated from backend ```json { "id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f", "created_at": "2023-11-19T17:41:45.478363Z", "updated_at": "2023-11-19T17:41:45.478383Z", "name": "Ideation", "description": "", "color": "#eb5757", "workspace_slug": "ideation", "sequence": 130000.0, "group": "unstarted", "default": false, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/state/add-state.html' description: >- Create a state via Plane API. HTTP request format, parameters, scopes, and example responses for create a state. --- # Create a state Create a new workflow state for a project with specified name, color, and group. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Color. Sequence. * `backlog` - Backlog * `unstarted` - Unstarted * `started` - Started * `completed` - Completed * `cancelled` - Cancelled * `triage` - Triage Is triage. Default. External source. External id. ### Scopes `projects.states:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "color": "#ff0000", "group": "backlog", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "color": "#ff0000", "group": "backlog", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", color: "#ff0000", group: "backlog", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#f39c12", "group": "started", "sequence": 2, "default": false, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/state/list-states.html' description: >- List all states via Plane API. HTTP request format, parameters, scopes, and example responses for list all states. --- # List all states Retrieve all workflow states for a project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Number of results per page (default: 20, max: 100) ### Scopes `projects.states:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#ffa500", "group": "started", "sequence": 2 } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/state/get-state-detail.html' description: >- Retrieve a state via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a state. --- # Retrieve a state Retrieve details of a specific state. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the state. ### Scopes `projects.states:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#f39c12", "group": "started", "sequence": 2, "default": false, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/state/update-state-detail.html' description: >- Update a state via Plane API. HTTP request format, parameters, scopes, and example responses for update a state. --- # Update a state Partially update an existing workflow state's properties like name, color, or group. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the state. ### Body Parameters Name. Description. Color. Sequence. * `backlog` - Backlog * `unstarted` - Unstarted * `started` - Started * `completed` - Completed * `cancelled` - Cancelled * `triage` - Triage Is triage. Default. External source. External id. ### Scopes `projects.states:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "color": "#00ff00", "group": "backlog", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "color": "#00ff00", "group": "backlog", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", color: "#00ff00", group: "backlog", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#f39c12", "group": "started", "sequence": 2, "default": false, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/state/delete-state.html' description: >- Delete a state via Plane API. HTTP request format, parameters, scopes, and example responses for delete a state. --- # Delete a state Permanently remove a workflow state from a project. Default states and states with existing work items cannot be deleted. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the state. ### Scopes `projects.states:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/states/state-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/label/overview.html' description: >- Plane Label API overview. Learn about endpoints, request/response format, and how to work with label via REST API. --- # Overview Labels are tags that help you categorize and organize work items in your project. [Learn more about Labels](https://docs.plane.so/core-concepts/work-items/labels) ## The Label Object ### Attributes * `name` *string* **(required)** Name of the label * `created_at` , `updated_at` *timestamp* Timestamp of the issue when it was created and when it was last updated. * `description` *string* Description of the Label * `color` *string* Hex code of the color * `sort_order` *float* Sort order of the label used for sorting * `created_by` & `updated_by` This values are auto saved and represent the id of the user that created or the updated the project. * `project` *uuid* The project which the issue is part of auto generated from backend * `workspace` *uuid* The workspace which the issue is part of auto generated from backend * `parent` *uuid or null* Parent of the label which is also a Label ```json { "id": "c7146baf-7058-496b-aa3a-df6c25a7e929", "created_at": "2023-11-20T06:01:03.538675Z", "updated_at": "2023-11-20T06:01:03.538683Z", "name": "High", "description": "", "color": "", "sort_order": 72416.0, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "parent": null } ``` --- --- url: 'https://developers.plane.so/api-reference/label/add-label.html' description: >- Create a label via Plane API. HTTP request format, parameters, scopes, and example responses for create a label. --- # Create a label Create a new label in the specified project with name, color, and description. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Color. Description. External source. External id. Parent. Sort order. ### Scopes `projects.labels:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "color": "#ff0000", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "color": "#ff0000", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", color: "#ff0000", description: "Example description", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#ff4444", "description": "Example description", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/label/list-labels.html' description: >- List all labels via Plane API. HTTP request format, parameters, scopes, and example responses for list all labels. --- # List all labels Retrieve all labels in a project. Supports filtering by name and color. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#ff4444", "description": "Example description" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/label/get-label-detail.html' description: >- Retrieve a label via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a label. --- # Retrieve a label Retrieve details of a specific label. ### Path Parameters The unique identifier of the label. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#ff4444", "description": "Example description", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/label/update-label-detail.html' description: >- Update a label via Plane API. HTTP request format, parameters, scopes, and example responses for update a label. --- # Update a label Partially update an existing label's properties like name, color, or description. ### Path Parameters The unique identifier of the label. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Color. Description. External source. External id. Parent. Sort order. ### Scopes `projects.labels:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "color": "#00ff00", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "color": "#00ff00", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", color: "#00ff00", description: "Example description", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "color": "#ff4444", "description": "Example description", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/label/delete-label.html' description: >- Delete a label via Plane API. HTTP request format, parameters, scopes, and example responses for delete a label. --- # Delete a label Permanently remove a label from the project. This action cannot be undone. ### Path Parameters The unique identifier of the label. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.labels:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/labels/label-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/issue-types/types/overview.html' description: >- Plane Types API overview. Learn about endpoints, request/response format, and how to work with types via REST API. --- # Overview Work item types categorize different kinds of work in your project (e.g., "Task", "Bug", "Feature"). [Learn more about Work Item Types](https://docs.plane.so/core-concepts/issues/work-item-types) ## The Work Item Type object ### Attributes * `id` *uuid* Unique identifier for the work item type * `name` *string* Name of the work item type * `description` *string* Description of the work item type * `logo_props` *object* Logo properties for the work item type * `is_epic` *boolean* Whether this work item type is an epic * `is_default` *boolean* Whether this is the default work item type * `is_active` *boolean* Whether this work item type is active * `level` *number* Level of the work item type * `workspace` *uuid* The workspace which the work item type is part of (auto generated from backend) * `project` *uuid* The project which the work item type is part of (auto generated from backend) * `created_at` *timestamp* Timestamp when the work item type was created * `updated_at` *timestamp* Timestamp when the work item type was last updated * `created_by` *uuid* ID of the user who created the work item type (auto saved) * `updated_by` *uuid* ID of the user who last updated the work item type (auto saved) * `deleted_at` *timestamp* Timestamp when the work item type was deleted (null if not deleted) * `external_id` *string* External ID for the work item type (auto saved) * `external_source` *string* External source for the work item type (auto saved) ```json { "id": "d6af3c13-3459-43ab-b91c-c33ef2fd7131", "name": "Postman work item type", "description": "Postman work item type description", "logo_props": {}, "is_epic": false, "is_default": false, "is_active": true, "level": 0, "workspace": "70b6599f-9313-4c0d-b5c0-406a13a05647", "project": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2024-10-23T06:54:46.169344Z", "updated_at": "2024-10-23T06:54:46.169390Z", "created_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "updated_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "deleted_at": null, "external_id": null, "external_source": null } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/types/add-issue-type.html description: >- Create a work item type via Plane API. HTTP request format, parameters, scopes, and example responses for create a work item type. --- # Create a work item type Create a new issue type for a project ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Is epic. Is active. External source. External id. ### Scopes `projects.work_item_types:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "project_ids": ["550e8400-e29b-41d4-a716-446655440000"], "logo_props": "example-value", "is_epic": true, "is_default": true, "is_active": true, "level": 1 } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/types/list-issue-types.html description: >- List all work item types via Plane API. HTTP request format, parameters, scopes, and example responses for list all work item types. --- # List all work item types List all issue types for a project ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_item_types:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "project_ids": ["550e8400-e29b-41d4-a716-446655440000"], "logo_props": "example-value", "is_epic": true, "is_default": true, "is_active": true, "level": 1 } ] ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/types/get-issue-type-details.html description: >- Retrieve a work item type via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a work item type. --- # Retrieve a work item type Retrieve an issue type by id ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Scopes `projects.work_item_types:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "project_ids": ["550e8400-e29b-41d4-a716-446655440000"], "logo_props": "example-value", "is_epic": true, "is_default": true, "is_active": true, "level": 1 } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/types/get-work-item-type-schema.html description: >- Get work item type schema via Plane API. HTTP request format, parameters, scopes, and example responses for get work item type schema. --- # Get work item type schema Returns the complete schema for a work item type including all standard fields and custom properties with their available options inline. This endpoint enables LLMs and MCP integrations to understand what fields are available when creating/updating work items. **Standard fields** are always included: * name, description\_html, priority, state\_id, assignee\_ids, label\_ids, start\_date, target\_date, parent\_id **Custom fields** are included when: * ISSUE\_TYPES feature is enabled AND * A type\_id is provided or a default type exists for the project **Options behavior:** * state\_id options are always included * priority options are always included * assignee\_ids and label\_ids options require `?include=members,labels` * estimate\_point\_id options are included when project has estimates configured ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Comma-separated list of additional options to include: members, labels Work item type ID. If not provided, returns schema for default type (when types enabled) or standard fields only. ### Scopes `projects.work_item_types:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/schema/?include=members&type_id=type-uuid" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/schema/?include=members&type_id=type-uuid", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/schema/?include=members&type_id=type-uuid", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "type_id": "550e8400-e29b-41d4-a716-446655440000", "type_name": "Bug", "type_description": "Example description", "type_logo_props": {}, "fields": { "name": { "type": "string", "required": true, "max_length": 255 }, "priority": { "type": "option", "required": false, "options": [ { "value": "urgent", "label": "Urgent" }, { "value": "high", "label": "High" } ] }, "state_id": { "type": "uuid", "required": false, "options": [ { "id": "...", "name": "Example Name", "group": "backlog" } ] } }, "custom_fields": { "custom_field_severity": { "id": "...", "type": "OPTION", "name": "Example Name", "display_name": "Example Name", "required": true, "is_multi": false, "options": [ { "id": "...", "name": "Example Name" } ] } } } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/types/update-issue-types.html description: >- Update a work item type via Plane API. HTTP request format, parameters, scopes, and example responses for update a work item type. --- # Update a work item type Update an issue type ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Body Parameters Name. Description. Is epic. Is active. External source. External id. ### Scopes `projects.work_item_types:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "project_ids": ["550e8400-e29b-41d4-a716-446655440000"], "logo_props": "example-value", "is_epic": true, "is_default": true, "is_active": true, "level": 1 } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/types/delete-issue-type.html description: >- Delete a work item type via Plane API. HTTP request format, parameters, scopes, and example responses for delete a work item type. --- # Delete a work item type Delete an issue type ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Scopes `projects.work_item_types:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/issue-types/properties/overview.html' description: >- Plane Properties API overview. Learn about endpoints, request/response format, and how to work with properties via REST API. --- # Overview Custom properties allow you to extend work items with additional fields specific to your workflow and processes. [Learn more about Custom properties](https://docs.plane.so/core-concepts/issues/work-item-types#add-custom-properties) ## The Properties Object ### Attributes * `workspace` *uuid* The workspace which the issue is part of auto generated from backend * `project` *uuid* The project which the issue is part of auto generated from backend * `created_at` , `updated_at` timestamp Timestamp of the issue when it was created and when it was last updated. * `created_by` & `updated_by` This values are auto saved and represent the id of the user that created or the updated the project. * `external_id` & `external_source` This values are auto saved and represent the id of the user that created or the updated the project. ```json { "id": "f962febb-98bc-43ca-8bfb-8012e4d54dae", "created_at": "2024-10-23T07:38:58.231897Z", "updated_at": "2024-10-23T07:38:58.231920Z", "deleted_at": null, "name": "first-issue-property", "display_name": "first issue property", "description": "first issue property", "logo_props": {}, "sort_order": 75535.0, "property_type": "OPTION", "relation_type": null, "is_required": false, "default_value": [], "settings": {}, "is_active": false, "is_multi": false, "validation_rules": {}, "external_source": null, "external_id": null, "created_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "updated_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "workspace": "70b6599f-9313-4c0d-b5c0-406a13a05647", "project": "03a9bf56-84f4-4afe-b232-9400eb9b7b6b", "issue_type": "1800681a-a749-487b-9003-3279031fea35" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/properties/add-property.html description: >- Create a custom property via Plane API. HTTP request format, parameters, scopes, and example responses for create a custom property. --- # Create a custom property Create a new issue property ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Body Parameters * `ISSUE` - Issue * `USER` - User List of options to create when property\_type is OPTION. Each option should have 'name', optionally 'description', 'is\_default', 'external\_id', and 'external\_source'. Display name. Description. * `TEXT` - Text * `DATETIME` - Datetime * `DECIMAL` - Decimal * `BOOLEAN` - Boolean * `OPTION` - Option * `RELATION` - Relation * `URL` - URL * `EMAIL` - Email * `FILE` - File * `FORMULA` - Formula Is required. Default value. Settings. Is active. Is multi. Validation rules. External source. External id. Formula config. ### Scopes `projects.work_item_properties:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "property_type": "OPTION", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "property_type": "OPTION", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", property_type: "OPTION", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "display_name": "Example Name", "description": "Example description", "property_type": "TEXT", "deleted_at": "2024-01-01T00:00:00Z", "relation_type": "ISSUE", "logo_props": "example-value", "sort_order": 1, "is_required": true } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/properties/list-properties.html description: >- List custom properties via Plane API. HTTP request format, parameters, scopes, and example responses for list custom properties. --- # List custom properties List issue properties ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Scopes `projects.work_item_properties:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "display_name": "Example Name", "description": "Example description", "property_type": "TEXT", "deleted_at": "2024-01-01T00:00:00Z", "relation_type": "ISSUE", "logo_props": "example-value", "sort_order": 1, "is_required": true } ] ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/properties/get-property-details.html description: >- Retrieve a custom property via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a custom property. --- # Retrieve a custom property Get issue property by id ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Scopes `projects.work_item_properties:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "display_name": "Example Name", "description": "Example description", "property_type": "TEXT", "deleted_at": "2024-01-01T00:00:00Z", "relation_type": "ISSUE", "logo_props": "example-value", "sort_order": 1, "is_required": true } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/properties/update-property.html description: >- Update a custom property via Plane API. HTTP request format, parameters, scopes, and example responses for update a custom property. --- # Update a custom property Update an issue property ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Body Parameters * `ISSUE` - Issue * `USER` - User List of options to create when property\_type is OPTION. Each option should have 'name', optionally 'description', 'is\_default', 'external\_id', and 'external\_source'. Display name. Description. * `TEXT` - Text * `DATETIME` - Datetime * `DECIMAL` - Decimal * `BOOLEAN` - Boolean * `OPTION` - Option * `RELATION` - Relation * `URL` - URL * `EMAIL` - Email * `FILE` - File * `FORMULA` - Formula Is required. Default value. Settings. Is active. Is multi. Validation rules. External source. External id. Formula config. ### Scopes `projects.work_item_properties:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "property_type": "OPTION", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "property_type": "OPTION", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", property_type: "OPTION", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "display_name": "Example Name", "description": "Example description", "property_type": "TEXT", "deleted_at": "2024-01-01T00:00:00Z", "relation_type": "ISSUE", "logo_props": "example-value", "sort_order": 1, "is_required": true } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/properties/delete-property.html description: >- Delete a custom property via Plane API. HTTP request format, parameters, scopes, and example responses for delete a custom property. --- # Delete a custom property Delete an issue property ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item type. ### Scopes `projects.work_item_properties:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-types/type-uuid/work-item-properties/property-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/issue-types/values/overview.html' description: >- Plane Values API overview. Learn about endpoints, request/response format, and how to work with values via REST API. --- # Overview Custom property values store the actual data entered into custom properties for specific work items. ## The Values Object ### Attributes * `workspace` *uuid* The workspace which the issue is part of auto generated from backend * `project` *uuid* The project which the issue is part of auto generated from backend * `created_at` , `updated_at` timestamp Timestamp of the issue when it was created and when it was last updated. * `created_by` & `updated_by` This values are auto saved and represent the id of the user that created or the updated the project. * `external_id` & `external_source` This values are auto saved and represent the id of the user that created or the updated the project. ```json { "id": "51a869d1-f612-4315-ac91-ffef3e96c20e", "created_at": "2024-10-23T07:44:42.883820Z", "updated_at": "2024-10-23T07:44:42.883855Z", "deleted_at": null, "name": "issue property option 3", "sort_order": 10000.0, "description": "issue property option 3 description", "logo_props": {}, "is_active": true, "is_default": false, "external_source": null, "external_id": null, "created_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "updated_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "workspace": "70b6599f-9313-4c0d-b5c0-406a13a05647", "project_ids": ["03a9bf56-84f4-4afe-b232-9400eb9b7b6b"], "property": "f962febb-98bc-43ca-8bfb-8012e4d54dae", "parent": null } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/values/add-property-values.html description: >- Add custom property values via Plane API. HTTP request format, parameters, scopes, and example responses for add custom property values. --- # Add custom property values Create or update the property value for a work item. Acts as an upsert operation since only one value is allowed per work item/property combination. ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Body Parameters The value to set for the property. Type depends on property type: string for text/url/email/file fields, string (UUID) or list of UUIDs for relations/options (list only when is\_multi=True), string (YYYY-MM-DD) for dates, number for decimals, boolean for booleans Optional external identifier for syncing with external systems Optional external source identifier (e.g., 'github', 'jira') ### Scopes `projects.work_item_property_values:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "value": "example text value", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", headers={"X-API-Key": "your-api-key"}, json={ "value": "example text value", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ value: "example text value", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "property_id": "550e8400-e29b-41d4-a716-446655440000", "issue_id": "550e8400-e29b-41d4-a716-446655440000", "value": "Example Name", "value_type": "Example Name", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/values/list-property-values.html description: >- List all custom property values via Plane API. HTTP request format, parameters, scopes, and example responses for list all custom property values. --- # List all custom property values Retrieve the property value(s) for a specific work item property. Returns a single value for non-multi properties, or a list for multi-value properties. ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Scopes `projects.work_item_property_values:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "property_id": "550e8400-e29b-41d4-a716-446655440000", "issue_id": "550e8400-e29b-41d4-a716-446655440000", "value": "Example Name", "value_type": "Example Name", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/values/get-property-value-detail.html description: >- Get property value via Plane API. HTTP request format, parameters, scopes, and example responses for get property value. --- # Get property value Retrieve the property value(s) for a specific work item property. Returns a single value for non-multi properties, or a list for multi-value properties. ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Scopes `projects.work_item_property_values:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "property_id": "550e8400-e29b-41d4-a716-446655440000", "issue_id": "550e8400-e29b-41d4-a716-446655440000", "value": "Example Name", "value_type": "Example Name", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/values/update-property-value.html description: >- Update property value via Plane API. HTTP request format, parameters, scopes, and example responses for update property value. --- # Update property value Update an existing property value for a work item (partial update) ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Body Parameters The value to set for the property. Type depends on property type: string for text/url/email/file fields, string (UUID) or list of UUIDs for relations/options (list only when is\_multi=True), string (YYYY-MM-DD) for dates, number for decimals, boolean for booleans Optional external identifier for syncing with external systems Optional external source identifier (e.g., 'github', 'jira') ### Scopes `projects.work_item_property_values:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "value": "updated text value" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", headers={"X-API-Key": "your-api-key"}, json={ "value": "updated text value" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ value: "updated text value", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "property_id": "550e8400-e29b-41d4-a716-446655440000", "issue_id": "550e8400-e29b-41d4-a716-446655440000", "value": "Example Name", "value_type": "Example Name", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/values/delete-property-value.html description: >- Delete property value via Plane API. HTTP request format, parameters, scopes, and example responses for delete property value. --- # Delete property value Delete the property value(s) for a work item. For multi-value properties, deletes all values. ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Scopes `projects.work_item_property_values:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/work-item-properties/property-uuid/values/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/issue-types/options/overview.html' description: >- Plane Options API overview. Learn about endpoints, request/response format, and how to work with options via REST API. --- # Overview Custom property options define the available choices for dropdown-style custom properties on work items. ## The Options Object ### Attributes * `workspace` *uuid* The workspace which the issue is part of auto generated from backend * `project` *uuid* The project which the issue is part of auto generated from backend * `created_at` , `updated_at` timestamp Timestamp of the issue when it was created and when it was last updated. * `created_by` & `updated_by` This values are auto saved and represent the id of the user that created or the updated the project. * `external_id` & `external_source` This values are auto saved and represent the id of the user that created or the updated the project. ```json { "id": "51a869d1-f612-4315-ac91-ffef3e96c20e", "created_at": "2024-10-23T07:44:42.883820Z", "updated_at": "2024-10-23T07:44:42.883855Z", "deleted_at": null, "name": "issue property option 3", "sort_order": 10000.0, "description": "issue property option 3 description", "logo_props": {}, "is_active": true, "is_default": false, "external_source": null, "external_id": null, "created_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "updated_by": "9d6d1ecd-bf73-4169-80c8-7dee79b217f4", "workspace": "70b6599f-9313-4c0d-b5c0-406a13a05647", "project": "03a9bf56-84f4-4afe-b232-9400eb9b7b6b", "property": "f962febb-98bc-43ca-8bfb-8012e4d54dae", "parent": null } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/options/add-dropdown-options.html description: >- Add dropdown options via Plane API. HTTP request format, parameters, scopes, and example responses for add dropdown options. --- # Add dropdown options Create a new issue property option ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Is active. Is default. External source. External id. Parent. ### Scopes `projects.work_item_property_options:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "sort_order": 1, "logo_props": "example-value", "is_active": true, "is_default": true, "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/options/list-dropdown-options.html description: >- List all dropdown options via Plane API. HTTP request format, parameters, scopes, and example responses for list all dropdown options. --- # List all dropdown options List issue property options ### Path Parameters The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_item_property_options:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "sort_order": 1, "logo_props": "example-value", "is_active": true, "is_default": true, "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000" } ] ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/options/get-option-details.html description: >- Retrieve option details via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve option details. --- # Retrieve option details Get issue property option by id ### Path Parameters The unique identifier of the option. The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_item_property_options:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "sort_order": 1, "logo_props": "example-value", "is_active": true, "is_default": true, "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/options/update-dropdown-options.html description: >- Update dropdown options via Plane API. HTTP request format, parameters, scopes, and example responses for update dropdown options. --- # Update dropdown options Update an issue property option ### Path Parameters The unique identifier of the option. The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Is active. Is default. External source. External id. Parent. ### Scopes `projects.work_item_property_options:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "Example description", "deleted_at": "2024-01-01T00:00:00Z", "sort_order": 1, "logo_props": "example-value", "is_active": true, "is_default": true, "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-types/options/delete-dropdown-options.html description: >- Delete dropdown options via Plane API. HTTP request format, parameters, scopes, and example responses for delete dropdown options. --- # Delete dropdown options Delete an issue property option ### Path Parameters The unique identifier of the option. The unique identifier of the project. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_item_property_options:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-item-properties/property-uuid/options/option-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/link/overview.html' description: >- Plane Link API overview. Learn about endpoints, request/response format, and how to work with link via REST API. --- # Overview Links attach external resources to work items, allowing you to reference documentation, designs, or other relevant URLs. [Learn more about Links](https://docs.plane.so/core-concepts/work-items/overview#add-links-and-attachments) ## The Link Object ### Attributes * `title` *string* Title of the url * `url` *url* **(required)** Url of the external link * `metadata` *json* Metadata from the resource * `created_at` , `updated_at` *timestamp* Timestamp of the issue when it was created and when it was last updated. * `created_by` & `updated_by` This values are auto saved and represent the id of the user that created or the updated the project. * `project` *uuid* The project which the issue is part of auto generated from backend * `workspace` *uuid* The workspace which the issue is part of auto generated from backend * `issue` *uuid* The issue which the link is attached to ```json { "id": "662dd6b2-2b01-4315-955f-480eb51baa14", "created_at": "2023-11-20T06:23:10.270664Z", "updated_at": "2023-11-20T06:23:10.270689Z", "title": "Plane Website", "url": "https://plane.so", "metadata": {}, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "issue": "e1c25c66-5bb8-465e-a818-92a483423443" } ``` --- --- url: 'https://developers.plane.so/api-reference/link/add-link.html' description: >- Create a link via Plane API. HTTP request format, parameters, scopes, and example responses for create a link. --- # Create a link Add a new external link to a work item with URL, title, and metadata. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Title. Url. ### Scopes `projects.work_items.links:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/resource", "title": "Example Name" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/", headers={"X-API-Key": "your-api-key"}, json={ "url": "https://example.com/resource", "title": "Example Name" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://example.com/resource", title: "Example Name", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/resource", "title": "Example Name", "metadata": { "title": "Example Name", "description": "Example description", "image": "https://example.com/assets/example-image.png" }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/link/list-links.html' description: >- List all links via Plane API. HTTP request format, parameters, scopes, and example responses for list all links. --- # List all links Retrieve all links associated with a work item. Supports filtering by URL, title, and metadata. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.work_items.links:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/link/get-link-detail.html' description: >- Retrieve a link via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a link. --- # Retrieve a link Retrieve details of a specific work item link. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Number of results per page (default: 20, max: 100) ### Scopes `projects.work_items.links:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/link/update-link-detail.html' description: >- Update a link via Plane API. HTTP request format, parameters, scopes, and example responses for update a link. --- # Update a link Modify the URL, title, or metadata of an existing issue link. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Title. Url. ### Scopes `projects.work_items.links:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/resource", "title": "Example Name" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "url": "https://example.com/resource", "title": "Example Name" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://example.com/resource", title: "Example Name", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/resource", "title": "Example Name", "metadata": { "title": "Example Name", "description": "Example description", "image": "https://example.com/assets/example-image.png" }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/link/delete-link.html' description: >- Delete a link via Plane API. HTTP request format, parameters, scopes, and example responses for delete a link. --- # Delete a link Permanently remove an external link from a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.links:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/links/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/issue-activity/overview.html' description: >- Plane Issue-Activity API overview. Learn about endpoints, request/response format, and how to work with issue-activity via REST API. --- # Overview Work item activity represents the history of all changes made to a work item, including property changes, comments, and other modifications. ## The Activity Object ### Attributes * `id` *uuid* Unique identifier for the activity * `created_at` *timestamp* The timestamp of the time when the activity was created * `updated_at` *timestamp* The timestamp of the time when the activity was last updated * `deleted_at` *timestamp* or *null* The timestamp when the activity was deleted (if deleted) * `verb` *string* created or updated * `field` *string* or *null* The field that got changed null when created * `old_value` *string* Old value of the field * `new_value` *string* New value of the field * `comment` *string* Comment auto generated * `attachments` - *\[url,]* Url of all the attachments that are in the activity * `old_identifier` *uuid* Old identifier of the field * `new_identifier` *uuid* New identifier of the field * `epoch` *float* Epoch float field when the activity was created. * `project` uuid It contains projects uuid which is automatically saved. * `workspace` uuid It contains workspace uuid which is automatically saved * `issue` *uuid* The work item the activity is attached to * `issue_comment` *uuid or null* The comment uuid if the activity was created due to a comment * `actor` uuid The actor who triggered this activity ```json { "id": "35612f5b-3eff-4130-b91c-c976ff887a20", "created_at": "2023-11-19T11:56:55.452555Z", "updated_at": "2023-11-19T11:56:55.452561Z", "verb": "created", "field": null, "old_value": null, "new_value": null, "comment": "created the work item", "attachments": [], "old_identifier": null, "new_identifier": null, "epoch": 1700395015.0, "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "issue": "e1c25c66-5bb8-465e-a818-92a483423443", "issue_comment": null, "actor": "16c61a3a-512a-48ac-b0be-b6b46fe6f430" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-activity/list-issue-activities.html description: >- List all work item activity via Plane API. HTTP request format, parameters, scopes, and example responses for list all work item activity. --- # List all work item activity Retrieve all activities for a work item. Supports filtering by activity type and date range. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.work_items.activities:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/activities/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/activities/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/activities/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-activity/get-issue-activity-detail.html description: >- Retrieve a work item activity via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a work item activity. --- # Retrieve a work item activity Retrieve details of a specific activity. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.work_items.activities:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/activities/resource-id-uuid/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/activities/resource-id-uuid/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/activities/resource-id-uuid/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/issue-comment/overview.html' description: >- Plane Issue-Comment API overview. Learn about endpoints, request/response format, and how to work with issue-comment via REST API. --- # Overview Comments allow team members to discuss and collaborate on work items by adding text, mentions, and attachments. [Learn more about Work Item Comments](https://docs.plane.so/core-concepts/issues/overview#comment-on-work-items) ## The Comments Object ### Attributes * `id` *uuid* Unique identifier for the comment * `created_at` *timestamp* The timestamp when the comment was created * `updated_at` *timestamp* The timestamp when the comment was last updated * `deleted_at` *timestamp* or *null* The timestamp when the comment was deleted (if deleted) * `edited_at` *timestamp* or *null* The timestamp when the comment was last edited * `comment_html` *string* HTML string version of the comment * `comment_stripped` *string* Stripped string version of the comment * `comment_json` *object* JSON object version of the comment * `attachments` *string\[]* Array of attachment URLs * `access` *string* If the comment should be visible externally also if the project is published or not. Takes in two values * INTERNAL * EXTERNAL * `external_source` *string* External source identifier * `external_id` *string* External ID from the external source * `is_member` *boolean* Whether the current user is a member of the project * `created_by` , `updated_by` *uuid* These values are auto saved and represent the id of the user that created or updated the comment * `project` uuid It contains project uuid which is automatically saved. * `workspace` uuid It contains workspace uuid which is automatically saved * `issue` *uuid* The work item the comment is attached to * `actor` *uuid* UUID of the user who commented. ```json { "id": "f3e29f26-708d-40f0-9209-7e0de44abc49", "created_at": "2023-11-20T09:26:10.383129Z", "updated_at": "2023-11-20T09:26:10.383140Z", "comment_stripped": "Initialf ThoughtsaMy initial thoughts on this are very good", "comment_json": {}, "comment_html": "

Initialf Thoughts

a

My initial thoughts on this are very good

", "attachments": [], "access": "INTERNAL", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "issue": "e1c25c66-5bb8-465e-a818-92a483423443", "actor": "16c61a3a-512a-48ac-b0be-b6b46fe6f430" } ``` --- --- url: 'https://developers.plane.so/api-reference/issue-comment/add-issue-comment.html' description: >- Create a work item comment via Plane API. HTTP request format, parameters, scopes, and example responses for create a work item comment. --- # Create a work item comment Add a new comment to a work item with HTML content. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Comment json. Comment html. * `INTERNAL` - INTERNAL * `EXTERNAL` - EXTERNAL External source. External id. Parent. ### Scopes `projects.work_items.comments:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "comment_html": "

Example content

", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/", headers={"X-API-Key": "your-api-key"}, json={ "comment_html": "

Example content

", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ comment_html: "

Example content

", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "comment_html": "

Example content

", "comment_json": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "This issue has been resolved by implementing OAuth 2.0 flow." } ] } ] }, "actor": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "display_name": "Example Name", "avatar": "https://example.com/assets/example-image.png" }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-comment/list-issue-comments.html description: >- List all work item comments via Plane API. HTTP request format, parameters, scopes, and example responses for list all work item comments. --- # List all work item comments Retrieve all comments for a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.work_items.comments:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-comment/get-issue-comment-detail.html description: >- Retrieve a work item comment via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a work item comment. --- # Retrieve a work item comment Retrieve details of a specific comment. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.comments:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "comment_html": "

Example content

", "comment_json": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "This issue has been resolved by implementing OAuth 2.0 flow." } ] } ] }, "actor": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "display_name": "Example Name", "avatar": "https://example.com/assets/example-image.png" }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-comment/update-issue-comment-detail.html description: >- Update a work item comment via Plane API. HTTP request format, parameters, scopes, and example responses for update a work item comment. --- # Update a work item comment Modify the content of an existing comment on a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Comment json. Comment html. * `INTERNAL` - INTERNAL * `EXTERNAL` - EXTERNAL External source. External id. Parent. ### Scopes `projects.work_items.comments:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "comment_html": "

Example content

", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "comment_html": "

Example content

", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ comment_html: "

Example content

", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "comment_html": "

Example content

", "comment_json": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "This issue has been resolved by implementing OAuth 2.0 flow." } ] } ] }, "actor": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "display_name": "Example Name", "avatar": "https://example.com/assets/example-image.png" }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-comment/delete-issue-comment.html description: >- Delete a work item comment via Plane API. HTTP request format, parameters, scopes, and example responses for delete a work item comment. --- # Delete a work item comment Permanently remove a comment from a work item. Records deletion activity for audit purposes. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.comments:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/comments/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/issue-attachments/overview.html' description: >- Plane Issue-Attachments API overview. Learn about endpoints, request/response format, and how to work with issue-attachments via REST API. --- # Overview Allows you to manage file attachments associated with work items and Intake work items. You can upload new attachments and retrieve existing attachments for a specific work item. [Learn more about Attachments](https://docs.plane.so/core-concepts/issues/overview#add-links-and-attachments) ## Upload process 1. Get the [upload credentials](/api-reference/issue-attachments/get-upload-credentials). 2. [Upload the file](/api-reference/issue-attachments/upload-file) to storage. 3. [Complete attachment upload](/api-reference/issue-attachments/complete-upload) to notify server. ## The Attachment Object ### Attributes * `id` *string* Unique identifier for the attachment * `created_at` , `updated_at`, `deleted_at` *timestamp* Timestamp when the attachment was created, when it was last modified or deleted * `attributes` *object* Contains file metadata: * `name` *string* Original filename of the attachment * `size` *integer* File size in bytes * `type` *string* MIME type of the file * `asset` *string* Storage path/identifier for the attachment file * `entity_type` *string* Always `ISSUE_ATTACHMENT` for work item attachments * `entity_identifier` *string* Entity identifier for the attachment * `is_deleted` *boolean* Whether the attachment has been deleted * `is_archived` *boolean* Whether the attachment has been archived * `external_id` *string* or *null* External identifier if the issue and its attachments are imported to Plane * `external_source` *string* or *null* Name of the source if the issue and its attachments are imported to Plane * `size` *integer* File size in bytes * `is_uploaded` *boolean* Whether the file has been successfully uploaded * `storage_metadata` *object* Cloud storage metadata: * `ETag` *string* Storage provider's entity tag * `Metadata` *object* Additional storage metadata * `ContentType` *object* MIME type of stored file * `LastModified` *timestamp* Last modification time in storage * `ContentLength` *integer* File size in bytes * `created_by` *string* ID of user who created the attachment * `updated_by` *string* ID of user who last modified the attachment * `deleted_by` *string* ID of user who deleted the attachment * `workspace` *string* ID of workspace containing the attachment * `project` *string* ID of project containing the work item * `issue` *string* ID of work item containing the attachment * `user` *string* ID of user associated with the attachment * `draft_issue` *string* ID of draft work item if applicable * `comment` *string* ID of comment if attachment is associated with a comment * `page` *string* ID of page if attachment is associated with a page ```json { "id": "8caf3ed5-4f57-9674-76c4fce146b2", "created_at": "2024-10-30T09:32:32.815273Z", "updated_at": "2024-10-30T09:32:35.533136Z", "deleted_at": null, "attributes": { "name": "plane-logo.png", "size": 135686, "type": "image/png" }, "asset": "9b8aab8a-9052-fc735350abe8/6893d862ecb740d4b7f9f6542cda539c-plane.png", "entity_type": "ISSUE_ATTACHMENT", "is_deleted": false, "is_archived": false, "external_id": null, "external_source": null, "size": 135686.0, "is_uploaded": true, "storage_metadata": { "ETag": "\"72d0d4be99999fe60c2fbc08c8b\"", "Metadata": {}, "ContentType": "image/png", "LastModified": "2024-10-30T09:32:34+00:00", "ContentLength": 135686 }, "created_by": "575de6bf-e120-43bb-9f6a-eae276210575", "updated_by": "575de6bf-e120-43bb-9f6a-eae276210575", "workspace": "9b8aab8a-9s6a-99ac-fc735350abe8", "project": "1790bd-5262-42fb-ac55-568c19a5", "issue": "7ba090-7702-4e26-a61e-aa6b866f7" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-attachments/get-attachments.html description: >- List all attachments via Plane API. HTTP request format, parameters, scopes, and example responses for list all attachments. --- # List all attachments Retrieve all attachments for a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.attachments:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "size": 1024000, "asset_url": "https://example.com/resource", "attributes": { "name": "Example Name", "type": "image/png", "size": 1024000 }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-attachments/get-attachment-detail.html description: >- Retrieve an attachment via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve an attachment. --- # Retrieve an attachment Download attachment file. Returns a redirect to the presigned download URL. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.attachments:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "detail": "Authentication credentials were not provided or are invalid." } ``` --- --- url: >- https://developers.plane.so/api-reference/issue-attachments/get-upload-credentials.html description: >- Get upload credentials via Plane API. HTTP request format, parameters, scopes, and example responses for get upload credentials. --- # Get upload credentials Generate presigned URL for uploading file attachments to a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Original filename of the asset MIME type of the file File size in bytes External identifier for the asset (for integration tracking) External source system (for integration tracking) ### Scopes `projects.work_items.attachments:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "type": "application/pdf", "size": 1024000, "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "type": "application/pdf", "size": 1024000, "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", type: "application/pdf", size: 1024000, external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "detail": "Presigned download URL generated successfully" } ``` --- --- url: 'https://developers.plane.so/api-reference/issue-attachments/upload-file.html' description: >- Upload a file to the presigned storage URL returned by Plane. Includes the required multipart form fields for attachment uploads. --- # Upload file Use the presigned form fields returned by the attachment upload-credentials endpoint to upload the binary file directly to object storage. ### Body Parameters MIME type of the file being uploaded. Storage key returned by Plane for this upload. Base64-encoded upload policy returned by Plane. AWS signature returned by Plane. Binary file contents to upload. ```bash curl -X POST \ "https://planefs-uploads.s3.amazonaws.com/" \ -F "Content-Type=image/png" \ -F "key=attachments/550e8400-e29b-41d4-a716-446655440000/example-image.png" \ -F "policy=example-policy" \ -F "x-amz-signature=example-signature" \ -F "file=@./example-image.png" ``` ```python import requests with open("example-image.png", "rb") as file_handle: response = requests.post( "https://planefs-uploads.s3.amazonaws.com/", files={"file": file_handle}, data={ "Content-Type": "image/png", "key": "attachments/550e8400-e29b-41d4-a716-446655440000/example-image.png", "policy": "example-policy", "x-amz-signature": "example-signature", }, ) print(response.status_code) ``` ```javascript const formData = new FormData(); formData.append("Content-Type", "image/png"); formData.append("key", "attachments/550e8400-e29b-41d4-a716-446655440000/example-image.png"); formData.append("policy", "example-policy"); formData.append("x-amz-signature", "example-signature"); formData.append("file", fileInput.files[0]); const response = await fetch("https://planefs-uploads.s3.amazonaws.com/", { method: "POST", body: formData, }); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/issue-attachments/complete-upload.html description: >- Complete upload via Plane API. HTTP request format, parameters, scopes, and example responses for complete upload. --- # Complete upload Mark an attachment as uploaded after successful file transfer to storage. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Mark attachment as uploaded ### Scopes `projects.work_items.attachments:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "is_uploaded": true }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "is_uploaded": true } ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ is_uploaded: true, }), }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/issue-attachments/update-attachment.html description: >- Update an attachment via Plane API. HTTP request format, parameters, scopes, and example responses for update an attachment. --- # Update an attachment Mark an attachment as uploaded after successful file transfer to storage. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Mark attachment as uploaded ### Scopes `projects.work_items.attachments:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "is_uploaded": true }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "is_uploaded": true } ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ is_uploaded: true, }), }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/issue-attachments/delete-attachment.html description: >- Delete an attachment via Plane API. HTTP request format, parameters, scopes, and example responses for delete an attachment. --- # Delete an attachment Permanently remove an attachment from a work item. Records deletion activity for audit purposes. ### Path Parameters The unique identifier of the work item. The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.attachments:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/attachments/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/work-item-pages/overview.html' description: >- Plane Work Item Pages API overview. Learn how to link pages to work items using the Plane API. --- # Overview Work item pages create links between work items and wiki pages so related documentation stays close to execution. [Learn more about Work Items](https://docs.plane.so/core-concepts/issues) ## The Work Item Page Link Object ### Attributes * `id` *string* Id. * `page` *object* Lightweight page serializer for work item page links. Provides essential page information including identifiers, name, timestamps, and visual properties for work item page associations. * `issue` *string* Issue. * `project` *string* Project. * `workspace` *string* Workspace. * `created_at` *string* Created at. * `updated_at` *string* Updated at. * `created_by` *string* Created by. ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "page": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description_html": "

Example content

", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "is_global": false, "logo_props": {} }, "issue": "550e8400-e29b-41d4-a716-446655440000", "project": "550e8400-e29b-41d4-a716-446655440000", "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/work-item-pages/add-work-item-page.html description: >- Create work item page link via Plane API. HTTP request format, parameters, scopes, and example responses for create work item page link. --- # Create work item page link Link a page to a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Body Parameters ID of the page to link to the work item ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "page_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/", headers={"X-API-Key": "your-api-key"}, json={ "page_id": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ page_id: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "page": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description_html": "

Example content

", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "is_global": false, "logo_props": {} }, "issue": "550e8400-e29b-41d4-a716-446655440000", "project": "550e8400-e29b-41d4-a716-446655440000", "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/work-item-pages/list-work-item-pages.html description: >- List work item pages via Plane API. HTTP request format, parameters, scopes, and example responses for list work item pages. --- # List work item pages Retrieve all page links associated with a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Query Parameters Pagination cursor for getting next set of results Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/?cursor=20:1:0&order_by=-created_at" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/?cursor=20:1:0&order_by=-created_at", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/?cursor=20:1:0&order_by=-created_at", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/work-item-pages/get-work-item-page-detail.html description: >- Get work item page link via Plane API. HTTP request format, parameters, scopes, and example responses for get work item page link. --- # Get work item page link Retrieve details of a specific page link for a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the page. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/page-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/page-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/page-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "page": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description_html": "

Example content

", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "is_global": false, "logo_props": {} }, "issue": "550e8400-e29b-41d4-a716-446655440000", "project": "550e8400-e29b-41d4-a716-446655440000", "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/work-item-pages/delete-work-item-page.html description: >- Delete work item page link via Plane API. HTTP request format, parameters, scopes, and example responses for delete work item page link. --- # Delete work item page link Remove a page link from a work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the page. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the work item. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/page-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/page-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/pages/page-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/cycle/overview.html' description: >- Plane Cycle API overview. Learn about endpoints, request/response format, and how to work with cycle via REST API. --- # Overview Cycles are custom time periods in which a team works to complete items from their backlog. At the end of a cycle, the team typically has a new version of their project or product ready. [Learn more about Cycles](https://docs.plane.so/core-concepts/cycles) ## The Cycles Object ### Attributes * `name` string (required) Name of the cycle * `description` string Description of the cycle * `start_date` date Start date of the cycle * `end_date` date End date of the cycle * `created_at` *timestamp* The timestamp of the time when the project was created * `updated_at` *timestamp* The timestamp of the time when the project was last updated * `view_props` It store the filters and the display properties selected by the user to visualize the issues in the module * `sort_order` It gives the position of the module at which it should be displayed * `created_by` , `updated_by` *uuid* These values are auto saved and represent the id of the user that created or the updated the * `Project` uuid It contains projects uuid which is automatically saved. * `Workspace` uuid It contains workspace uuid which is automatically saved * `owned_by` uuid The user ID of the cycle owner * `archived_at` *timestamp* The timestamp when the cycle was archived (if archived) * `timezone` string The timezone for the cycle * `version` number Version number of the cycle ```json { "id": "50ebc791-65e4-4b4d-a164-3b4e529e55a5", "created_at": "2023-11-19T12:18:14.900078Z", "updated_at": "2023-11-19T12:18:14.900088Z", "name": "cycle testing", "description": "", "start_date": null, "end_date": null, "view_props": {}, "sort_order": 35535.0, "created_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "updated_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "project": "6436c4ae-fba7-45dc-ad4a-5440e17cb1b2", "workspace": "c467e125-59e3-44ec-b5ee-f9c1e138c611", "owned_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c" } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/add-cycle.html' description: >- Create a cycle via Plane API. HTTP request format, parameters, scopes, and example responses for create a cycle. --- # Create a cycle Create a new development cycle with specified name, description, and date range. Supports external ID tracking for integration purposes. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Start date. End date. User who owns the cycle. If not provided, defaults to the current user. External source. External id. * `Africa/Abidjan` - Africa/Abidjan * `Africa/Accra` - Africa/Accra * `Africa/Addis_Ababa` - Africa/Addis\_Ababa * `Africa/Algiers` - Africa/Algiers * `Africa/Asmara` - Africa/Asmara * `Africa/Bamako` - Africa/Bamako * `Africa/Bangui` - Africa/Bangui * `Africa/Banjul` - Africa/Banjul * `Africa/Bissau` - Africa/Bissau * `Africa/Blantyre` - Africa/Blantyre * `Africa/Brazzaville` - Africa/Brazzaville * `Africa/Bujumbura` - Africa/Bujumbura * `Africa/Cairo` - Africa/Cairo * `Africa/Casablanca` - Africa/Casablanca * `Africa/Ceuta` - Africa/Ceuta * `Africa/Conakry` - Africa/Conakry * `Africa/Dakar` - Africa/Dakar * `Africa/Dar_es_Salaam` - Africa/Dar\_es\_Salaam * `Africa/Djibouti` - Africa/Djibouti * `Africa/Douala` - Africa/Douala * `Africa/El_Aaiun` - Africa/El\_Aaiun * `Africa/Freetown` - Africa/Freetown * `Africa/Gaborone` - Africa/Gaborone * `Africa/Harare` - Africa/Harare * `Africa/Johannesburg` - Africa/Johannesburg * `Africa/Juba` - Africa/Juba * `Africa/Kampala` - Africa/Kampala * `Africa/Khartoum` - Africa/Khartoum * `Africa/Kigali` - Africa/Kigali * `Africa/Kinshasa` - Africa/Kinshasa * `Africa/Lagos` - Africa/Lagos * `Africa/Libreville` - Africa/Libreville * `Africa/Lome` - Africa/Lome * `Africa/Luanda` - Africa/Luanda * `Africa/Lubumbashi` - Africa/Lubumbashi * `Africa/Lusaka` - Africa/Lusaka * `Africa/Malabo` - Africa/Malabo * `Africa/Maputo` - Africa/Maputo * `Africa/Maseru` - Africa/Maseru * `Africa/Mbabane` - Africa/Mbabane * `Africa/Mogadishu` - Africa/Mogadishu * `Africa/Monrovia` - Africa/Monrovia * `Africa/Nairobi` - Africa/Nairobi * `Africa/Ndjamena` - Africa/Ndjamena * `Africa/Niamey` - Africa/Niamey * `Africa/Nouakchott` - Africa/Nouakchott * `Africa/Ouagadougou` - Africa/Ouagadougou * `Africa/Porto-Novo` - Africa/Porto-Novo * `Africa/Sao_Tome` - Africa/Sao\_Tome * `Africa/Tripoli` - Africa/Tripoli * `Africa/Tunis` - Africa/Tunis * `Africa/Windhoek` - Africa/Windhoek * `America/Adak` - America/Adak * `America/Anchorage` - America/Anchorage * `America/Anguilla` - America/Anguilla * `America/Antigua` - America/Antigua * `America/Araguaina` - America/Araguaina * `America/Argentina/Buenos_Aires` - America/Argentina/Buenos\_Aires * `America/Argentina/Catamarca` - America/Argentina/Catamarca * `America/Argentina/Cordoba` - America/Argentina/Cordoba * `America/Argentina/Jujuy` - America/Argentina/Jujuy * `America/Argentina/La_Rioja` - America/Argentina/La\_Rioja * `America/Argentina/Mendoza` - America/Argentina/Mendoza * `America/Argentina/Rio_Gallegos` - America/Argentina/Rio\_Gallegos * `America/Argentina/Salta` - America/Argentina/Salta * `America/Argentina/San_Juan` - America/Argentina/San\_Juan * `America/Argentina/San_Luis` - America/Argentina/San\_Luis * `America/Argentina/Tucuman` - America/Argentina/Tucuman * `America/Argentina/Ushuaia` - America/Argentina/Ushuaia * `America/Aruba` - America/Aruba * `America/Asuncion` - America/Asuncion * `America/Atikokan` - America/Atikokan * `America/Bahia` - America/Bahia * `America/Bahia_Banderas` - America/Bahia\_Banderas * `America/Barbados` - America/Barbados * `America/Belem` - America/Belem * `America/Belize` - America/Belize * `America/Blanc-Sablon` - America/Blanc-Sablon * `America/Boa_Vista` - America/Boa\_Vista * `America/Bogota` - America/Bogota * `America/Boise` - America/Boise * `America/Cambridge_Bay` - America/Cambridge\_Bay * `America/Campo_Grande` - America/Campo\_Grande * `America/Cancun` - America/Cancun * `America/Caracas` - America/Caracas * `America/Cayenne` - America/Cayenne * `America/Cayman` - America/Cayman * `America/Chicago` - America/Chicago * `America/Chihuahua` - America/Chihuahua * `America/Ciudad_Juarez` - America/Ciudad\_Juarez * `America/Costa_Rica` - America/Costa\_Rica * `America/Creston` - America/Creston * `America/Cuiaba` - America/Cuiaba * `America/Curacao` - America/Curacao * `America/Danmarkshavn` - America/Danmarkshavn * `America/Dawson` - America/Dawson * `America/Dawson_Creek` - America/Dawson\_Creek * `America/Denver` - America/Denver * `America/Detroit` - America/Detroit * `America/Dominica` - America/Dominica * `America/Edmonton` - America/Edmonton * `America/Eirunepe` - America/Eirunepe * `America/El_Salvador` - America/El\_Salvador * `America/Fort_Nelson` - America/Fort\_Nelson * `America/Fortaleza` - America/Fortaleza * `America/Glace_Bay` - America/Glace\_Bay * `America/Goose_Bay` - America/Goose\_Bay * `America/Grand_Turk` - America/Grand\_Turk * `America/Grenada` - America/Grenada * `America/Guadeloupe` - America/Guadeloupe * `America/Guatemala` - America/Guatemala * `America/Guayaquil` - America/Guayaquil * `America/Guyana` - America/Guyana * `America/Halifax` - America/Halifax * `America/Havana` - America/Havana * `America/Hermosillo` - America/Hermosillo * `America/Indiana/Indianapolis` - America/Indiana/Indianapolis * `America/Indiana/Knox` - America/Indiana/Knox * `America/Indiana/Marengo` - America/Indiana/Marengo * `America/Indiana/Petersburg` - America/Indiana/Petersburg * `America/Indiana/Tell_City` - America/Indiana/Tell\_City * `America/Indiana/Vevay` - America/Indiana/Vevay * `America/Indiana/Vincennes` - America/Indiana/Vincennes * `America/Indiana/Winamac` - America/Indiana/Winamac * `America/Inuvik` - America/Inuvik * `America/Iqaluit` - America/Iqaluit * `America/Jamaica` - America/Jamaica * `America/Juneau` - America/Juneau * `America/Kentucky/Louisville` - America/Kentucky/Louisville * `America/Kentucky/Monticello` - America/Kentucky/Monticello * `America/Kralendijk` - America/Kralendijk * `America/La_Paz` - America/La\_Paz * `America/Lima` - America/Lima * `America/Los_Angeles` - America/Los\_Angeles * `America/Lower_Princes` - America/Lower\_Princes * `America/Maceio` - America/Maceio * `America/Managua` - America/Managua * `America/Manaus` - America/Manaus * `America/Marigot` - America/Marigot * `America/Martinique` - America/Martinique * `America/Matamoros` - America/Matamoros * `America/Mazatlan` - America/Mazatlan * `America/Menominee` - America/Menominee * `America/Merida` - America/Merida * `America/Metlakatla` - America/Metlakatla * `America/Mexico_City` - America/Mexico\_City * `America/Miquelon` - America/Miquelon * `America/Moncton` - America/Moncton * `America/Monterrey` - America/Monterrey * `America/Montevideo` - America/Montevideo * `America/Montserrat` - America/Montserrat * `America/Nassau` - America/Nassau * `America/New_York` - America/New\_York * `America/Nome` - America/Nome * `America/Noronha` - America/Noronha * `America/North_Dakota/Beulah` - America/North\_Dakota/Beulah * `America/North_Dakota/Center` - America/North\_Dakota/Center * `America/North_Dakota/New_Salem` - America/North\_Dakota/New\_Salem * `America/Nuuk` - America/Nuuk * `America/Ojinaga` - America/Ojinaga * `America/Panama` - America/Panama * `America/Paramaribo` - America/Paramaribo * `America/Phoenix` - America/Phoenix * `America/Port-au-Prince` - America/Port-au-Prince * `America/Port_of_Spain` - America/Port\_of\_Spain * `America/Porto_Velho` - America/Porto\_Velho * `America/Puerto_Rico` - America/Puerto\_Rico * `America/Punta_Arenas` - America/Punta\_Arenas * `America/Rankin_Inlet` - America/Rankin\_Inlet * `America/Recife` - America/Recife * `America/Regina` - America/Regina * `America/Resolute` - America/Resolute * `America/Rio_Branco` - America/Rio\_Branco * `America/Santarem` - America/Santarem * `America/Santiago` - America/Santiago * `America/Santo_Domingo` - America/Santo\_Domingo * `America/Sao_Paulo` - America/Sao\_Paulo * `America/Scoresbysund` - America/Scoresbysund * `America/Sitka` - America/Sitka * `America/St_Barthelemy` - America/St\_Barthelemy * `America/St_Johns` - America/St\_Johns * `America/St_Kitts` - America/St\_Kitts * `America/St_Lucia` - America/St\_Lucia * `America/St_Thomas` - America/St\_Thomas * `America/St_Vincent` - America/St\_Vincent * `America/Swift_Current` - America/Swift\_Current * `America/Tegucigalpa` - America/Tegucigalpa * `America/Thule` - America/Thule * `America/Tijuana` - America/Tijuana * `America/Toronto` - America/Toronto * `America/Tortola` - America/Tortola * `America/Vancouver` - America/Vancouver * `America/Whitehorse` - America/Whitehorse * `America/Winnipeg` - America/Winnipeg * `America/Yakutat` - America/Yakutat * `Antarctica/Casey` - Antarctica/Casey * `Antarctica/Davis` - Antarctica/Davis * `Antarctica/DumontDUrville` - Antarctica/DumontDUrville * `Antarctica/Macquarie` - Antarctica/Macquarie * `Antarctica/Mawson` - Antarctica/Mawson * `Antarctica/McMurdo` - Antarctica/McMurdo * `Antarctica/Palmer` - Antarctica/Palmer * `Antarctica/Rothera` - Antarctica/Rothera * `Antarctica/Syowa` - Antarctica/Syowa * `Antarctica/Troll` - Antarctica/Troll * `Antarctica/Vostok` - Antarctica/Vostok * `Arctic/Longyearbyen` - Arctic/Longyearbyen * `Asia/Aden` - Asia/Aden * `Asia/Almaty` - Asia/Almaty * `Asia/Amman` - Asia/Amman * `Asia/Anadyr` - Asia/Anadyr * `Asia/Aqtau` - Asia/Aqtau * `Asia/Aqtobe` - Asia/Aqtobe * `Asia/Ashgabat` - Asia/Ashgabat * `Asia/Atyrau` - Asia/Atyrau * `Asia/Baghdad` - Asia/Baghdad * `Asia/Bahrain` - Asia/Bahrain * `Asia/Baku` - Asia/Baku * `Asia/Bangkok` - Asia/Bangkok * `Asia/Barnaul` - Asia/Barnaul * `Asia/Beirut` - Asia/Beirut * `Asia/Bishkek` - Asia/Bishkek * `Asia/Brunei` - Asia/Brunei * `Asia/Chita` - Asia/Chita * `Asia/Choibalsan` - Asia/Choibalsan * `Asia/Colombo` - Asia/Colombo * `Asia/Damascus` - Asia/Damascus * `Asia/Dhaka` - Asia/Dhaka * `Asia/Dili` - Asia/Dili * `Asia/Dubai` - Asia/Dubai * `Asia/Dushanbe` - Asia/Dushanbe * `Asia/Famagusta` - Asia/Famagusta * `Asia/Gaza` - Asia/Gaza * `Asia/Hebron` - Asia/Hebron * `Asia/Ho_Chi_Minh` - Asia/Ho\_Chi\_Minh * `Asia/Hong_Kong` - Asia/Hong\_Kong * `Asia/Hovd` - Asia/Hovd * `Asia/Irkutsk` - Asia/Irkutsk * `Asia/Jakarta` - Asia/Jakarta * `Asia/Jayapura` - Asia/Jayapura * `Asia/Jerusalem` - Asia/Jerusalem * `Asia/Kabul` - Asia/Kabul * `Asia/Kamchatka` - Asia/Kamchatka * `Asia/Karachi` - Asia/Karachi * `Asia/Kathmandu` - Asia/Kathmandu * `Asia/Khandyga` - Asia/Khandyga * `Asia/Kolkata` - Asia/Kolkata * `Asia/Krasnoyarsk` - Asia/Krasnoyarsk * `Asia/Kuala_Lumpur` - Asia/Kuala\_Lumpur * `Asia/Kuching` - Asia/Kuching * `Asia/Kuwait` - Asia/Kuwait * `Asia/Macau` - Asia/Macau * `Asia/Magadan` - Asia/Magadan * `Asia/Makassar` - Asia/Makassar * `Asia/Manila` - Asia/Manila * `Asia/Muscat` - Asia/Muscat * `Asia/Nicosia` - Asia/Nicosia * `Asia/Novokuznetsk` - Asia/Novokuznetsk * `Asia/Novosibirsk` - Asia/Novosibirsk * `Asia/Omsk` - Asia/Omsk * `Asia/Oral` - Asia/Oral * `Asia/Phnom_Penh` - Asia/Phnom\_Penh * `Asia/Pontianak` - Asia/Pontianak * `Asia/Pyongyang` - Asia/Pyongyang * `Asia/Qatar` - Asia/Qatar * `Asia/Qostanay` - Asia/Qostanay * `Asia/Qyzylorda` - Asia/Qyzylorda * `Asia/Riyadh` - Asia/Riyadh * `Asia/Sakhalin` - Asia/Sakhalin * `Asia/Samarkand` - Asia/Samarkand * `Asia/Seoul` - Asia/Seoul * `Asia/Shanghai` - Asia/Shanghai * `Asia/Singapore` - Asia/Singapore * `Asia/Srednekolymsk` - Asia/Srednekolymsk * `Asia/Taipei` - Asia/Taipei * `Asia/Tashkent` - Asia/Tashkent * `Asia/Tbilisi` - Asia/Tbilisi * `Asia/Tehran` - Asia/Tehran * `Asia/Thimphu` - Asia/Thimphu * `Asia/Tokyo` - Asia/Tokyo * `Asia/Tomsk` - Asia/Tomsk * `Asia/Ulaanbaatar` - Asia/Ulaanbaatar * `Asia/Urumqi` - Asia/Urumqi * `Asia/Ust-Nera` - Asia/Ust-Nera * `Asia/Vientiane` - Asia/Vientiane * `Asia/Vladivostok` - Asia/Vladivostok * `Asia/Yakutsk` - Asia/Yakutsk * `Asia/Yangon` - Asia/Yangon * `Asia/Yekaterinburg` - Asia/Yekaterinburg * `Asia/Yerevan` - Asia/Yerevan * `Atlantic/Azores` - Atlantic/Azores * `Atlantic/Bermuda` - Atlantic/Bermuda * `Atlantic/Canary` - Atlantic/Canary * `Atlantic/Cape_Verde` - Atlantic/Cape\_Verde * `Atlantic/Faroe` - Atlantic/Faroe * `Atlantic/Madeira` - Atlantic/Madeira * `Atlantic/Reykjavik` - Atlantic/Reykjavik * `Atlantic/South_Georgia` - Atlantic/South\_Georgia * `Atlantic/St_Helena` - Atlantic/St\_Helena * `Atlantic/Stanley` - Atlantic/Stanley * `Australia/Adelaide` - Australia/Adelaide * `Australia/Brisbane` - Australia/Brisbane * `Australia/Broken_Hill` - Australia/Broken\_Hill * `Australia/Darwin` - Australia/Darwin * `Australia/Eucla` - Australia/Eucla * `Australia/Hobart` - Australia/Hobart * `Australia/Lindeman` - Australia/Lindeman * `Australia/Lord_Howe` - Australia/Lord\_Howe * `Australia/Melbourne` - Australia/Melbourne * `Australia/Perth` - Australia/Perth * `Australia/Sydney` - Australia/Sydney * `Canada/Atlantic` - Canada/Atlantic * `Canada/Central` - Canada/Central * `Canada/Eastern` - Canada/Eastern * `Canada/Mountain` - Canada/Mountain * `Canada/Newfoundland` - Canada/Newfoundland * `Canada/Pacific` - Canada/Pacific * `Europe/Amsterdam` - Europe/Amsterdam * `Europe/Andorra` - Europe/Andorra * `Europe/Astrakhan` - Europe/Astrakhan * `Europe/Athens` - Europe/Athens * `Europe/Belgrade` - Europe/Belgrade * `Europe/Berlin` - Europe/Berlin * `Europe/Bratislava` - Europe/Bratislava * `Europe/Brussels` - Europe/Brussels * `Europe/Bucharest` - Europe/Bucharest * `Europe/Budapest` - Europe/Budapest * `Europe/Busingen` - Europe/Busingen * `Europe/Chisinau` - Europe/Chisinau * `Europe/Copenhagen` - Europe/Copenhagen * `Europe/Dublin` - Europe/Dublin * `Europe/Gibraltar` - Europe/Gibraltar * `Europe/Guernsey` - Europe/Guernsey * `Europe/Helsinki` - Europe/Helsinki * `Europe/Isle_of_Man` - Europe/Isle\_of\_Man * `Europe/Istanbul` - Europe/Istanbul * `Europe/Jersey` - Europe/Jersey * `Europe/Kaliningrad` - Europe/Kaliningrad * `Europe/Kirov` - Europe/Kirov * `Europe/Kyiv` - Europe/Kyiv * `Europe/Lisbon` - Europe/Lisbon * `Europe/Ljubljana` - Europe/Ljubljana * `Europe/London` - Europe/London * `Europe/Luxembourg` - Europe/Luxembourg * `Europe/Madrid` - Europe/Madrid * `Europe/Malta` - Europe/Malta * `Europe/Mariehamn` - Europe/Mariehamn * `Europe/Minsk` - Europe/Minsk * `Europe/Monaco` - Europe/Monaco * `Europe/Moscow` - Europe/Moscow * `Europe/Oslo` - Europe/Oslo * `Europe/Paris` - Europe/Paris * `Europe/Podgorica` - Europe/Podgorica * `Europe/Prague` - Europe/Prague * `Europe/Riga` - Europe/Riga * `Europe/Rome` - Europe/Rome * `Europe/Samara` - Europe/Samara * `Europe/San_Marino` - Europe/San\_Marino * `Europe/Sarajevo` - Europe/Sarajevo * `Europe/Saratov` - Europe/Saratov * `Europe/Simferopol` - Europe/Simferopol * `Europe/Skopje` - Europe/Skopje * `Europe/Sofia` - Europe/Sofia * `Europe/Stockholm` - Europe/Stockholm * `Europe/Tallinn` - Europe/Tallinn * `Europe/Tirane` - Europe/Tirane * `Europe/Ulyanovsk` - Europe/Ulyanovsk * `Europe/Vaduz` - Europe/Vaduz * `Europe/Vatican` - Europe/Vatican * `Europe/Vienna` - Europe/Vienna * `Europe/Vilnius` - Europe/Vilnius * `Europe/Volgograd` - Europe/Volgograd * `Europe/Warsaw` - Europe/Warsaw * `Europe/Zagreb` - Europe/Zagreb * `Europe/Zurich` - Europe/Zurich * `GMT` - GMT * `Indian/Antananarivo` - Indian/Antananarivo * `Indian/Chagos` - Indian/Chagos * `Indian/Christmas` - Indian/Christmas * `Indian/Cocos` - Indian/Cocos * `Indian/Comoro` - Indian/Comoro * `Indian/Kerguelen` - Indian/Kerguelen * `Indian/Mahe` - Indian/Mahe * `Indian/Maldives` - Indian/Maldives * `Indian/Mauritius` - Indian/Mauritius * `Indian/Mayotte` - Indian/Mayotte * `Indian/Reunion` - Indian/Reunion * `Pacific/Apia` - Pacific/Apia * `Pacific/Auckland` - Pacific/Auckland * `Pacific/Bougainville` - Pacific/Bougainville * `Pacific/Chatham` - Pacific/Chatham * `Pacific/Chuuk` - Pacific/Chuuk * `Pacific/Easter` - Pacific/Easter * `Pacific/Efate` - Pacific/Efate * `Pacific/Fakaofo` - Pacific/Fakaofo * `Pacific/Fiji` - Pacific/Fiji * `Pacific/Funafuti` - Pacific/Funafuti * `Pacific/Galapagos` - Pacific/Galapagos * `Pacific/Gambier` - Pacific/Gambier * `Pacific/Guadalcanal` - Pacific/Guadalcanal * `Pacific/Guam` - Pacific/Guam * `Pacific/Honolulu` - Pacific/Honolulu * `Pacific/Kanton` - Pacific/Kanton * `Pacific/Kiritimati` - Pacific/Kiritimati * `Pacific/Kosrae` - Pacific/Kosrae * `Pacific/Kwajalein` - Pacific/Kwajalein * `Pacific/Majuro` - Pacific/Majuro * `Pacific/Marquesas` - Pacific/Marquesas * `Pacific/Midway` - Pacific/Midway * `Pacific/Nauru` - Pacific/Nauru * `Pacific/Niue` - Pacific/Niue * `Pacific/Norfolk` - Pacific/Norfolk * `Pacific/Noumea` - Pacific/Noumea * `Pacific/Pago_Pago` - Pacific/Pago\_Pago * `Pacific/Palau` - Pacific/Palau * `Pacific/Pitcairn` - Pacific/Pitcairn * `Pacific/Pohnpei` - Pacific/Pohnpei * `Pacific/Port_Moresby` - Pacific/Port\_Moresby * `Pacific/Rarotonga` - Pacific/Rarotonga * `Pacific/Saipan` - Pacific/Saipan * `Pacific/Tahiti` - Pacific/Tahiti * `Pacific/Tarawa` - Pacific/Tarawa * `Pacific/Tongatapu` - Pacific/Tongatapu * `Pacific/Wake` - Pacific/Wake * `Pacific/Wallis` - Pacific/Wallis * `US/Alaska` - US/Alaska * `US/Arizona` - US/Arizona * `US/Central` - US/Central * `US/Eastern` - US/Eastern * `US/Hawaii` - US/Hawaii * `US/Mountain` - US/Mountain * `US/Pacific` - US/Pacific * `UTC` - UTC ### Scopes `projects.cycles:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", start_date: "2024-01-01T00:00:00Z", end_date: "2024-01-01T00:00:00Z", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "status": "current", "total_issues": 15, "completed_issues": 8, "cancelled_issues": 1, "started_issues": 4, "unstarted_issues": 2, "backlog_issues": 0, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/add-cycle-work-items.html' description: >- Add work items to cycle via Plane API. HTTP request format, parameters, scopes, and example responses for add work items to cycle. --- # Add work items to cycle Assign multiple work items to a cycle. Automatically handles bulk creation and updates with activity tracking. ### Path Parameters The unique identifier of the cycle. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters List of issue IDs to add to the cycle ### Scopes `projects.cycles:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issues": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/", headers={"X-API-Key": "your-api-key"}, json={ "issues": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ issues: ["550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "cycle": "550e8400-e29b-41d4-a716-446655440000", "issue": "550e8400-e29b-41d4-a716-446655440000", "sub_issues_count": 3, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ] ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/transfer-cycle-work-items.html' description: >- Transfer cycle work items via Plane API. HTTP request format, parameters, scopes, and example responses for transfer cycle work items. --- # Transfer cycle work items Move incomplete work items from the current cycle to a new target cycle. Captures progress snapshot and transfers only unfinished work items. ### Path Parameters The unique identifier of the cycle. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters ID of the target cycle to transfer issues to ### Scopes `projects.cycles:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/transfer-issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "new_cycle_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/transfer-issues/", headers={"X-API-Key": "your-api-key"}, json={ "new_cycle_id": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/transfer-issues/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ new_cycle_id: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "message": "Success" } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/archive-cycle.html' description: >- Archive a cycle via Plane API. HTTP request format, parameters, scopes, and example responses for archive a cycle. --- # Archive a cycle Move a completed cycle to archived status for historical tracking. Only cycles that have ended can be archived. ### Path Parameters The unique identifier of the cycle. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.cycles:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/archive/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/archive/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/archive/", { method: "POST", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/cycle/list-cycles.html' description: >- List all cycles via Plane API. HTTP request format, parameters, scopes, and example responses for list all cycles. --- # List all cycles Retrieve all cycles in a project. Supports filtering by cycle status like current, upcoming, completed, or draft. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Filter cycles by status Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.cycles:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/?cursor=20:1:0&cycle_view=all" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/?cursor=20:1:0&cycle_view=all", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/?cursor=20:1:0&cycle_view=all", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "status": "current" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/get-cycle-detail.html' description: >- Retrieve a cycle via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a cycle. --- # Retrieve a cycle Retrieve details of a specific cycle by its ID. Supports cycle status filtering. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.cycles:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "status": "current", "total_issues": 15, "completed_issues": 8, "cancelled_issues": 1, "started_issues": 4, "unstarted_issues": 2, "backlog_issues": 0, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/list-cycle-work-items.html' description: >- List all work items in a cycle via Plane API. HTTP request format, parameters, scopes, and example responses for list all work items in a cycle. --- # List all work items in a cycle Retrieve all work items assigned to a cycle. ### Path Parameters The unique identifier of the cycle. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `projects.cycles:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "cycle": "550e8400-e29b-41d4-a716-446655440000", "issue": "550e8400-e29b-41d4-a716-446655440000", "sub_issues_count": 3, "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/list-archived-cycles.html' description: >- List all archived cycles via Plane API. HTTP request format, parameters, scopes, and example responses for list all archived cycles. --- # List all archived cycles Retrieve all cycles that have been archived in the project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `projects.cycles:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-cycles/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-cycles/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-cycles/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/update-cycle-detail.html' description: >- Update a cycle via Plane API. HTTP request format, parameters, scopes, and example responses for update a cycle. --- # Update a cycle Modify an existing cycle's properties like name, description, or date range. Completed cycles can only have their sort order changed. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Start date. End date. User who owns the cycle. If not provided, defaults to the current user. External source. External id. * `Africa/Abidjan` - Africa/Abidjan * `Africa/Accra` - Africa/Accra * `Africa/Addis_Ababa` - Africa/Addis\_Ababa * `Africa/Algiers` - Africa/Algiers * `Africa/Asmara` - Africa/Asmara * `Africa/Bamako` - Africa/Bamako * `Africa/Bangui` - Africa/Bangui * `Africa/Banjul` - Africa/Banjul * `Africa/Bissau` - Africa/Bissau * `Africa/Blantyre` - Africa/Blantyre * `Africa/Brazzaville` - Africa/Brazzaville * `Africa/Bujumbura` - Africa/Bujumbura * `Africa/Cairo` - Africa/Cairo * `Africa/Casablanca` - Africa/Casablanca * `Africa/Ceuta` - Africa/Ceuta * `Africa/Conakry` - Africa/Conakry * `Africa/Dakar` - Africa/Dakar * `Africa/Dar_es_Salaam` - Africa/Dar\_es\_Salaam * `Africa/Djibouti` - Africa/Djibouti * `Africa/Douala` - Africa/Douala * `Africa/El_Aaiun` - Africa/El\_Aaiun * `Africa/Freetown` - Africa/Freetown * `Africa/Gaborone` - Africa/Gaborone * `Africa/Harare` - Africa/Harare * `Africa/Johannesburg` - Africa/Johannesburg * `Africa/Juba` - Africa/Juba * `Africa/Kampala` - Africa/Kampala * `Africa/Khartoum` - Africa/Khartoum * `Africa/Kigali` - Africa/Kigali * `Africa/Kinshasa` - Africa/Kinshasa * `Africa/Lagos` - Africa/Lagos * `Africa/Libreville` - Africa/Libreville * `Africa/Lome` - Africa/Lome * `Africa/Luanda` - Africa/Luanda * `Africa/Lubumbashi` - Africa/Lubumbashi * `Africa/Lusaka` - Africa/Lusaka * `Africa/Malabo` - Africa/Malabo * `Africa/Maputo` - Africa/Maputo * `Africa/Maseru` - Africa/Maseru * `Africa/Mbabane` - Africa/Mbabane * `Africa/Mogadishu` - Africa/Mogadishu * `Africa/Monrovia` - Africa/Monrovia * `Africa/Nairobi` - Africa/Nairobi * `Africa/Ndjamena` - Africa/Ndjamena * `Africa/Niamey` - Africa/Niamey * `Africa/Nouakchott` - Africa/Nouakchott * `Africa/Ouagadougou` - Africa/Ouagadougou * `Africa/Porto-Novo` - Africa/Porto-Novo * `Africa/Sao_Tome` - Africa/Sao\_Tome * `Africa/Tripoli` - Africa/Tripoli * `Africa/Tunis` - Africa/Tunis * `Africa/Windhoek` - Africa/Windhoek * `America/Adak` - America/Adak * `America/Anchorage` - America/Anchorage * `America/Anguilla` - America/Anguilla * `America/Antigua` - America/Antigua * `America/Araguaina` - America/Araguaina * `America/Argentina/Buenos_Aires` - America/Argentina/Buenos\_Aires * `America/Argentina/Catamarca` - America/Argentina/Catamarca * `America/Argentina/Cordoba` - America/Argentina/Cordoba * `America/Argentina/Jujuy` - America/Argentina/Jujuy * `America/Argentina/La_Rioja` - America/Argentina/La\_Rioja * `America/Argentina/Mendoza` - America/Argentina/Mendoza * `America/Argentina/Rio_Gallegos` - America/Argentina/Rio\_Gallegos * `America/Argentina/Salta` - America/Argentina/Salta * `America/Argentina/San_Juan` - America/Argentina/San\_Juan * `America/Argentina/San_Luis` - America/Argentina/San\_Luis * `America/Argentina/Tucuman` - America/Argentina/Tucuman * `America/Argentina/Ushuaia` - America/Argentina/Ushuaia * `America/Aruba` - America/Aruba * `America/Asuncion` - America/Asuncion * `America/Atikokan` - America/Atikokan * `America/Bahia` - America/Bahia * `America/Bahia_Banderas` - America/Bahia\_Banderas * `America/Barbados` - America/Barbados * `America/Belem` - America/Belem * `America/Belize` - America/Belize * `America/Blanc-Sablon` - America/Blanc-Sablon * `America/Boa_Vista` - America/Boa\_Vista * `America/Bogota` - America/Bogota * `America/Boise` - America/Boise * `America/Cambridge_Bay` - America/Cambridge\_Bay * `America/Campo_Grande` - America/Campo\_Grande * `America/Cancun` - America/Cancun * `America/Caracas` - America/Caracas * `America/Cayenne` - America/Cayenne * `America/Cayman` - America/Cayman * `America/Chicago` - America/Chicago * `America/Chihuahua` - America/Chihuahua * `America/Ciudad_Juarez` - America/Ciudad\_Juarez * `America/Costa_Rica` - America/Costa\_Rica * `America/Creston` - America/Creston * `America/Cuiaba` - America/Cuiaba * `America/Curacao` - America/Curacao * `America/Danmarkshavn` - America/Danmarkshavn * `America/Dawson` - America/Dawson * `America/Dawson_Creek` - America/Dawson\_Creek * `America/Denver` - America/Denver * `America/Detroit` - America/Detroit * `America/Dominica` - America/Dominica * `America/Edmonton` - America/Edmonton * `America/Eirunepe` - America/Eirunepe * `America/El_Salvador` - America/El\_Salvador * `America/Fort_Nelson` - America/Fort\_Nelson * `America/Fortaleza` - America/Fortaleza * `America/Glace_Bay` - America/Glace\_Bay * `America/Goose_Bay` - America/Goose\_Bay * `America/Grand_Turk` - America/Grand\_Turk * `America/Grenada` - America/Grenada * `America/Guadeloupe` - America/Guadeloupe * `America/Guatemala` - America/Guatemala * `America/Guayaquil` - America/Guayaquil * `America/Guyana` - America/Guyana * `America/Halifax` - America/Halifax * `America/Havana` - America/Havana * `America/Hermosillo` - America/Hermosillo * `America/Indiana/Indianapolis` - America/Indiana/Indianapolis * `America/Indiana/Knox` - America/Indiana/Knox * `America/Indiana/Marengo` - America/Indiana/Marengo * `America/Indiana/Petersburg` - America/Indiana/Petersburg * `America/Indiana/Tell_City` - America/Indiana/Tell\_City * `America/Indiana/Vevay` - America/Indiana/Vevay * `America/Indiana/Vincennes` - America/Indiana/Vincennes * `America/Indiana/Winamac` - America/Indiana/Winamac * `America/Inuvik` - America/Inuvik * `America/Iqaluit` - America/Iqaluit * `America/Jamaica` - America/Jamaica * `America/Juneau` - America/Juneau * `America/Kentucky/Louisville` - America/Kentucky/Louisville * `America/Kentucky/Monticello` - America/Kentucky/Monticello * `America/Kralendijk` - America/Kralendijk * `America/La_Paz` - America/La\_Paz * `America/Lima` - America/Lima * `America/Los_Angeles` - America/Los\_Angeles * `America/Lower_Princes` - America/Lower\_Princes * `America/Maceio` - America/Maceio * `America/Managua` - America/Managua * `America/Manaus` - America/Manaus * `America/Marigot` - America/Marigot * `America/Martinique` - America/Martinique * `America/Matamoros` - America/Matamoros * `America/Mazatlan` - America/Mazatlan * `America/Menominee` - America/Menominee * `America/Merida` - America/Merida * `America/Metlakatla` - America/Metlakatla * `America/Mexico_City` - America/Mexico\_City * `America/Miquelon` - America/Miquelon * `America/Moncton` - America/Moncton * `America/Monterrey` - America/Monterrey * `America/Montevideo` - America/Montevideo * `America/Montserrat` - America/Montserrat * `America/Nassau` - America/Nassau * `America/New_York` - America/New\_York * `America/Nome` - America/Nome * `America/Noronha` - America/Noronha * `America/North_Dakota/Beulah` - America/North\_Dakota/Beulah * `America/North_Dakota/Center` - America/North\_Dakota/Center * `America/North_Dakota/New_Salem` - America/North\_Dakota/New\_Salem * `America/Nuuk` - America/Nuuk * `America/Ojinaga` - America/Ojinaga * `America/Panama` - America/Panama * `America/Paramaribo` - America/Paramaribo * `America/Phoenix` - America/Phoenix * `America/Port-au-Prince` - America/Port-au-Prince * `America/Port_of_Spain` - America/Port\_of\_Spain * `America/Porto_Velho` - America/Porto\_Velho * `America/Puerto_Rico` - America/Puerto\_Rico * `America/Punta_Arenas` - America/Punta\_Arenas * `America/Rankin_Inlet` - America/Rankin\_Inlet * `America/Recife` - America/Recife * `America/Regina` - America/Regina * `America/Resolute` - America/Resolute * `America/Rio_Branco` - America/Rio\_Branco * `America/Santarem` - America/Santarem * `America/Santiago` - America/Santiago * `America/Santo_Domingo` - America/Santo\_Domingo * `America/Sao_Paulo` - America/Sao\_Paulo * `America/Scoresbysund` - America/Scoresbysund * `America/Sitka` - America/Sitka * `America/St_Barthelemy` - America/St\_Barthelemy * `America/St_Johns` - America/St\_Johns * `America/St_Kitts` - America/St\_Kitts * `America/St_Lucia` - America/St\_Lucia * `America/St_Thomas` - America/St\_Thomas * `America/St_Vincent` - America/St\_Vincent * `America/Swift_Current` - America/Swift\_Current * `America/Tegucigalpa` - America/Tegucigalpa * `America/Thule` - America/Thule * `America/Tijuana` - America/Tijuana * `America/Toronto` - America/Toronto * `America/Tortola` - America/Tortola * `America/Vancouver` - America/Vancouver * `America/Whitehorse` - America/Whitehorse * `America/Winnipeg` - America/Winnipeg * `America/Yakutat` - America/Yakutat * `Antarctica/Casey` - Antarctica/Casey * `Antarctica/Davis` - Antarctica/Davis * `Antarctica/DumontDUrville` - Antarctica/DumontDUrville * `Antarctica/Macquarie` - Antarctica/Macquarie * `Antarctica/Mawson` - Antarctica/Mawson * `Antarctica/McMurdo` - Antarctica/McMurdo * `Antarctica/Palmer` - Antarctica/Palmer * `Antarctica/Rothera` - Antarctica/Rothera * `Antarctica/Syowa` - Antarctica/Syowa * `Antarctica/Troll` - Antarctica/Troll * `Antarctica/Vostok` - Antarctica/Vostok * `Arctic/Longyearbyen` - Arctic/Longyearbyen * `Asia/Aden` - Asia/Aden * `Asia/Almaty` - Asia/Almaty * `Asia/Amman` - Asia/Amman * `Asia/Anadyr` - Asia/Anadyr * `Asia/Aqtau` - Asia/Aqtau * `Asia/Aqtobe` - Asia/Aqtobe * `Asia/Ashgabat` - Asia/Ashgabat * `Asia/Atyrau` - Asia/Atyrau * `Asia/Baghdad` - Asia/Baghdad * `Asia/Bahrain` - Asia/Bahrain * `Asia/Baku` - Asia/Baku * `Asia/Bangkok` - Asia/Bangkok * `Asia/Barnaul` - Asia/Barnaul * `Asia/Beirut` - Asia/Beirut * `Asia/Bishkek` - Asia/Bishkek * `Asia/Brunei` - Asia/Brunei * `Asia/Chita` - Asia/Chita * `Asia/Choibalsan` - Asia/Choibalsan * `Asia/Colombo` - Asia/Colombo * `Asia/Damascus` - Asia/Damascus * `Asia/Dhaka` - Asia/Dhaka * `Asia/Dili` - Asia/Dili * `Asia/Dubai` - Asia/Dubai * `Asia/Dushanbe` - Asia/Dushanbe * `Asia/Famagusta` - Asia/Famagusta * `Asia/Gaza` - Asia/Gaza * `Asia/Hebron` - Asia/Hebron * `Asia/Ho_Chi_Minh` - Asia/Ho\_Chi\_Minh * `Asia/Hong_Kong` - Asia/Hong\_Kong * `Asia/Hovd` - Asia/Hovd * `Asia/Irkutsk` - Asia/Irkutsk * `Asia/Jakarta` - Asia/Jakarta * `Asia/Jayapura` - Asia/Jayapura * `Asia/Jerusalem` - Asia/Jerusalem * `Asia/Kabul` - Asia/Kabul * `Asia/Kamchatka` - Asia/Kamchatka * `Asia/Karachi` - Asia/Karachi * `Asia/Kathmandu` - Asia/Kathmandu * `Asia/Khandyga` - Asia/Khandyga * `Asia/Kolkata` - Asia/Kolkata * `Asia/Krasnoyarsk` - Asia/Krasnoyarsk * `Asia/Kuala_Lumpur` - Asia/Kuala\_Lumpur * `Asia/Kuching` - Asia/Kuching * `Asia/Kuwait` - Asia/Kuwait * `Asia/Macau` - Asia/Macau * `Asia/Magadan` - Asia/Magadan * `Asia/Makassar` - Asia/Makassar * `Asia/Manila` - Asia/Manila * `Asia/Muscat` - Asia/Muscat * `Asia/Nicosia` - Asia/Nicosia * `Asia/Novokuznetsk` - Asia/Novokuznetsk * `Asia/Novosibirsk` - Asia/Novosibirsk * `Asia/Omsk` - Asia/Omsk * `Asia/Oral` - Asia/Oral * `Asia/Phnom_Penh` - Asia/Phnom\_Penh * `Asia/Pontianak` - Asia/Pontianak * `Asia/Pyongyang` - Asia/Pyongyang * `Asia/Qatar` - Asia/Qatar * `Asia/Qostanay` - Asia/Qostanay * `Asia/Qyzylorda` - Asia/Qyzylorda * `Asia/Riyadh` - Asia/Riyadh * `Asia/Sakhalin` - Asia/Sakhalin * `Asia/Samarkand` - Asia/Samarkand * `Asia/Seoul` - Asia/Seoul * `Asia/Shanghai` - Asia/Shanghai * `Asia/Singapore` - Asia/Singapore * `Asia/Srednekolymsk` - Asia/Srednekolymsk * `Asia/Taipei` - Asia/Taipei * `Asia/Tashkent` - Asia/Tashkent * `Asia/Tbilisi` - Asia/Tbilisi * `Asia/Tehran` - Asia/Tehran * `Asia/Thimphu` - Asia/Thimphu * `Asia/Tokyo` - Asia/Tokyo * `Asia/Tomsk` - Asia/Tomsk * `Asia/Ulaanbaatar` - Asia/Ulaanbaatar * `Asia/Urumqi` - Asia/Urumqi * `Asia/Ust-Nera` - Asia/Ust-Nera * `Asia/Vientiane` - Asia/Vientiane * `Asia/Vladivostok` - Asia/Vladivostok * `Asia/Yakutsk` - Asia/Yakutsk * `Asia/Yangon` - Asia/Yangon * `Asia/Yekaterinburg` - Asia/Yekaterinburg * `Asia/Yerevan` - Asia/Yerevan * `Atlantic/Azores` - Atlantic/Azores * `Atlantic/Bermuda` - Atlantic/Bermuda * `Atlantic/Canary` - Atlantic/Canary * `Atlantic/Cape_Verde` - Atlantic/Cape\_Verde * `Atlantic/Faroe` - Atlantic/Faroe * `Atlantic/Madeira` - Atlantic/Madeira * `Atlantic/Reykjavik` - Atlantic/Reykjavik * `Atlantic/South_Georgia` - Atlantic/South\_Georgia * `Atlantic/St_Helena` - Atlantic/St\_Helena * `Atlantic/Stanley` - Atlantic/Stanley * `Australia/Adelaide` - Australia/Adelaide * `Australia/Brisbane` - Australia/Brisbane * `Australia/Broken_Hill` - Australia/Broken\_Hill * `Australia/Darwin` - Australia/Darwin * `Australia/Eucla` - Australia/Eucla * `Australia/Hobart` - Australia/Hobart * `Australia/Lindeman` - Australia/Lindeman * `Australia/Lord_Howe` - Australia/Lord\_Howe * `Australia/Melbourne` - Australia/Melbourne * `Australia/Perth` - Australia/Perth * `Australia/Sydney` - Australia/Sydney * `Canada/Atlantic` - Canada/Atlantic * `Canada/Central` - Canada/Central * `Canada/Eastern` - Canada/Eastern * `Canada/Mountain` - Canada/Mountain * `Canada/Newfoundland` - Canada/Newfoundland * `Canada/Pacific` - Canada/Pacific * `Europe/Amsterdam` - Europe/Amsterdam * `Europe/Andorra` - Europe/Andorra * `Europe/Astrakhan` - Europe/Astrakhan * `Europe/Athens` - Europe/Athens * `Europe/Belgrade` - Europe/Belgrade * `Europe/Berlin` - Europe/Berlin * `Europe/Bratislava` - Europe/Bratislava * `Europe/Brussels` - Europe/Brussels * `Europe/Bucharest` - Europe/Bucharest * `Europe/Budapest` - Europe/Budapest * `Europe/Busingen` - Europe/Busingen * `Europe/Chisinau` - Europe/Chisinau * `Europe/Copenhagen` - Europe/Copenhagen * `Europe/Dublin` - Europe/Dublin * `Europe/Gibraltar` - Europe/Gibraltar * `Europe/Guernsey` - Europe/Guernsey * `Europe/Helsinki` - Europe/Helsinki * `Europe/Isle_of_Man` - Europe/Isle\_of\_Man * `Europe/Istanbul` - Europe/Istanbul * `Europe/Jersey` - Europe/Jersey * `Europe/Kaliningrad` - Europe/Kaliningrad * `Europe/Kirov` - Europe/Kirov * `Europe/Kyiv` - Europe/Kyiv * `Europe/Lisbon` - Europe/Lisbon * `Europe/Ljubljana` - Europe/Ljubljana * `Europe/London` - Europe/London * `Europe/Luxembourg` - Europe/Luxembourg * `Europe/Madrid` - Europe/Madrid * `Europe/Malta` - Europe/Malta * `Europe/Mariehamn` - Europe/Mariehamn * `Europe/Minsk` - Europe/Minsk * `Europe/Monaco` - Europe/Monaco * `Europe/Moscow` - Europe/Moscow * `Europe/Oslo` - Europe/Oslo * `Europe/Paris` - Europe/Paris * `Europe/Podgorica` - Europe/Podgorica * `Europe/Prague` - Europe/Prague * `Europe/Riga` - Europe/Riga * `Europe/Rome` - Europe/Rome * `Europe/Samara` - Europe/Samara * `Europe/San_Marino` - Europe/San\_Marino * `Europe/Sarajevo` - Europe/Sarajevo * `Europe/Saratov` - Europe/Saratov * `Europe/Simferopol` - Europe/Simferopol * `Europe/Skopje` - Europe/Skopje * `Europe/Sofia` - Europe/Sofia * `Europe/Stockholm` - Europe/Stockholm * `Europe/Tallinn` - Europe/Tallinn * `Europe/Tirane` - Europe/Tirane * `Europe/Ulyanovsk` - Europe/Ulyanovsk * `Europe/Vaduz` - Europe/Vaduz * `Europe/Vatican` - Europe/Vatican * `Europe/Vienna` - Europe/Vienna * `Europe/Vilnius` - Europe/Vilnius * `Europe/Volgograd` - Europe/Volgograd * `Europe/Warsaw` - Europe/Warsaw * `Europe/Zagreb` - Europe/Zagreb * `Europe/Zurich` - Europe/Zurich * `GMT` - GMT * `Indian/Antananarivo` - Indian/Antananarivo * `Indian/Chagos` - Indian/Chagos * `Indian/Christmas` - Indian/Christmas * `Indian/Cocos` - Indian/Cocos * `Indian/Comoro` - Indian/Comoro * `Indian/Kerguelen` - Indian/Kerguelen * `Indian/Mahe` - Indian/Mahe * `Indian/Maldives` - Indian/Maldives * `Indian/Mauritius` - Indian/Mauritius * `Indian/Mayotte` - Indian/Mayotte * `Indian/Reunion` - Indian/Reunion * `Pacific/Apia` - Pacific/Apia * `Pacific/Auckland` - Pacific/Auckland * `Pacific/Bougainville` - Pacific/Bougainville * `Pacific/Chatham` - Pacific/Chatham * `Pacific/Chuuk` - Pacific/Chuuk * `Pacific/Easter` - Pacific/Easter * `Pacific/Efate` - Pacific/Efate * `Pacific/Fakaofo` - Pacific/Fakaofo * `Pacific/Fiji` - Pacific/Fiji * `Pacific/Funafuti` - Pacific/Funafuti * `Pacific/Galapagos` - Pacific/Galapagos * `Pacific/Gambier` - Pacific/Gambier * `Pacific/Guadalcanal` - Pacific/Guadalcanal * `Pacific/Guam` - Pacific/Guam * `Pacific/Honolulu` - Pacific/Honolulu * `Pacific/Kanton` - Pacific/Kanton * `Pacific/Kiritimati` - Pacific/Kiritimati * `Pacific/Kosrae` - Pacific/Kosrae * `Pacific/Kwajalein` - Pacific/Kwajalein * `Pacific/Majuro` - Pacific/Majuro * `Pacific/Marquesas` - Pacific/Marquesas * `Pacific/Midway` - Pacific/Midway * `Pacific/Nauru` - Pacific/Nauru * `Pacific/Niue` - Pacific/Niue * `Pacific/Norfolk` - Pacific/Norfolk * `Pacific/Noumea` - Pacific/Noumea * `Pacific/Pago_Pago` - Pacific/Pago\_Pago * `Pacific/Palau` - Pacific/Palau * `Pacific/Pitcairn` - Pacific/Pitcairn * `Pacific/Pohnpei` - Pacific/Pohnpei * `Pacific/Port_Moresby` - Pacific/Port\_Moresby * `Pacific/Rarotonga` - Pacific/Rarotonga * `Pacific/Saipan` - Pacific/Saipan * `Pacific/Tahiti` - Pacific/Tahiti * `Pacific/Tarawa` - Pacific/Tarawa * `Pacific/Tongatapu` - Pacific/Tongatapu * `Pacific/Wake` - Pacific/Wake * `Pacific/Wallis` - Pacific/Wallis * `US/Alaska` - US/Alaska * `US/Arizona` - US/Arizona * `US/Central` - US/Central * `US/Eastern` - US/Eastern * `US/Hawaii` - US/Hawaii * `US/Mountain` - US/Mountain * `US/Pacific` - US/Pacific * `UTC` - UTC ### Scopes `projects.cycles:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", start_date: "2024-01-01T00:00:00Z", end_date: "2024-01-01T00:00:00Z", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "status": "current", "total_issues": 15, "completed_issues": 8, "cancelled_issues": 1, "started_issues": 4, "unstarted_issues": 2, "backlog_issues": 0, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/cycle/unarchive-cycle.html' description: >- Restore a cycle via Plane API. HTTP request format, parameters, scopes, and example responses for restore a cycle. --- # Restore a cycle Restore an archived cycle to active status, making it available for regular use. ### Path Parameters The unique identifier of the cycle. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.cycles:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-cycles/cycle-uuid/unarchive/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-cycles/cycle-uuid/unarchive/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-cycles/cycle-uuid/unarchive/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/cycle/remove-cycle-work-item.html' description: >- Remove work item from cycle via Plane API. HTTP request format, parameters, scopes, and example responses for remove work item from cycle. --- # Remove work item from cycle Remove a work item from a cycle while keeping the work item in the project. ### Path Parameters The unique identifier of the cycle. The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.cycles:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/work-item-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/work-item-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/cycle-uuid/cycle-issues/work-item-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/cycle/delete-cycle.html' description: >- Delete a cycle via Plane API. HTTP request format, parameters, scopes, and example responses for delete a cycle. --- # Delete a cycle Permanently remove a cycle and all its associated issue relationships ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.cycles:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/cycles/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/module/overview.html' description: >- Plane Module API overview. Learn about endpoints, request/response format, and how to work with module via REST API. --- # Overview Modules are smaller, focused projects that help you group and organize issues within a specific time frame. They allow you to break down your work into manageable chunks and track progress towards specific goals or objectives. [Learn more about Modules](https://docs.plane.so/core-concepts/modules) ## The Module Object ### Attributes * `name` string(required) Name of the module * `description` string Description of the module * `description_html` string Description in HTML format * `start_date` date Start date of the module * `target_date` date Estimated date to complete the module * `created_at` *timestamp* The timestamp of the time when the project was created * `updated_at` *timestamp* The timestamp of the time when the project was last updated * `status` It describes the status of the module The status can be * backlog * planned * in-progress * paused * completed * cancelled * `view_props` It store the filters and the display properties selected by the user to visualize the issues in the module * `sort_order` It gives the position of the module at which it should be displayed * `created_by` , `updated_by` *uuid* These values are auto saved and represent the id of the user that created or updated the module * `Project` uuid It contains projects uuid which is automatically saved. * `Workspace` uuid It contains workspace uuid which is automatically saved * `lead` uuid Lead of the module * `members` string\[] List of member user IDs assigned to the module * `archived_at` *timestamp* The timestamp when the module was archived (if archived) * `logo_props` Logo properties for the module * `description_text` Description in plain text format ```json { "id": "b69b19ae-261f-428c-899f-dd58efaa36c0", "created_at": "2023-11-19T11:48:21.130161Z", "updated_at": "2023-11-19T11:48:21.130168Z", "name": "module stesting", "description": "", "description_text": null, "description_html": null, "start_date": null, "target_date": null, "status": "planned", "view_props": {}, "sort_order": 55535.0, "created_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "updated_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "project": "6436c4ae-fba7-45dc-ad4a-5440e17cb1b2", "workspace": "c467e125-59e3-44ec-b5ee-f9c1e138c611", "lead": null, "members": [] } ``` --- --- url: 'https://developers.plane.so/api-reference/module/add-module.html' description: >- Create a module via Plane API. HTTP request format, parameters, scopes, and example responses for create a module. --- # Create a module Create a new project module with specified name, description, and timeline. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Start date. Target date. * `backlog` - Backlog * `planned` - Planned * `in-progress` - In Progress * `paused` - Paused * `completed` - Completed * `cancelled` - Cancelled Lead. Members. External source. External id. ### Scopes `projects.modules:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "end_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "end_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", start_date: "2024-01-01", end_date: "2024-01-01", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "target_date": "2024-01-01", "status": "in-progress", "total_issues": 12, "completed_issues": 5, "cancelled_issues": 0, "started_issues": 4, "unstarted_issues": 3, "backlog_issues": 0, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/module/add-module-work-items.html' description: >- Add work items to module via Plane API. HTTP request format, parameters, scopes, and example responses for add work items to module. --- # Add work items to module Assign multiple work items to a module or move them from another module. Automatically handles bulk creation and updates with activity tracking. ### Path Parameters The unique identifier of the module. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters List of issue IDs to add to the module ### Scopes `projects.modules:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issues": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/", headers={"X-API-Key": "your-api-key"}, json={ "issues": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ issues: ["550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "module": "550e8400-e29b-41d4-a716-446655440000", "issue": "550e8400-e29b-41d4-a716-446655440000", "sub_issues_count": 2, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ] ``` --- --- url: 'https://developers.plane.so/api-reference/module/archive-module.html' description: >- Archive a module via Plane API. HTTP request format, parameters, scopes, and example responses for archive a module. --- # Archive a module Move a module to archived status for historical tracking. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.modules:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/archive/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/archive/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/archive/", { method: "POST", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/module/list-modules.html' description: >- List all modules via Plane API. HTTP request format, parameters, scopes, and example responses for list all modules. --- # List all modules Retrieve all modules in a project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.modules:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "target_date": "2024-01-01", "status": "in_progress" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/module/get-module-detail.html' description: >- Retrieve a module via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a module. --- # Retrieve a module Retrieve details of a specific module. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.modules:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "target_date": "2024-01-01", "status": "in-progress", "total_issues": 12, "completed_issues": 5, "cancelled_issues": 0, "started_issues": 4, "unstarted_issues": 3, "backlog_issues": 0, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/module/list-module-work-items.html' description: >- List all work items in a module via Plane API. HTTP request format, parameters, scopes, and example responses for list all work items in a module. --- # List all work items in a module Retrieve all work items assigned to a module with detailed information. ### Path Parameters The unique identifier of the module. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.modules:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/module/list-archived-modules.html' description: >- List all archived modules via Plane API. HTTP request format, parameters, scopes, and example responses for list all archived modules. --- # List all archived modules Retrieve all modules that have been archived in the project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.modules:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-modules/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-modules/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-modules/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/module/update-module-detail.html' description: >- Update module details via Plane API. HTTP request format, parameters, scopes, and example responses for update module details. --- # Update module details Modify an existing module's properties like name, description, status, or timeline. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Start date. Target date. * `backlog` - Backlog * `planned` - Planned * `in-progress` - In Progress * `paused` - Paused * `completed` - Completed * `cancelled` - Cancelled Lead. Members. External source. External id. ### Scopes `projects.modules:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "end_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "end_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", start_date: "2024-01-01", end_date: "2024-01-01", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "start_date": "2024-01-01", "target_date": "2024-01-01", "status": "in-progress", "total_issues": 12, "completed_issues": 5, "cancelled_issues": 0, "started_issues": 4, "unstarted_issues": 3, "backlog_issues": 0, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/module/unarchive-module.html' description: >- Restore a module via Plane API. HTTP request format, parameters, scopes, and example responses for restore a module. --- # Restore a module Restore an archived module to active status, making it available for regular use. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.modules:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-modules/resource-id-uuid/unarchive/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-modules/resource-id-uuid/unarchive/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/archived-modules/resource-id-uuid/unarchive/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/module/remove-module-work-item.html' description: >- Remove work item from module via Plane API. HTTP request format, parameters, scopes, and example responses for remove work item from module. --- # Remove work item from module Remove a work item from a module while keeping the work item in the project. ### Path Parameters The unique identifier of the work item. The unique identifier of the module. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.modules:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/work-item-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/work-item-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/module-uuid/module-issues/work-item-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/module/delete-module.html' description: >- Delete a module via Plane API. HTTP request format, parameters, scopes, and example responses for delete a module. --- # Delete a module Permanently remove a module and all its associated issue relationships. ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.modules:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/modules/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/page/overview.html' description: >- Plane Page API overview. Learn about endpoints, request/response format, and how to work with page via REST API. --- # Overview Pages allow you to create and manage documentation at both workspace and project levels. Workspace pages are accessible across all projects, while project pages are specific to individual projects. Page body mutations are applied to Plane's collaborative document so API updates remain consistent with active editor sessions. **Documentation**: [Wiki](https://docs.plane.so/core-concepts/pages/wiki), [Pages](https://docs.plane.so/core-concepts/pages/overview) ## The Pages Object ### Attributes * `id` *uuid* Unique identifier for the page * `name` *string* Name of the page * `description_html` *string* HTML description/content of the page * `created_at` *timestamp* The timestamp when the page was created * `updated_at` *timestamp* The timestamp when the page was last updated * `created_by` *uuid* ID of the user who created the page * `updated_by` *uuid* ID of the user who last updated the page * `parent_id` *uuid or null* ID of the parent page for a child page * `collection_id` *uuid or null* ID of the collection containing a workspace page * `page_collection_id` *uuid or null* ID of the page's placement record within its collection * `archived_at` *timestamp or null* Time the page was archived * `is_locked` *boolean* Whether content mutations are blocked ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2023-11-19T11:56:55.176802Z", "updated_at": "2023-11-19T11:56:55.176809Z", "name": "Getting Started", "description_html": "

Welcome

This is a getting started guide.

", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "parent_id": null, "collection_id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "page_collection_id": "1d9b4f7b-56e8-4c63-9ff4-e24b508c162d", "archived_at": null, "is_locked": false } ``` --- --- url: 'https://developers.plane.so/api-reference/page/page-content-html.html' description: >- Format page body HTML, use Plane editor components, and attach files through the Workspace Page API. --- # Page content HTML Send page body content in `description_html` when you create or update a page. Plane sanitizes the HTML, converts it to the editor document format, and replaces the complete page body during an update. > Unsupported tags, attributes, and unsafe URLs can be removed. Fetch the page after a write if your integration > needs to inspect the final stored HTML. The maximum HTML payload is 10 MB. ## Standard HTML The following shapes are suitable for API-created page content: | Content | HTML shape | | --------------- | ----------------------------------------------------------------- | | Paragraph | `

Text

` | | Headings | `

Title

` through `
Title
` | | Bold | `Bold` | | Italic | `Italic` | | Underline | `Underline` | | Strikethrough | `Strike` | | Inline code | `const value = 1` | | Link | `Plane` | | Blockquote | `

Quote

` | | Code block | `
const value = 1;
` | | Bullet list | `
  • Item
` | | Ordered list | `
  1. Item
` | | Task list | `
  • Done
` | | Horizontal rule | `
` | | Table | `
Cell
` | | Image | `Image` | URL attributes support safe protocols such as `http`, `https`, `mailto`, and `tel`. ```html

Project brief

This page was updated through the API.

  • Write the brief
  • Review with the team
Owner Status
Design In progress
``` ## Plane editor components Entity-backed components must reference existing Plane entities. Use UUIDs, not human-readable work item keys, and do not include private user data in page content. ### User mention ```html ``` ### Work item embed ```html ``` ### Page embed ```html ``` ### Image component ```html ``` ### Callout ```html

This is a callout created through the API.

``` ### External embed ```html ``` ### Math and dates ```html

Inline formula:

Target date:

``` ## Attach a file Page attachments use the workspace asset upload flow; there is no multipart page-attachment endpoint. 1. Create upload credentials with [`POST /workspaces/{workspace_slug}/assets/`](/api-reference/assets/create-workspace-asset-upload): ```json { "name": "requirements.pdf", "type": "application/pdf", "size": 12345, "entity_type": "PAGE_DESCRIPTION", "entity_identifier": "PAGE_UUID" } ``` 2. Upload the file bytes to `upload_data.url` using the returned form fields. 3. Confirm the upload with the [page attachment endpoint](/api-reference/page/confirm-workspace-page-attachment-upload). 4. Include the attachment in the complete page body sent through `description_html`: ```html ``` Use the asset ID returned by Plane. Attachment metadata and downloads remain subject to page visibility; confirming or deleting an attachment requires edit access to an unlocked, active page. --- --- url: 'https://developers.plane.so/api-reference/page/list-workspace-pages.html' description: >- List workspace wiki pages via Plane API. HTTP request format, parameters, scopes, and example responses for listing workspace wiki pages. --- # List workspace wiki pages List all wiki pages in a workspace with optional filtering and search. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Filter pages by scope. Defaults to `all`. * `all` — all pages the user has access to * `public` — pages with public access, excluding archived * `private` — pages owned by the user and not shared, excluding archived * `shared` — private pages explicitly shared with the user * `archived` — pages that have been archived Case-insensitive search on page title. Number of results per page. Defaults to `20`, maximum `100`. Pagination cursor for getting the next or previous set of results. ### Scopes `wiki.pages:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/pages/?type=public&search=welcome&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/", headers={"X-API-Key": "your-api-key"}, params={"type": "public", "search": "welcome", "per_page": 20}, ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/?type=public&search=welcome&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": null, "sub_grouped_by": null, "total_count": 4, "next_cursor": "20:1:0", "prev_cursor": "20:-1:1", "next_page_results": false, "prev_page_results": false, "count": 4, "total_pages": 1, "total_results": 4, "extra_stats": null, "results": [ { "id": "b3478c56-31f6-4f7e-b445-8392a4b26621", "name": "welcome 3 b", "owned_by": "5b0af4aa-e310-408a-a480-868429af5701", "access": 0, "is_locked": false, "archived_at": null, "workspace": "8725ddfa-c181-49f6-9173-97b8d0b7d599", "created_at": "2026-04-01T15:41:19.062280Z", "updated_at": "2026-04-07T19:30:39.274060Z", "logo_props": {}, "parent_id": "a2819c8b-f7ac-4cbd-b971-682726c4f8cc" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/page/add-workspace-page.html' description: >- Create a wiki page via Plane API. HTTP request format, parameters, scopes, and example responses for create a wiki page. --- # Create a wiki page Create a workspace page. To create a child page, pass `parent_id`; Plane creates the page and inserts a page embed into the parent document. The parent must be visible, editable, unlocked, and active. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. * `0` - Public * `1` - Private Color. Is locked. Archived at. View props. Logo props. External id. External source. HTML content for the page body. Plane sanitizes the HTML before storing it. UUID of the parent workspace page. Do not combine `parent_id` with `collection_id`. ### Scopes `write` or `wiki.pages:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/pages/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "access": 0, "color": "Example Name", "is_locked": true, "archived_at": "2024-01-01", "view_props": "example-value", "logo_props": "example-value", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "parent_id": "4d2f6f7e-9b4a-4d6a-8f4a-1c3f7c0f4a10", "description_html": "

Example content

" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "access": 0, "color": "Example Name", "is_locked": true, "archived_at": "2024-01-01", "view_props": "example-value", "logo_props": "example-value", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "parent_id": "4d2f6f7e-9b4a-4d6a-8f4a-1c3f7c0f4a10", "description_html": "

Example content

" } ) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/pages/", { method: "POST", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", access: 0, color: "Example Name", is_locked: true, archived_at: "2024-01-01", view_props: "example-value", logo_props: "example-value", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", parent_id: "4d2f6f7e-9b4a-4d6a-8f4a-1c3f7c0f4a10", description_html: "

Example content

", }), }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "description_html": "

Example content

", "owned_by": "550e8400-e29b-41d4-a716-446655440000", "access": 0, "color": "Example Name", "is_locked": true, "archived_at": "2024-01-01", "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ``` ## Child-page responses * `201 Created`: the child was created and linked in the parent document. * `202 Accepted`: the child was created, but Plane is still retrying the parent link. Store the returned page ID and check the parent later. The response already contains the requested `parent_id`; only the parent document embed is pending. * `400 Bad Request`: the parent is locked or archived, or `parent_id` conflicts with `collection_id`. * `403 Forbidden`: the caller cannot edit the parent. * `404 Not Found`: the parent is unavailable in this workspace. * `502 Bad Gateway`: the parent link was rejected and the new child was removed. * `503 Service Unavailable`: the collaborative document service is not configured. --- --- url: 'https://developers.plane.so/api-reference/page/get-workspace-page.html' description: >- Retrieve a wiki page via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a wiki page. --- # Retrieve a wiki page Get a workspace page by ID ### Path Parameters The unique identifier of the page. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `wiki.pages:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/page/update-workspace-page.html' description: Update the title or content of a workspace page via the Plane API. --- # Update a workspace page Update a workspace page. Send `name`, `description_html`, or both. `description_html` replaces the page's current content rather than appending to it. Plane applies the mutation through its collaborative document service so API writes remain consistent with active editor sessions. Locked or archived pages cannot be updated. The API returns `502` when the collaborative document service cannot complete the update and `503` when that service is not configured. See [Page content HTML](/api-reference/page/page-content-html) for supported HTML and Plane editor components. ### Path Parameters The workspace's unique slug. The page UUID. ### Body Parameters The new page title. HTML that replaces the current page content. At least one body parameter is required. ### Scopes `write` or `wiki.pages:write` ```bash curl -X PUT "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"Release notes","description_html":"

Current release notes

"}' ``` ```python import requests response = requests.put( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/", headers={"X-API-Key": "your-api-key"}, json={"name": "Release notes", "description_html": "

Current release notes

"}, ) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/", { method: "PUT", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ name: "Release notes", description_html: "

Current release notes

", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Release notes", "description_html": "

Current release notes

" } ``` --- --- url: 'https://developers.plane.so/api-reference/page/archive-workspace-page.html' description: Archive a workspace page via the Plane API before deleting it. --- # Archive a workspace page Archive a workspace page and its subpages. A page must be archived before it can be deleted. ### Path Parameters The workspace's unique slug. The page UUID. ### Scopes `write` or `wiki.pages:write` ```bash curl -X POST "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/archive/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.post("https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/archive/", headers={"X-API-Key": "your-api-key"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/archive/", { method: "POST", headers: { "X-API-Key": process.env.PLANE_API_KEY }, }, ); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/page/restore-workspace-page.html' description: Restore an archived workspace page via the Plane API. --- # Restore a workspace page Restore an archived workspace page. If its parent is archived, restore the parent before restoring the child; otherwise the API returns `400`. ### Path Parameters The workspace's unique slug. The page UUID. ### Scopes `write` or `wiki.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/archive/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.delete("https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/archive/", headers={"X-API-Key": "your-api-key"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/archive/", { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY }, }, ); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/page/delete-workspace-page.html' description: Delete an archived workspace page via the Plane API. --- # Delete a workspace page Delete a workspace page. Archive the page before calling this operation; deleting an active page returns `400`. ### Path Parameters The workspace's unique slug. The page UUID. ### Scopes `write` or `wiki.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.delete("https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/", headers={"X-API-Key": "your-api-key"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/", { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY }, }, ); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/page/get-workspace-page-attachment.html description: Retrieve metadata for an attachment linked to a workspace page. --- # Retrieve workspace page attachment metadata Retrieve metadata and links for an existing workspace page attachment. The caller must be able to view the page. ### Path Parameters The workspace's unique slug. The workspace page UUID. The attachment asset UUID. ### Scopes `read` or `wiki.pages:read` ```bash curl -X GET "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.get("https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/", headers={"X-API-Key": "your-api-key"}) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/", { headers: { "X-API-Key": process.env.PLANE_API_KEY } }, ); const data = await response.json(); ``` ```json { "id": "attachment-uuid", "page_id": "page-uuid", "name": "diagram.png", "type": "image/png", "size": 24576, "is_uploaded": true, "asset_url": "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/", "_links": { "download": "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/download/" } } ``` --- --- url: >- https://developers.plane.so/api-reference/page/confirm-workspace-page-attachment-upload.html description: Confirm the upload status of an attachment linked to a workspace page. --- # Confirm a workspace page attachment upload Set an attachment's upload status after uploading it through the generic asset upload flow. This operation queues storage metadata extraction when needed. The page must be editable and cannot be locked or archived. ### Path Parameters The workspace's unique slug. The workspace page UUID. The attachment asset UUID. ### Body Parameters Whether the attachment was uploaded successfully. Defaults to `true`. ### Scopes `write` or `wiki.pages:write` ```bash curl -X PATCH "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/" -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" -d '{"is_uploaded":true}' ``` ```python import requests response = requests.patch("https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/", headers={"X-API-Key": "your-api-key"}, json={"is_uploaded": True}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/", { method: "PATCH", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ is_uploaded: true }), }, ); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/page/download-workspace-page-attachment.html description: Download an uploaded workspace page attachment through a presigned redirect. --- # Download a workspace page attachment Validate access and receive a `302` redirect to a temporary presigned download URL. An attachment that has not been confirmed as uploaded returns `400`. ### Path Parameters The workspace's unique slug. The workspace page UUID. The attachment asset UUID. ### Scopes `read` or `wiki.pages:read` ```bash download_url="$( curl --fail --silent --show-error --dump-header - --output /dev/null \ "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/download/" \ -H "X-API-Key: $PLANE_API_KEY" \ | sed -n 's/^Location: \(https:\/\/.*\)\r$/\1/p' )" test -n "$download_url" curl --fail --location "$download_url" --output diagram.png ``` ```python from urllib.parse import urlparse import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/download/", headers={"X-API-Key": "your-api-key"}, allow_redirects=False, ) response.raise_for_status() download_url = response.headers["Location"] if urlparse(download_url).scheme != "https": raise ValueError("Expected an HTTPS download URL") with requests.get(download_url, stream=True) as download: download.raise_for_status() with open("diagram.png", "wb") as output: for chunk in download.iter_content(chunk_size=8192): output.write(chunk) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. import { writeFile } from "node:fs/promises"; const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/download/", { headers: { "X-API-Key": process.env.PLANE_API_KEY }, redirect: "manual" }, ); if (response.status !== 302) throw new Error(`Expected a redirect, received ${response.status}`); const downloadUrl = new URL(response.headers.get("location")); if (downloadUrl.protocol !== "https:") throw new Error("Expected an HTTPS download URL"); const download = await fetch(downloadUrl); if (!download.ok) throw new Error(`Download failed with ${download.status}`); await writeFile("diagram.png", Buffer.from(await download.arrayBuffer())); ``` The `Location` header contains the temporary presigned download URL. --- --- url: >- https://developers.plane.so/api-reference/page/delete-workspace-page-attachment.html description: Delete an attachment linked to a workspace page via the Plane API. --- # Delete a workspace page attachment Soft-delete an attachment from an editable workspace page. Later metadata and download requests return `404`. Locked or archived pages cannot be changed. ### Path Parameters The workspace's unique slug. The workspace page UUID. The attachment asset UUID. ### Scopes `write` or `wiki.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.delete("https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/", headers={"X-API-Key": "your-api-key"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/pages/page-uuid/attachments/attachment-uuid/", { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY } }, ); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/page/list-project-pages.html' description: >- List project pages via Plane API. HTTP request format, parameters, scopes, and example responses for listing project pages. --- # List project pages List all pages in a project with optional filtering and search. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project. ### Query Parameters Filter pages by scope. Defaults to `all`. * `all` — all pages the user has access to * `public` — pages with public access, excluding archived * `private` — pages owned by the user and not shared, excluding archived * `shared` — private pages explicitly shared with the user * `archived` — pages that have been archived Case-insensitive search on page title. Number of results per page. Defaults to `20`, maximum `100`. Pagination cursor for getting the next or previous set of results. ### Scopes `projects.pages:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-id/pages/?type=public&search=welcome&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-id/pages/", headers={"X-API-Key": "your-api-key"}, params={"type": "public", "search": "welcome", "per_page": 20}, ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-id/pages/?type=public&search=welcome&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": null, "sub_grouped_by": null, "total_count": 4, "next_cursor": "20:1:0", "prev_cursor": "20:-1:1", "next_page_results": false, "prev_page_results": false, "count": 4, "total_pages": 1, "total_results": 4, "extra_stats": null, "results": [ { "id": "b3478c56-31f6-4f7e-b445-8392a4b26621", "name": "welcome 3 b", "owned_by": "5b0af4aa-e310-408a-a480-868429af5701", "access": 0, "is_locked": false, "archived_at": null, "workspace": "8725ddfa-c181-49f6-9173-97b8d0b7d599", "created_at": "2026-04-01T15:41:19.062280Z", "updated_at": "2026-04-07T19:30:39.274060Z", "logo_props": {}, "parent_id": "a2819c8b-f7ac-4cbd-b971-682726c4f8cc" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/page/add-project-page.html' description: >- Create a project page via Plane API. HTTP request format, parameters, scopes, and example responses for create a project page. --- # Create a project page Create a project page ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. * `0` - Public * `1` - Private Color. Is locked. Archived at. View props. Logo props. External id. External source. Description html. ### Scopes `projects.pages:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "access": 0, "color": "Example Name", "is_locked": true, "archived_at": "2024-01-01", "view_props": "example-value", "logo_props": "example-value", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "description_html": "

Example content

" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "access": 0, "color": "Example Name", "is_locked": true, "archived_at": "2024-01-01", "view_props": "example-value", "logo_props": "example-value", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "description_html": "

Example content

" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", access: 0, color: "Example Name", is_locked: true, archived_at: "2024-01-01", view_props: "example-value", logo_props: "example-value", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", description_html: "

Example content

", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "description_html": "

Example content

", "owned_by": "550e8400-e29b-41d4-a716-446655440000", "access": 0, "color": "Example Name", "is_locked": true, "archived_at": "2024-01-01", "workspace": "550e8400-e29b-41d4-a716-446655440000", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: 'https://developers.plane.so/api-reference/page/get-project-page.html' description: >- Retrieve a project page via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a project page. --- # Retrieve a project page Get a project page by ID ### Path Parameters The unique identifier of the page. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.pages:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/page/update-project-page.html' description: Update the title or content of a project page via the Plane API. --- # Update a project page Send `name`, `description_html`, or both. `description_html` replaces the current content, and Plane sanitizes it before storing it. See [Page content HTML](/api-reference/page/page-content-html) for supported HTML and Plane editor components. Locked or archived pages return `400`; document-service failures return `502`, and an unconfigured service returns `503`. ### Path Parameters The workspace's unique slug. The project UUID. The page UUID. ### Body Parameters The new page title. HTML that Plane sanitizes and uses to replace the current page content. At least one body parameter is required. ### Scopes `write` or `projects.pages:write` ```bash curl -X PUT "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"Release notes","description_html":"

Current release notes

"}' ``` ```python import requests response = requests.put("https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/", headers={"X-API-Key": "your-api-key"}, json={"name": "Release notes", "description_html": "

Current release notes

"}) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/", { method: "PUT", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ name: "Release notes", description_html: "

Current release notes

", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Release notes", "description_html": "

Current release notes

" } ``` --- --- url: 'https://developers.plane.so/api-reference/page/archive-project-page.html' description: Archive a project page via the Plane API before deleting it. --- # Archive a project page Archive a project page and its subpages. A page must be archived before it can be deleted. ### Path Parameters The workspace's unique slug. The project UUID. The page UUID. ### Scopes `write` or `projects.pages:write` ```bash curl -X POST "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/archive/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.post("https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/archive/", headers={"X-API-Key": "your-api-key"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/archive/", { method: "POST", headers: { "X-API-Key": process.env.PLANE_API_KEY } }, ); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/page/restore-project-page.html' description: Restore an archived project page via the Plane API. --- # Restore a project page Restore an archived project page. Restore an archived parent before restoring its child; otherwise the API returns `400`. ### Path Parameters The workspace's unique slug. The project UUID. The page UUID. ### Scopes `write` or `projects.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/archive/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.delete("https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/archive/", headers={"X-API-Key": "your-api-key"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/archive/", { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY } }, ); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/page/delete-project-page.html' description: Delete an archived project page via the Plane API. --- # Delete a project page Delete a project page. Archive the page first; deleting an active page returns `400`. ### Path Parameters The workspace's unique slug. The project UUID. The page UUID. ### Scopes `write` or `projects.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.delete("https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/", headers={"X-API-Key": "your-api-key"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/pages/page-uuid/", { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY } }, ); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/collection/overview.html' description: >- Organize Plane workspace pages into public or private collections and manage collection membership. --- # Collection API overview Collections organize workspace wiki pages. Public collections are visible according to workspace permissions; private collections are visible only to permitted users. A page and its sub-pages move together when the page is added, moved, or removed. ## Collection object ```json { "id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "name": "Product docs", "owned_by_id": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "access": 0, "current_user_access": null, "has_pages": true, "is_default": false, "is_global": true, "logo_props": { "emoji": "📚" }, "sort_order": 65535.0, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ``` `access` is `0` for public or `1` for private. `current_user_access` is `null` for a public collection. For a private collection, member access is `0` (view), `1` (comment), or `2` (edit). Collection access cannot be changed after creation. Private collections require the private collections feature, and only workspace admins can create them. The creator of a private collection is automatically added with edit access. Collection edit access allows a member to edit content and arrange pages, but it does not grant collection ownership. Only the collection owner or a workspace admin can add, update, or remove collection members. ## Common errors | Status | Cause | | ------ | -------------------------------------------------------------------------------------------------------------- | | `400` | Invalid request payload or page placement, or an attempt to modify or delete the protected default collection. | | `403` | Insufficient collection or workspace permissions, or unavailable private-collection feature access. | | `404` | The collection, page, membership, or other requested resource is unavailable or inaccessible to the caller. | --- --- url: 'https://developers.plane.so/api-reference/collection/list-collections.html' description: List the collections visible to the authenticated user in a Plane workspace. --- # List collections Returns all public and permitted private collections in sort order. A private collection is included when the caller owns it, is a workspace admin, or is an explicit collection member. ### Path Parameters The workspace slug. ### Scopes `read` or `wiki.pages:read` ```bash curl "https://api.plane.so/api/v1/workspaces/my-workspace/collections/" \ -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/collections/", headers={"X-API-Key": "your-api-key"}, ) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/collections/", { headers: { "X-API-Key": process.env.PLANE_API_KEY }, }); console.log(await response.json()); ``` ```json [ { "id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "name": "Product docs", "owned_by_id": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "access": 0, "current_user_access": null, "has_pages": true, "is_default": false, "is_global": true, "logo_props": {}, "sort_order": 65535.0, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ] ``` --- --- url: 'https://developers.plane.so/api-reference/collection/create-collection.html' description: Create a public or private collection in a Plane workspace. --- # Create a collection Creates a collection. Private collections can only be created by workspace admins when the feature is available. The creator of a private collection is automatically added as a member with edit access. ### Path Parameters The workspace slug. ### Body Parameters The collection name. Defaults to an empty string. `0` for public (default) or `1` for private. Logo or emoji properties. Defaults to an empty object. ### Scopes `write` or `wiki.pages:write` ```bash curl -X POST "https://api.plane.so/api/v1/workspaces/my-workspace/collections/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"Product docs","access":0,"logo_props":{"emoji":"📚"}}' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/collections/", headers={"X-API-Key": "your-api-key"}, json={"name": "Product docs", "access": 0, "logo_props": {"emoji": "📚"}}, ) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/collections/", { method: "POST", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ name: "Product docs", access: 0, logo_props: { emoji: "📚" } }), }); console.log(await response.json()); ``` ```json { "id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "name": "Product docs", "owned_by_id": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "access": 0, "current_user_access": null, "has_pages": false, "is_default": false, "is_global": true, "logo_props": { "emoji": "📚" }, "sort_order": 65535.0, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ``` --- --- url: 'https://developers.plane.so/api-reference/collection/retrieve-collection.html' description: Retrieve a visible Plane workspace collection by ID. --- # Retrieve a collection Returns a collection if the authenticated user can view it. An inaccessible private collection returns `404 Not Found`. ### Path Parameters The workspace slug. The collection ID. ### Scopes `read` or `wiki.pages:read` ```bash curl "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/" \ -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/" print(requests.get(url, headers={"X-API-Key": "your-api-key"}).json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/"; const response = await fetch(url, { headers: { "X-API-Key": process.env.PLANE_API_KEY } }); console.log(await response.json()); ``` ```json { "id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "name": "Product docs", "owned_by_id": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "access": 0, "current_user_access": null, "has_pages": true, "is_default": false, "is_global": true, "logo_props": {}, "sort_order": 65535.0, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ``` --- --- url: 'https://developers.plane.so/api-reference/collection/update-collection.html' description: 'Update a Plane collection''s name, logo, or sort order.' --- # Update a collection Partially updates a collection. Its public or private access cannot be changed after creation. ### Path Parameters The workspace slug. The collection ID. ### Body Parameters A new collection name. New logo or emoji properties. The collection's ordering value. ### Scopes `write` or `wiki.pages:write` ```bash curl -X PATCH "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"API docs","sort_order":25000}' ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/" response = requests.patch(url, headers={"X-API-Key": "your-api-key"}, json={"name": "API docs", "sort_order": 25000}) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/"; const response = await fetch(url, { method: "PATCH", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ name: "API docs", sort_order: 25000 }), }); console.log(await response.json()); ``` ```json { "id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "name": "API docs", "owned_by_id": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "access": 0, "current_user_access": null, "has_pages": true, "is_default": false, "is_global": true, "logo_props": {}, "sort_order": 25000.0, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:05:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ``` --- --- url: 'https://developers.plane.so/api-reference/collection/delete-collection.html' description: Delete a non-default Plane collection and optionally preserve its pages. --- # Delete a collection Deletes a non-default collection. Pages are archived by default. Set `archive_pages=false` to preserve pages when deleting a public collection; pages in a private collection are always archived. ### Path Parameters The workspace slug. The collection ID. ### Query Parameters Whether to archive contained pages. Defaults to `true`. ### Scopes `write` or `wiki.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/?archive_pages=false" \ -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/" response = requests.delete(url, headers={"X-API-Key": "your-api-key"}, params={"archive_pages": "false"}) print(response.status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10/?archive_pages=false"; const response = await fetch(url, { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY }, }); console.log(response.status); ``` ```text No response body ``` --- --- url: >- https://developers.plane.so/api-reference/collection/list-collection-members.html description: List users who have explicit access to a Plane collection. --- # List collection members Returns explicit collection memberships. ### Path Parameters The workspace slug. The collection ID. ### Scopes `read` or `wiki.pages:read` ```bash curl "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/" print(requests.get(url, headers={"X-API-Key": "your-api-key"}).json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/"; const response = await fetch(url, { headers: { "X-API-Key": process.env.PLANE_API_KEY } }); console.log(await response.json()); ``` ```json [ { "id": "4edec253-26f4-4667-8f52-9488dca1c620", "collection": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "member": "6f356c85-bb22-47e0-b8b1-cf18aa6adad3", "access": 0, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ] ``` --- --- url: >- https://developers.plane.so/api-reference/collection/add-collection-member.html description: Grant a workspace user access to a private Plane collection. --- # Add a collection member Adds an explicit member to a private collection. The caller must be able to manage the collection. ### Path Parameters The workspace slug. The collection ID. ### Body Parameters The workspace user's ID. `0` (view, default), `1` (comment), or `2` (edit). ### Scopes `write` or `wiki.pages:write` ```bash curl -X POST "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \ -d '{"member":"6f356c85-bb22-47e0-b8b1-cf18aa6adad3","access":0}' ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/" response = requests.post(url, headers={"X-API-Key": "your-api-key"}, json={"member": "6f356c85-bb22-47e0-b8b1-cf18aa6adad3", "access": 0}) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/"; const response = await fetch(url, { method: "POST", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ member: "6f356c85-bb22-47e0-b8b1-cf18aa6adad3", access: 0 }), }); console.log(await response.json()); ``` ```json { "id": "4edec253-26f4-4667-8f52-9488dca1c620", "collection": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "member": "6f356c85-bb22-47e0-b8b1-cf18aa6adad3", "access": 0, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ``` --- --- url: >- https://developers.plane.so/api-reference/collection/update-collection-member.html description: Change a Plane collection member's access level. --- # Update a collection member Changes an explicit collection membership's access level. `member_id` identifies the membership record, not the user. ### Path Parameters The workspace slug. The collection ID. The collection membership ID. ### Body Parameters `0` (view), `1` (comment), or `2` (edit). ### Scopes `write` or `wiki.pages:write` ```bash curl -X PATCH "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/membership-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" -d '{"access":2}' ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/membership-uuid/" print(requests.patch(url, headers={"X-API-Key": "your-api-key"}, json={"access": 2}).json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/membership-uuid/"; const response = await fetch(url, { method: "PATCH", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ access: 2 }), }); console.log(await response.json()); ``` ```json { "id": "4edec253-26f4-4667-8f52-9488dca1c620", "collection": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "member": "6f356c85-bb22-47e0-b8b1-cf18aa6adad3", "access": 2, "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:05:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ``` --- --- url: >- https://developers.plane.so/api-reference/collection/remove-collection-member.html description: Revoke a user's explicit access to a private Plane collection. --- # Remove a collection member Removes an explicit collection membership. `member_id` identifies the membership record, not the user. ### Path Parameters The workspace slug. The collection ID. The collection membership ID. ### Scopes `write` or `wiki.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/membership-uuid/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/membership-uuid/" print(requests.delete(url, headers={"X-API-Key": "your-api-key"}).status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/members/membership-uuid/"; const response = await fetch(url, { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY }, }); console.log(response.status); ``` ```text No response body ``` --- --- url: >- https://developers.plane.so/api-reference/collection/list-collection-pages.html description: List and filter a branch of pages in a Plane collection. --- # List collection pages Returns a paginated branch of visible pages. By default, it returns the collection's root pages. Pass `next_cursor` back as `cursor` to continue until `next_page_results` is `false`. ### Path Parameters The workspace slug. The collection ID. ### Query Parameters Return direct children of this page. Case-insensitive page-name search. Comma-separated creator IDs. Filter by the current user's favorite status. Comma-separated label IDs. Created on or after this date. Created on or before this date. Filter by owner. Comma-separated owner IDs. Comma-separated parent IDs. Results per page. Defaults to 50; maximum 100. Cursor returned by a previous page. ### Scopes `read` or `wiki.pages:read` ```bash curl "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/?search=API&per_page=50" \ -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/" response = requests.get(url, headers={"X-API-Key": "your-api-key"}, params={"search": "API", "per_page": 50}) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/?search=API&per_page=50"; const response = await fetch(url, { headers: { "X-API-Key": process.env.PLANE_API_KEY } }); console.log(await response.json()); ``` ```json { "next_cursor": "50:50:0", "prev_cursor": "50:0:0", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "page_collection_id": "55ebf2cc-61ba-478a-b88c-88db969e29dc", "collection_id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "parent_id": null, "sort_order": 65535.0, "page": { "id": "ea8ccdab-1cf4-448b-8205-51e4b98d82b8", "name": "API guide", "access": 0, "logo_props": {}, "parent_id": null, "collection_id": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "sub_pages_count": 2, "is_shared": false, "owned_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_at": "2026-08-18T10:00:00Z", "created_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "is_favorite": false, "label_ids": [] } } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/collection/search-collection-pages.html description: Search for workspace pages that can be added to a Plane collection. --- # Search addable collection pages Returns eligible root pages that are not already assigned to the collection. Results respect page and private collection permissions. Without `search`, the endpoint returns at most 10 results. With `search`, it performs a case-insensitive page-name search without that limit. ### Path Parameters The workspace slug. The destination collection ID. ### Query Parameters A page-name search string. ### Scopes `read` or `wiki.pages:read` ```bash curl "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages-search/?search=API" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages-search/" print(requests.get(url, headers={"X-API-Key": "your-api-key"}, params={"search": "API"}).json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages-search/?search=API"; const response = await fetch(url, { headers: { "X-API-Key": process.env.PLANE_API_KEY } }); console.log(await response.json()); ``` ```json [ { "id": "ea8ccdab-1cf4-448b-8205-51e4b98d82b8", "name": "API guide", "logo_props": { "emoji": "📘" } } ] ``` --- --- url: 'https://developers.plane.so/api-reference/collection/add-collection-pages.html' description: Add one or more Plane workspace page trees to a collection. --- # Add pages to a collection Adds each selected page and its sub-pages. Adding to a private collection makes the page tree private; adding an owned private page to a public collection makes it public. Private collections accept only root pages. ### Path Parameters The workspace slug. The destination collection ID. ### Body Parameters One or more workspace page IDs. Page-ID keys mapped to numeric sort orders. Every key must occur in `page_ids`. Placement with `type`: `append`, `before`, or `after`; optional `parent_id`; and required `target_page_id` for `before` or `after`. Before/after accepts exactly one page. `placement` takes precedence over `sort_orders`. An `append` placement accepts multiple pages and preserves their order from `page_ids`. ### Scopes `write` or `wiki.pages:write` ```bash curl -X POST "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \ -d '{"page_ids":["ea8ccdab-1cf4-448b-8205-51e4b98d82b8"],"placement":{"type":"append","parent_id":null}}' ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/" payload = {"page_ids": ["ea8ccdab-1cf4-448b-8205-51e4b98d82b8"], "placement": {"type": "append", "parent_id": None}} print(requests.post(url, headers={"X-API-Key": "your-api-key"}, json=payload).json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/"; const response = await fetch(url, { method: "POST", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ page_ids: ["ea8ccdab-1cf4-448b-8205-51e4b98d82b8"], placement: { type: "append", parent_id: null }, }), }); console.log(await response.json()); ``` ```json [ { "id": "55ebf2cc-61ba-478a-b88c-88db969e29dc", "collection": "0a8a3e6a-3c32-49c7-bbb5-b7a8e32c2f10", "page": "ea8ccdab-1cf4-448b-8205-51e4b98d82b8", "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "sort_order": 65535.0, "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ] ``` The response `id` is the page placement ID. Use it as `page_collection_id` when moving, reordering, or removing the page from a collection. --- --- url: >- https://developers.plane.so/api-reference/collection/move-or-reorder-collection-page.html description: Move a Plane page tree to another collection or change its collection order. --- # Move or reorder a collection page Reorders a page or moves its page tree to another visible collection. A private collection's sub-page cannot be moved independently. Moving a private page to a public collection makes its tree public when permitted. Moving a root page out of a private collection requires workspace admin access, or ownership of the page tree together with edit access to the source collection. Moving into a private collection also requires access to the destination collection. ### Path Parameters The workspace slug. The source collection ID. The page membership ID returned as `page_collection_id` by the list endpoint. ### Body Parameters A destination collection ID. Omit or use the source ID to reorder in place. An explicit ordering value. Placement with `type`: `append`, `before`, or `after`; optional `parent_id`; and required `target_page_id` for before/after. Overrides `sort_order`. ### Scopes `write` or `wiki.pages:write` ```bash curl -X PATCH "https://api.plane.so/api/v1/workspaces/my-workspace/collections/source-uuid/pages/membership-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \ -d '{"collection":"target-collection-uuid","placement":{"type":"append"}}' ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/source-uuid/pages/membership-uuid/" payload = {"collection": "target-collection-uuid", "placement": {"type": "append"}} print(requests.patch(url, headers={"X-API-Key": "your-api-key"}, json=payload).json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/source-uuid/pages/membership-uuid/"; const response = await fetch(url, { method: "PATCH", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ collection: "target-collection-uuid", placement: { type: "append" } }), }); console.log(await response.json()); ``` ```json { "id": "55ebf2cc-61ba-478a-b88c-88db969e29dc", "collection": "82c72604-2019-43b7-930f-692af61f3ea7", "page": "ea8ccdab-1cf4-448b-8205-51e4b98d82b8", "workspace": "95d1f03f-16e5-4807-a8c5-ec0c7cf0e4ab", "sort_order": 65535.0, "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:05:00Z", "created_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f", "updated_by": "d2f1b470-55e8-4e7a-8a61-7fc695bc1a4f" } ``` --- --- url: >- https://developers.plane.so/api-reference/collection/remove-collection-page.html description: Remove a Plane page tree from its collection. --- # Remove a page from a collection Removes the page and its descendants from the collection. A sub-page cannot be removed independently from a private collection. Removing a private collection root requires permission to move that page tree out of the collection. ### Path Parameters The workspace slug. The collection ID. The page membership ID returned as `page_collection_id` by the list endpoint. ### Scopes `write` or `wiki.pages:write` ```bash curl -X DELETE "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/membership-uuid/" -H "X-API-Key: $PLANE_API_KEY" ``` ```python import requests url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/membership-uuid/" print(requests.delete(url, headers={"X-API-Key": "your-api-key"}).status_code) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const url = "https://api.plane.so/api/v1/workspaces/my-workspace/collections/collection-uuid/pages/membership-uuid/"; const response = await fetch(url, { method: "DELETE", headers: { "X-API-Key": process.env.PLANE_API_KEY }, }); console.log(response.status); ``` ```text No response body ``` --- --- url: 'https://developers.plane.so/api-reference/intake-issue/overview.html' description: >- Plane Intake-Issue API overview. Learn about endpoints, request/response format, and how to work with intake-issue via REST API. --- # Overview Intake allows guests to create work items that admins and members can review and move into a project. [Learn more about Intake](https://docs.plane.so/core-concepts/intake) ## Enable Intake To enable the Intake feature, the user can hit a PATCH request on the project api with the body as ``` { intake_view:true, } ``` To create an Intake work item, the payload should be sent in the below format ```json { "issue": { "name": "Snoozed task 2", "priority": "high" } } ``` ### The Intake Object **Attribute** * `created_at` *timestamp* The timestamp of the time when the project was created * `updated_at` *timestamp* The timestamp of the time when the project was last updated * `status` the status of the work item can be in above mentioned status * -2 - Pending * -1 - Rejected * 0 - Snoozed * 1 - Accepted * 2 - Duplicate * `snoozed_till` The time untill the work item is snoozed. * `source` The source describes the type of intake from * `created_by` , `updated_by` *uuid* These values are auto saved and represent the id of the user that created or updated the module * `Project` uuid It contains projects uuid which is automatically saved. * `Workspace` uuid It contains workspace uuid which is automatically saved. * `inbox` intake id of the work item * `issue` work item id of the work item * `duplicate_to` Id of the work item of which the current work item is duplicate of. ```json { "id": "0de4d6d1-fdc7-4849-8080-dc379ab210e3", "pending_issue_count": 0, "created_at": "2023-11-21T07:32:26.072634Z", "updated_at": "2023-11-21T07:32:26.072648Z", "name": "a dummy project with Intake", "description": "", "is_default": true, "view_props": {}, "created_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "updated_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "project": "6436c4ae-fba7-45dc-ad4a-5440e17cb1b2", "workspace": "c467e125-59e3-44ec-b5ee-f9c1e138c611" } ``` --- --- url: 'https://developers.plane.so/api-reference/intake-issue/add-intake-issue.html' description: >- Create an intake work item via Plane API. HTTP request format, parameters, scopes, and example responses for create an intake work item. --- # Create an intake work item Submit a new work item to the project's intake queue for review and triage. Automatically creates the work item with default triage state and tracks activity. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Issue data for the intake issue ### Scopes `projects.intakes:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issue": { "name": "Example Name", "description": "Example description", "priority": "medium" } }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/", headers={"X-API-Key": "your-api-key"}, json={ "issue": { "name": "Example Name", "description": "Example description", "priority": "medium" } } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ issue: { name: "Example Name", description: "Example description", priority: "medium", }, }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "status": 0, "source": "in_app", "issue": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "priority": "medium", "sequence_id": 124 }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/intake-issue/list-intake-issues.html' description: >- List all intake work items via Plane API. HTTP request format, parameters, scopes, and example responses for list all intake work items. --- # List all intake work items Retrieve all work items in the project's intake queue. Returns paginated results when listing all intake work items. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Number of results per page (default: 20, max: 100) ### Scopes `projects.intakes:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/intake-issue/get-intake-issue-detail.html description: >- Retrieve an intake work item via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve an intake work item. --- # Retrieve an intake work item Retrieve details of a specific intake work item. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.intakes:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "status": 0, "source": "in_app", "issue": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "priority": "medium", "sequence_id": 124 }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/intake-issue/update-intake-issue-detail.html description: >- Update an intake work item via Plane API. HTTP request format, parameters, scopes, and example responses for update an intake work item. --- # Update an intake work item Modify an existing intake work item's properties or status for triage processing. Supports status changes like accept, reject, or mark as duplicate. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters * `-2` - Pending * `-1` - Rejected * `0` - Snoozed * `1` - Accepted * `2` - Duplicate Snoozed till. Duplicate to. Source. Source email. Issue data to update in the intake issue ### Scopes `projects.intakes:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": 1, "issue": { "name": "Example Name", "description": "Example description", "priority": "high" } }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "status": 1, "issue": { "name": "Example Name", "description": "Example description", "priority": "high" } } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ status: 1, issue: { name: "Example Name", description: "Example description", priority: "high", }, }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "status": 0, "source": "in_app", "issue": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "priority": "medium", "sequence_id": 124 }, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/intake-issue/delete-intake-issue.html description: >- Delete an intake work item via Plane API. HTTP request format, parameters, scopes, and example responses for delete an intake work item. --- # Delete an intake work item Permanently remove an intake work item from the triage queue. Also deletes the underlying work item if it hasn't been accepted yet. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.intakes:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/intake-issues/work-item-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/assets/overview.html' description: >- Plane Assets API overview. Learn how user and workspace asset uploads work through the Plane API. --- # Overview Assets let you upload and manage files used across user profiles and workspaces, including images and other binary resources. [Learn more about using the Plane API](https://developers.plane.so/api-reference/introduction) ## The Asset Object ### Attributes ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "design-spec.pdf", "type": "application/pdf", "asset_url": "https://cdn.example.com/workspace-assets/design-spec.pdf", "attributes": { "entity_type": "WORKSPACE" } } ``` --- --- url: 'https://developers.plane.so/api-reference/assets/create-user-asset-upload.html' description: >- Create user asset upload via Plane API. HTTP request format, parameters, scopes, and example responses for create user asset upload. --- # Create user asset upload Generate presigned URL for user asset upload ### Body Parameters Original filename of the asset MIME type of the file * `image/jpeg` - JPEG * `image/png` - PNG * `image/webp` - WebP * `image/jpg` - JPG * `image/gif` - GIF File size in bytes Type of user asset * `USER_AVATAR` - User Avatar * `USER_COVER` - User Cover ### Scopes `assets:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/assets/user-assets/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "type": "image/jpeg", "size": 1024000, "entity_type": "USER_AVATAR" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/assets/user-assets/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "type": "image/jpeg", "size": 1024000, "entity_type": "USER_AVATAR" } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/assets/user-assets/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", type: "image/jpeg", size: 1024000, entity_type: "USER_AVATAR", }), }); const data = await response.json(); ``` ```json { "asset_id": "550e8400-e29b-41d4-a716-446655440000", "asset_url": "/api/assets/v2/static/550e8400-e29b-41d4-a716-446655440000/", "upload_data": { "url": "https://uploads.example.com/plane-bucket", "fields": { "Content-Type": "image/png", "key": "user-assets/550e8400-e29b-41d4-a716-446655440000/profile-image.png", "x-amz-algorithm": "AWS4-HMAC-SHA256", "x-amz-credential": "example/20240101/us-east-1/s3/aws4_request", "x-amz-date": "20240101T000000Z", "policy": "example-policy", "x-amz-signature": "example-signature" } } } ``` --- --- url: 'https://developers.plane.so/api-reference/assets/update-user-asset.html' description: >- Update user asset via Plane API. HTTP request format, parameters, scopes, and example responses for update user asset. --- # Update user asset Mark user asset as uploaded ### Path Parameters Asset ID ### Body Parameters Additional attributes to update for the asset ### Scopes `assets:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/assets/user-assets/asset-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "attributes": { "name": "Example Name", "type": "image/jpeg", "size": 1024000 }, "entity_type": "USER_AVATAR" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/assets/user-assets/asset-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "attributes": { "name": "Example Name", "type": "image/jpeg", "size": 1024000 }, "entity_type": "USER_AVATAR" } ) print(response.status_code) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/assets/user-assets/asset-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ attributes: { name: "Example Name", type: "image/jpeg", size: 1024000, }, entity_type: "USER_AVATAR", }), }); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/assets/delete-user-asset.html' description: >- Delete user asset via Plane API. HTTP request format, parameters, scopes, and example responses for delete user asset. --- # Delete user asset Delete user asset. Delete a user profile asset (avatar or cover image) and remove its reference from the user profile. This performs a soft delete by marking the asset as deleted and updating the user's profile. ### Path Parameters Asset ID ### Scopes `assets:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/assets/user-assets/asset-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/assets/user-assets/asset-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/assets/user-assets/asset-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/assets/create-workspace-asset-upload.html description: >- Create workspace asset upload via Plane API. HTTP request format, parameters, scopes, and example responses for create workspace asset upload. --- # Create workspace asset upload Generate a presigned URL for a workspace asset upload. To attach a file to a workspace page, set `entity_type` to `PAGE_DESCRIPTION` and pass the page UUID as `entity_identifier`. The page must be editable, unlocked, and active. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Original filename of the asset MIME type of the file File size in bytes UUID of the project to associate with the asset Asset context. Use `PAGE_DESCRIPTION` for a workspace page attachment; when you do, `entity_identifier` is also required. UUID of the workspace page. This value and `entity_type` are required together for a `PAGE_DESCRIPTION` attachment. External identifier for the asset (for integration tracking) External source system (for integration tracking) ### Scopes `assets:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/assets/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "type": "image/jpeg", "size": 1024000, "entity_type": "PAGE_DESCRIPTION", "entity_identifier": "4d2f6f7e-9b4a-4d6a-8f4a-1c3f7c0f4a10", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/assets/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "type": "image/jpeg", "size": 1024000, "entity_type": "PAGE_DESCRIPTION", "entity_identifier": "4d2f6f7e-9b4a-4d6a-8f4a-1c3f7c0f4a10", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript // Run this example server-side. Browser apps must call your backend to keep the API key secret. const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/assets/", { method: "POST", headers: { "X-API-Key": process.env.PLANE_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", type: "image/jpeg", size: 1024000, entity_type: "PAGE_DESCRIPTION", entity_identifier: "4d2f6f7e-9b4a-4d6a-8f4a-1c3f7c0f4a10", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }); const data = await response.json(); ``` ```json { "asset_id": "550e8400-e29b-41d4-a716-446655440000", "asset_url": "/api/v1/workspaces/my-workspace/pages/4d2f6f7e-9b4a-4d6a-8f4a-1c3f7c0f4a10/attachments/550e8400-e29b-41d4-a716-446655440000/", "upload_data": { "url": "https://uploads.example.com/plane-bucket", "fields": { "Content-Type": "image/png", "key": "workspace-assets/550e8400-e29b-41d4-a716-446655440000/workspace-image.png", "x-amz-algorithm": "AWS4-HMAC-SHA256", "x-amz-credential": "example/20240101/us-east-1/s3/aws4_request", "x-amz-date": "20240101T000000Z", "policy": "example-policy", "x-amz-signature": "example-signature" } } } ``` --- --- url: 'https://developers.plane.so/api-reference/assets/get-workspace-asset.html' description: >- Get workspace asset via Plane API. HTTP request format, parameters, scopes, and example responses for get workspace asset. --- # Get workspace asset Get presigned URL for asset download ### Path Parameters The unique identifier of the asset. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `assets:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/assets/asset-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/assets/asset-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/assets/asset-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "design-spec.pdf", "type": "application/pdf", "asset_url": "https://cdn.example.com/workspace-assets/design-spec.pdf", "attributes": { "entity_type": "WORKSPACE" } } ``` --- --- url: 'https://developers.plane.so/api-reference/assets/update-workspace-asset.html' description: >- Update workspace asset via Plane API. HTTP request format, parameters, scopes, and example responses for update workspace asset. --- # Update workspace asset Update generic asset after upload completion ### Path Parameters The unique identifier of the asset. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Whether the asset has been successfully uploaded ### Scopes `assets:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/assets/asset-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "is_uploaded": true }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/assets/asset-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "is_uploaded": true } ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/assets/asset-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ is_uploaded: true, }), }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/milestones/overview.html' description: >- Plane Milestones API overview. Learn how to manage milestones and milestone work items through the Plane API. --- # Overview Milestones help teams group work items around important dates, releases, and delivery checkpoints inside a project. [Learn more about Projects](https://docs.plane.so/core-concepts/projects/overview) ## The Milestone Object ### Attributes * `id` *string* Id. * `title` *string* Title. * `target_date` *string* Target date. * `external_id` *string* External id. * `external_source` *string* External source. * `created_at` *string* Created at. * `updated_at` *string* Updated at. ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/milestones/add-milestone.html' description: >- Create milestone via Plane API. HTTP request format, parameters, scopes, and example responses for create milestone. --- # Create milestone Create a new milestone in a project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Title. Target date. External id. External source. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/", headers={"X-API-Key": "your-api-key"}, json={ "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ title: "Example Name", target_date: "2024-01-01", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/milestones/list-milestones.html' description: >- List milestones via Plane API. HTTP request format, parameters, scopes, and example responses for list milestones. --- # List milestones List all milestones in a project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ] ``` --- --- url: 'https://developers.plane.so/api-reference/milestones/get-milestone-detail.html' description: >- Get milestone via Plane API. HTTP request format, parameters, scopes, and example responses for get milestone. --- # Get milestone Retrieve a specific milestone by its ID. ### Path Parameters The unique identifier of the milestone. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/milestones/list-milestone-work-items.html description: >- List milestone work items via Plane API. HTTP request format, parameters, scopes, and example responses for list milestone work items. --- # List milestone work items List all work items for a milestone. ### Path Parameters The unique identifier of the milestone. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/work-items/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/work-items/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/work-items/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "issue": "550e8400-e29b-41d4-a716-446655440000", "milestone": "550e8400-e29b-41d4-a716-446655440000" } ] ``` --- --- url: >- https://developers.plane.so/api-reference/milestones/update-milestone-detail.html description: >- Update milestone via Plane API. HTTP request format, parameters, scopes, and example responses for update milestone. --- # Update milestone Update a specific milestone by its ID. ### Path Parameters The unique identifier of the milestone. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Title. Target date. External id. External source. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ title: "Example Name", target_date: "2024-01-01", external_id: "550e8400-e29b-41d4-a716-446655440000", external_source: "github", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Example Name", "target_date": "2024-01-01", "external_id": "550e8400-e29b-41d4-a716-446655440000", "external_source": "github", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/milestones/delete-milestone.html' description: >- Delete milestone via Plane API. HTTP request format, parameters, scopes, and example responses for delete milestone. --- # Delete milestone Delete a specific milestone by its ID. ### Path Parameters The unique identifier of the milestone. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes API key authentication or an OAuth token with equivalent access. ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/milestones/milestone-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/estimate/overview.html' description: >- Plane Estimate API overview. Learn about estimate objects, estimate points, and how to work with estimates via REST API. --- # Overview Estimates define how work is sized in a project. An estimate represents the scale (categories, points, or time), and estimate points represent the allowed values on work items. ## The Estimate Object ### Attributes * `name` *string* **(required)** Name of the estimate * `description` *string* Description of the estimate * `type` *string* Type of estimate. Possible values: `categories`, `points`, `time` * `last_used` *boolean* Whether this estimate is the most recently used estimate for the project * `external_id` *string* or *null* External ID from an external system * `external_source` *string* or *null* External source identifier * `created_at`, `updated_at` *timestamp* Timestamps when the estimate was created and last updated * `created_by`, `updated_by` *uuid* IDs of the users who created and last updated the estimate * `project` *uuid* Project ID associated with the estimate * `workspace` *uuid* Workspace ID associated with the estimate ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-20T12:10:00Z", "name": "Story Points", "description": "Standard story point scale", "type": "points", "last_used": true, "external_id": "sp-001", "external_source": "jira", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` ## The Estimate Point Object ### Attributes * `estimate` *uuid* **(required)** Estimate ID this point belongs to * `key` *integer* Numeric key used for ordering and display * `value` *string* **(required)** Display value for the estimate point (max 20 characters) * `description` *string* Description of the estimate point * `external_id` *string* or *null* External ID from an external system * `external_source` *string* or *null* External source identifier * `created_at`, `updated_at` *timestamp* Timestamps when the estimate point was created and last updated * `created_by`, `updated_by` *uuid* IDs of the users who created and last updated the estimate point * `project` *uuid* Project ID associated with the estimate point * `workspace` *uuid* Workspace ID associated with the estimate point ```json { "id": "550e8400-e29b-41d4-a716-446655440010", "created_at": "2024-01-15T10:40:00Z", "updated_at": "2024-01-20T12:15:00Z", "estimate": "550e8400-e29b-41d4-a716-446655440000", "key": 3, "value": "3", "description": "Small", "external_id": "sp-3", "external_source": "jira", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/estimate/add-estimate.html' description: >- Create an estimate via Plane API. HTTP request format, parameters, scopes, and example responses for create an estimate. --- # Create an estimate Create a new estimate for the project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name of the estimate. Description of the estimate. Type of estimate. Possible values: `categories`, `points`, `time`. Whether this estimate is the most recently used estimate for the project. External ID from an external system. External source identifier. ### Scopes `projects.estimates:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Story Points", "description": "Standard story point scale", "type": "points" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Story Points", "description": "Standard story point scale", "type": "points", }, ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Story Points", description: "Standard story point scale", type: "points", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-20T12:10:00Z", "name": "Story Points", "description": "Standard story point scale", "type": "points", "last_used": true, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/estimate/get-estimate.html' description: >- Get an estimate via Plane API. HTTP request format, parameters, scopes, and example responses for get an estimate. --- # Get an estimate Retrieve the estimate configured for a project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.estimates:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-20T12:10:00Z", "name": "Story Points", "description": "Standard story point scale", "type": "points", "last_used": true, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/estimate/update-estimate.html' description: >- Update an estimate via Plane API. HTTP request format, parameters, scopes, and example responses for update an estimate. --- # Update an estimate Update the estimate for a project. Only fields provided in the request will be updated. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name of the estimate. Description of the estimate. External ID from an external system. External source identifier. ### Scopes `projects.estimates:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "Updated story point scale" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", headers={"X-API-Key": "your-api-key"}, json={"description": "Updated story point scale"}, ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ description: "Updated story point scale", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-21T09:30:00Z", "name": "Story Points", "description": "Updated story point scale", "type": "points", "last_used": true, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/estimate/delete-estimate.html' description: >- Delete an estimate via Plane API. HTTP request format, parameters, scopes, and example responses for delete an estimate. --- # Delete an estimate Delete the estimate configured for a project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.estimates:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/estimate/list-estimate-points.html' description: >- List estimate points via Plane API. HTTP request format, parameters, scopes, and example responses for list estimate points. --- # List estimate points Retrieve all estimate points for a project estimate. ### Path Parameters The unique identifier of the estimate. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.estimates:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440010", "created_at": "2024-01-15T10:40:00Z", "updated_at": "2024-01-20T12:15:00Z", "estimate": "550e8400-e29b-41d4-a716-446655440000", "key": 1, "value": "1", "description": "Tiny", "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" }, { "id": "550e8400-e29b-41d4-a716-446655440011", "created_at": "2024-01-15T10:41:00Z", "updated_at": "2024-01-20T12:16:00Z", "estimate": "550e8400-e29b-41d4-a716-446655440000", "key": 2, "value": "2", "description": "Small", "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ] ``` --- --- url: 'https://developers.plane.so/api-reference/estimate/add-estimate-points.html' description: >- Create estimate points via Plane API. HTTP request format, parameters, scopes, and example responses for create estimate points. --- # Create estimate points Create estimate points for a project estimate. You can send a JSON array directly or wrap it inside `estimate_points`. ### Path Parameters The unique identifier of the estimate. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Array of estimate point objects. Each object can include: `value` (string, required, max 20 chars), `key` (integer), `description` (string), `external_id` (string), and `external_source` (string). ### Scopes `projects.estimates:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "key": 1, "value": "1", "description": "Tiny" }, { "key": 2, "value": "2", "description": "Small" } ]' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/", headers={"X-API-Key": "your-api-key"}, json=[ {"key": 1, "value": "1", "description": "Tiny"}, {"key": 2, "value": "2", "description": "Small"}, ], ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify([ { key: 1, value: "1", description: "Tiny" }, { key: 2, value: "2", description: "Small" }, ]), }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440010", "created_at": "2024-01-15T10:40:00Z", "updated_at": "2024-01-20T12:15:00Z", "estimate": "550e8400-e29b-41d4-a716-446655440000", "key": 1, "value": "1", "description": "Tiny", "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" }, { "id": "550e8400-e29b-41d4-a716-446655440011", "created_at": "2024-01-15T10:41:00Z", "updated_at": "2024-01-20T12:16:00Z", "estimate": "550e8400-e29b-41d4-a716-446655440000", "key": 2, "value": "2", "description": "Small", "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ] ``` --- --- url: 'https://developers.plane.so/api-reference/estimate/update-estimate-point.html' description: >- Update an estimate point via Plane API. HTTP request format, parameters, scopes, and example responses for update an estimate point. --- # Update an estimate point Update a single estimate point for a project estimate. ### Path Parameters The unique identifier of the estimate point. The unique identifier of the estimate. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Numeric key used for ordering and display. Display value for the estimate point (max 20 characters). Description of the estimate point. External ID from an external system. External source identifier. ### Scopes `projects.estimates:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/estimate-point-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "value": "3", "description": "Small" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/estimate-point-uuid/", headers={"X-API-Key": "your-api-key"}, json={"value": "3", "description": "Small"}, ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/estimate-point-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ value: "3", description: "Small", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440010", "created_at": "2024-01-15T10:40:00Z", "updated_at": "2024-01-21T09:45:00Z", "estimate": "550e8400-e29b-41d4-a716-446655440000", "key": 3, "value": "3", "description": "Small", "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "project": "4af68566-94a4-4eb3-94aa-50dc9427067b", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/estimate/delete-estimate-point.html' description: >- Delete an estimate point via Plane API. HTTP request format, parameters, scopes, and example responses for delete an estimate point. --- # Delete an estimate point Delete a single estimate point from a project estimate. ### Path Parameters The unique identifier of the estimate point. The unique identifier of the estimate. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.estimates:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/estimate-point-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/estimate-point-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/estimates/estimate-uuid/estimate-points/estimate-point-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/worklogs/overview.html' description: >- Plane Worklogs API overview. Learn about endpoints, request/response format, and how to work with worklogs via REST API. --- # Overview Worklogs enable time tracking for work items within a project, recording time spent in minutes along with descriptions and user information. [Learn more about Time tracking](https://docs.plane.so/core-concepts/issues/time-tracking) ## The Worklogs Object ### Attributes * `id` *string* Unique identifier for the worklog * `created_at` *timestamp* Timestamp when the worklog was created * `updated_at` *timestamp* Timestamp when the worklog was last modified * `deleted_at` *timestamp* Timestamp when the worklog was deleted * `description` *string* Description of the work done during the worklog * `duration` *integer* Time spent on the issue, recorded in minutes * `created_by` *string* ID of user who created the worklog * `updated_by` *string* ID of user who last modified the worklog * `project_id` *string* ID of project associated with the worklog * `workspace_id` *string* ID of workspace associated with the worklog * `logged_by` *string* ID of the user who logged the work ```json { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "created_at": "2025-01-29T21:27:54.197306+05:30", "updated_at": "2025-01-29T21:27:54.197320+05:30", "description": "", "duration": 1, "created_by": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "updated_by": null, "project_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "workspace_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "logged_by": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } ``` --- --- url: 'https://developers.plane.so/api-reference/worklogs/create-worklog.html' description: >- Create a worklog via Plane API. HTTP request format, parameters, scopes, and example responses for create a worklog. --- # Create a worklog Create a new worklog entry ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Description. Duration. Created by. Updated by. ### Scopes `projects.work_items.worklogs:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "Example description", "duration": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/", headers={"X-API-Key": "your-api-key"}, json={ "description": "Example description", "duration": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ description: "Example description", duration: 1, created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "description": "Example description", "duration": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "project_id": "550e8400-e29b-41d4-a716-446655440000", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "logged_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: 'https://developers.plane.so/api-reference/worklogs/get-worklogs-for-issue.html' description: >- List all worklogs for a work item via Plane API. HTTP request format, parameters, scopes, and example responses for list all worklogs for a work item. --- # List all worklogs for a work item List worklog entries ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.worklogs:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "description": "Example description", "duration": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "project_id": "550e8400-e29b-41d4-a716-446655440000", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "logged_by": "550e8400-e29b-41d4-a716-446655440000" } ] ``` --- --- url: 'https://developers.plane.so/api-reference/worklogs/get-total-time.html' description: >- Get total time for each work item via Plane API. HTTP request format, parameters, scopes, and example responses for get total time for each work item. --- # Get total time for each work item Get project worklog summary ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.worklogs:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/total-worklogs/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/total-worklogs/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/total-worklogs/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "issue_id": "550e8400-e29b-41d4-a716-446655440000", "duration": 1 } ] ``` --- --- url: 'https://developers.plane.so/api-reference/worklogs/update-worklog.html' description: >- Update a worklog via Plane API. HTTP request format, parameters, scopes, and example responses for update a worklog. --- # Update a worklog Update a worklog entry ### Path Parameters The unique identifier of the work item. The unique identifier of the worklog. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Description. Duration. Created by. Updated by. ### Scopes `projects.work_items.worklogs:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/worklog-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "Example description", "duration": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/worklog-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "description": "Example description", "duration": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/worklog-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ description: "Example description", duration: 1, created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "description": "Example description", "duration": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "project_id": "550e8400-e29b-41d4-a716-446655440000", "workspace_id": "550e8400-e29b-41d4-a716-446655440000", "logged_by": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: 'https://developers.plane.so/api-reference/worklogs/delete-worklog.html' description: >- Delete a worklog via Plane API. HTTP request format, parameters, scopes, and example responses for delete a worklog. --- # Delete a worklog Delete a worklog entry ### Path Parameters The unique identifier of the work item. The unique identifier of the worklog. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.work_items.worklogs:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/worklog-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/worklog-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/worklogs/worklog-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/epics/overview.html' description: >- Plane Epics API overview. Learn about endpoints, request/response format, and how to work with epics via REST API. --- # Overview Epics help you group related tasks into a larger work item, providing a hierarchical structure for managing complex projects. Use epics to break down major objectives into smaller, manageable pieces while keeping everything organized. [Learn more about Epics](https://docs.plane.so/core-concepts/issues/epics). ## The Epics Object ### Attributes * `id` string Unique identifier for the epic. * `name` string Name of the epic. * `description` object JSON representation of the epic description. * `description_html` string HTML-formatted description of the epic. * `description_stripped` string Plain text version of the description. * `description_binary` string Binary representation of the description. * `state` string ID of the state (status) of the epic. * `priority` string Priority level. Possible values: `none`, `urgent`, `high`, `medium`, `low`. * `assignees` array Array of user IDs assigned to the epic. * `labels` array Array of label IDs applied to the epic. * `type` string ID of the work item type for the epic. * `estimate_point` string ID of the estimate point, or null if not estimated. * `point` integer Point value for the epic, or null. * `start_date` string Start date of the epic in YYYY-MM-DD format. * `target_date` string Target completion date in YYYY-MM-DD format. * `parent` string ID of the parent work item, or null if no parent. * `sequence_id` integer Auto-generated sequential identifier for the epic within the project. * `sort_order` number Auto-generated sort order for display purposes. * `is_draft` boolean Whether the epic is a draft. * `completed_at` timestamp Time at which the epic was completed, or null if not completed. * `archived_at` timestamp Time at which the epic was archived, or null if not archived. * `project` string ID of the project containing this epic. * `workspace` string ID of the workspace containing this epic. * `external_id` string External identifier if imported from another system, or null. * `external_source` string Name of the source system if imported, or null. * `deleted_at` timestamp Time at which the epic was deleted, or null if not deleted. * `created_at` timestamp Time at which the epic was created. * `updated_at` timestamp Time at which the epic was last updated. * `created_by` string ID of the user who created the epic. * `updated_by` string ID of the user who last updated the epic. ```json { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "name": "Develop Mobile Application Framework", "description": {}, "description_html": "

Create a cross-platform mobile application framework that supports all core system functionalities with native-like performance and user experience

", "description_stripped": "Create a cross-platform mobile application framework that supports all core system functionalities with native-like performance and user experience", "description_binary": null, "state": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "priority": "medium", "assignees": [], "labels": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], "type": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "estimate_point": null, "point": null, "start_date": "2025-02-28", "target_date": "2025-06-20", "parent": null, "sequence_id": 57, "sort_order": 605535.0, "is_draft": false, "completed_at": null, "archived_at": null, "project": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "workspace": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "external_id": null, "external_source": null, "deleted_at": null, "created_at": "2025-03-01T21:23:54.645263+05:30", "updated_at": "2025-03-03T10:38:44.667276+05:30", "created_by": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "updated_by": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } ``` --- --- url: 'https://developers.plane.so/api-reference/epics/create-epic.html' description: >- Create an epic via Plane API. HTTP request format, parameters, scopes, and example responses for create an epic. --- # Create an epic Create a new epic in the specified project with the provided details. ### Path Parameters Project ID Workspace slug ### Body Parameters Name of the epic. HTML-formatted description of the epic. ID of the state (status) to assign to the epic. ID of the parent work item. Priority level. Possible values: `none`, `urgent`, `high`, `medium`, `low`. Start date of the epic in YYYY-MM-DD format. Target completion date in YYYY-MM-DD format. List of user IDs to assign to the epic. List of label IDs to apply to the epic. ID of the estimate point. Name of the source system if importing from another tool. External identifier from the source system. ### Scopes `projects.epics:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Develop Mobile Application Framework", "description_html": "

Create a cross-platform mobile application framework

", "priority": "high", "start_date": "2025-03-01", "target_date": "2025-06-30" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Develop Mobile Application Framework", "description_html": "

Create a cross-platform mobile application framework

", "priority": "high", "start_date": "2025-03-01", "target_date": "2025-06-30" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Develop Mobile Application Framework", description_html: "

Create a cross-platform mobile application framework

", priority: "high", start_date: "2025-03-01", target_date: "2025-06-30", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Develop Mobile Application Framework", "description": {}, "description_html": "

Create a cross-platform mobile application framework

", "description_stripped": "Create a cross-platform mobile application framework", "description_binary": null, "state": "550e8400-e29b-41d4-a716-446655440001", "priority": "high", "assignees": [], "labels": [], "type": "550e8400-e29b-41d4-a716-446655440002", "estimate_point": null, "point": null, "start_date": "2025-03-01", "target_date": "2025-06-30", "parent": null, "sequence_id": 57, "sort_order": 605535.0, "is_draft": false, "completed_at": null, "archived_at": null, "project": "550e8400-e29b-41d4-a716-446655440000", "workspace": "550e8400-e29b-41d4-a716-446655440003", "external_id": null, "external_source": null, "deleted_at": null, "created_at": "2025-03-01T21:23:54.645263Z", "updated_at": "2025-03-01T21:23:54.645263Z", "created_by": "550e8400-e29b-41d4-a716-446655440004", "updated_by": null } ``` --- --- url: 'https://developers.plane.so/api-reference/epics/list-epics.html' description: >- List all epics via Plane API. HTTP request format, parameters, scopes, and example responses for list all epics. --- # List all epics List epics ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `projects.epics:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/epics/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/epics/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/epics/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/epics/get-epic-detail.html' description: >- Retrieve an epic via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve an epic. --- # Retrieve an epic Retrieve an epic by id ### Path Parameters The unique identifier of the resource. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Comma-separated list of fields to include in response ### Scopes `projects.epics:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/epics/resource-id-uuid/?fields=id,name,description" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/epics/resource-id-uuid/?fields=id,name,description", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/epics/resource-id-uuid/?fields=id,name,description", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/epics/update-epic.html' description: >- Update an epic via Plane API. HTTP request format, parameters, scopes, and example responses for update an epic. --- # Update an epic Partially update an existing epic with the provided fields. Supports external ID validation to prevent conflicts. ### Path Parameters Epic ID Project ID Workspace slug ### Body Parameters Name of the epic. HTML-formatted description of the epic. ID of the state (status) to assign to the epic. ID of the parent work item. Priority level. Possible values: `none`, `urgent`, `high`, `medium`, `low`. Start date of the epic in YYYY-MM-DD format. Target completion date in YYYY-MM-DD format. List of user IDs to assign to the epic. List of label IDs to apply to the epic. ID of the estimate point. Name of the source system if importing from another tool. External identifier from the source system. ### Scopes `projects.epics:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Epic Name", "priority": "medium", "target_date": "2025-09-30" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Updated Epic Name", "priority": "medium", "target_date": "2025-09-30" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Updated Epic Name", priority: "medium", target_date: "2025-09-30", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Updated Epic Name", "description": {}, "description_html": "

Create a cross-platform mobile application framework

", "description_stripped": "Create a cross-platform mobile application framework", "description_binary": null, "state": "550e8400-e29b-41d4-a716-446655440002", "priority": "medium", "assignees": [], "labels": [], "type": "550e8400-e29b-41d4-a716-446655440003", "estimate_point": null, "point": null, "start_date": "2025-03-01", "target_date": "2025-09-30", "parent": null, "sequence_id": 57, "sort_order": 605535.0, "is_draft": false, "completed_at": null, "archived_at": null, "project": "550e8400-e29b-41d4-a716-446655440000", "workspace": "550e8400-e29b-41d4-a716-446655440004", "external_id": null, "external_source": null, "deleted_at": null, "created_at": "2025-03-01T21:23:54.645263Z", "updated_at": "2025-03-05T14:12:00.123456Z", "created_by": "550e8400-e29b-41d4-a716-446655440005", "updated_by": "550e8400-e29b-41d4-a716-446655440005" } ``` --- --- url: 'https://developers.plane.so/api-reference/epics/delete-epic.html' description: >- Delete an epic via Plane API. HTTP request format, parameters, scopes, and example responses for delete an epic. --- # Delete an epic Permanently delete an existing epic from the project. Child work items will have their parent unset. ### Path Parameters Epic ID Project ID Workspace slug ### Scopes `projects.epics:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/epics/add-epic-work-items.html' description: >- Add work items to epic via Plane API. HTTP request format, parameters, scopes, and example responses for add work items to epic. --- # Add work items to epic Add multiple work items as sub-issues under an epic. Validates type hierarchy before assignment. ### Path Parameters Epic ID Project ID Workspace slug ### Body Parameters List of work item IDs to add to the epic ### Scopes `projects.epics:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "work_item_ids": [ "550e8400-e29b-41d4-a716-446655440010", "550e8400-e29b-41d4-a716-446655440011" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/issues/", headers={"X-API-Key": "your-api-key"}, json={ "work_item_ids": [ "550e8400-e29b-41d4-a716-446655440010", "550e8400-e29b-41d4-a716-446655440011" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/issues/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ work_item_ids: [ "550e8400-e29b-41d4-a716-446655440010", "550e8400-e29b-41d4-a716-446655440011", ], }), }, ); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440010", "name": "Implement login screen", "description_html": "

Build the login screen UI

", "description_stripped": "Build the login screen UI", "description_binary": null, "state": "550e8400-e29b-41d4-a716-446655440002", "priority": "high", "assignees": [], "labels": [], "type": null, "type_id": null, "estimate_point": null, "point": null, "start_date": null, "target_date": null, "parent": "550e8400-e29b-41d4-a716-446655440001", "sequence_id": 12, "sort_order": 65535.0, "is_draft": false, "completed_at": null, "archived_at": null, "last_activity_at": "2025-03-15T10:00:00Z", "project": "550e8400-e29b-41d4-a716-446655440000", "workspace": "550e8400-e29b-41d4-a716-446655440003", "external_id": null, "external_source": null, "deleted_at": null, "created_at": "2025-03-10T09:00:00Z", "updated_at": "2025-03-15T10:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440005", "updated_by": null } ] ``` --- --- url: 'https://developers.plane.so/api-reference/epics/list-epic-work-items.html' description: >- List epic work items via Plane API. HTTP request format, parameters, scopes, and example responses for list epic work items. --- # List epic work items Retrieve all work items under an epic. ### Path Parameters Epic ID Project ID Workspace slug ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `projects.epics:read` `projects.work-items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/issues/?per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/issues/?per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/550e8400-e29b-41d4-a716-446655440000/epics/550e8400-e29b-41d4-a716-446655440001/issues/?per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": null, "sub_grouped_by": null, "total_count": 5, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": false, "prev_page_results": false, "count": 5, "total_pages": 1, "total_results": 5, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440010", "name": "Implement login screen", "description_html": "

Build the login screen UI

", "description_stripped": "Build the login screen UI", "description_binary": null, "state": "550e8400-e29b-41d4-a716-446655440002", "priority": "high", "assignees": ["550e8400-e29b-41d4-a716-446655440005"], "labels": [], "type": null, "type_id": null, "estimate_point": null, "point": null, "start_date": "2025-03-10", "target_date": "2025-03-20", "parent": "550e8400-e29b-41d4-a716-446655440001", "sequence_id": 12, "sort_order": 65535.0, "is_draft": false, "completed_at": null, "archived_at": null, "last_activity_at": "2025-03-15T10:00:00Z", "project": "550e8400-e29b-41d4-a716-446655440000", "workspace": "550e8400-e29b-41d4-a716-446655440003", "external_id": null, "external_source": null, "deleted_at": null, "created_at": "2025-03-10T09:00:00Z", "updated_at": "2025-03-15T10:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440005", "updated_by": null } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/initiative/overview.html' description: >- Plane Initiative API overview. Learn about endpoints, request/response format, and how to work with initiative via REST API. --- # Overview Initiatives are high-level strategic goals that help organize and track work across multiple projects, epics, and work items, providing a way to group related work and measure progress toward larger objectives. [Learn more about Initiatives](https://docs.plane.so/core-concepts/projects/initiatives) ## The Initiatives Object ### Attributes * `id` *uuid* Unique identifier for the initiative * `name` *string* **(required)** Name of the initiative * `description` *string* Plain text description of the initiative * `description_html` *string* HTML description of the initiative * `description_stripped` *string* Stripped version of the HTML description * `description_binary` *string* Binary description of the initiative * `lead` *uuid* User ID of the initiative lead * `start_date` *date* Start date of the initiative in YYYY-MM-DD format * `end_date` *date* End date of the initiative in YYYY-MM-DD format * `logo_props` *object* Logo properties for the initiative * `state` *string* State of the initiative. Can be: DRAFT, PLANNED, ACTIVE, COMPLETED, CLOSED * `workspace` *uuid* Workspace UUID which is automatically saved * `created_at` *timestamp* The timestamp when the initiative was created * `updated_at` *timestamp* The timestamp when the initiative was last updated * `created_by` *uuid* ID of the user who created the initiative * `updated_by` *uuid* ID of the user who last updated the initiative ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2023-11-19T11:56:55.176802Z", "updated_at": "2023-11-19T11:56:55.176809Z", "name": "Q1 Product Launch", "description": "Launch new product features in Q1", "description_html": "

Launch new product features in Q1

", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "start_date": "2024-01-01", "end_date": "2024-03-31", "state": "ACTIVE", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/initiative/add-initiative.html' description: >- Create an initiative via Plane API. HTTP request format, parameters, scopes, and example responses for create an initiative. --- # Create an initiative Create a new initiative in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Description html. Description stripped. Start date. End date. Logo props. * `DRAFT` - Draft * `PLANNED` - Planned * `ACTIVE` - Active * `COMPLETED` - Completed * `CLOSED` - Closed Archived at. Created by. Updated by. Lead. ### Scopes `initiatives:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "description_html": "

Example content

", "description_stripped": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "logo_props": "example-value", "state": "DRAFT", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "description_html": "

Example content

", "description_stripped": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "logo_props": "example-value", "state": "DRAFT", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", description_html: "

Example content

", description_stripped: "Example description", start_date: "2024-01-01T00:00:00Z", end_date: "2024-01-01T00:00:00Z", logo_props: "example-value", state: "DRAFT", archived_at: "2024-01-01T00:00:00Z", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", lead: "550e8400-e29b-41d4-a716-446655440000", }), }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description" } ``` --- --- url: 'https://developers.plane.so/api-reference/initiative/list-initiatives.html' description: >- List all initiatives via Plane API. HTTP request format, parameters, scopes, and example responses for list all initiatives. --- # List all initiatives List all initiatives in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `initiatives:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ] } ] ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/get-initiative-detail.html description: >- Retrieve an initiative via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve an initiative. --- # Retrieve an initiative Retrieve an initiative by its ID ### Path Parameters The unique identifier of the initiative. The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `initiatives:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description" } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/update-initiative-detail.html description: >- Update an initiative via Plane API. HTTP request format, parameters, scopes, and example responses for update an initiative. --- # Update an initiative Update an initiative by its ID ### Path Parameters The unique identifier of the initiative. The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Description html. Description stripped. Start date. End date. Logo props. * `DRAFT` - Draft * `PLANNED` - Planned * `ACTIVE` - Active * `COMPLETED` - Completed * `CLOSED` - Closed Archived at. Created by. Updated by. Lead. ### Scopes `initiatives:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "description_html": "

Example content

", "description_stripped": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "logo_props": "example-value", "state": "DRAFT", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "description_html": "

Example content

", "description_stripped": "Example description", "start_date": "2024-01-01T00:00:00Z", "end_date": "2024-01-01T00:00:00Z", "logo_props": "example-value", "state": "DRAFT", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", description_html: "

Example content

", description_stripped: "Example description", start_date: "2024-01-01T00:00:00Z", end_date: "2024-01-01T00:00:00Z", logo_props: "example-value", state: "DRAFT", archived_at: "2024-01-01T00:00:00Z", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", lead: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description" } ``` --- --- url: 'https://developers.plane.so/api-reference/initiative/delete-initiative.html' description: >- Delete an initiative via Plane API. HTTP request format, parameters, scopes, and example responses for delete an initiative. --- # Delete an initiative Delete an initiative by its ID ### Path Parameters The unique identifier of the initiative. The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `initiatives:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/initiative/add-initiative-label.html' description: >- Create an initiative label via Plane API. HTTP request format, parameters, scopes, and example responses for create an initiative label. --- # Create an initiative label Create a new initiative label in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Color. Sort order. ### Scopes `initiatives.labels:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "color": "Example Name", "sort_order": 1 }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "color": "Example Name", "sort_order": 1 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", color: "Example Name", sort_order: 1, }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/add-labels-to-initiative.html description: >- Add labels to initiative via Plane API. HTTP request format, parameters, scopes, and example responses for add labels to initiative. --- # Add labels to initiative Add labels to an initiative by its ID ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Label ids. ### Scopes `initiatives.labels:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "label_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/", headers={"X-API-Key": "your-api-key"}, json={ "label_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ label_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/list-initiative-labels.html description: >- List all initiative labels via Plane API. HTTP request format, parameters, scopes, and example responses for list all initiative labels. --- # List all initiative labels List all initiative labels in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `initiatives.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ] } ] ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/get-initiative-label-detail.html description: >- Retrieve an initiative label via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve an initiative label. --- # Retrieve an initiative label Retrieve an initiative label by its ID ### Path Parameters The unique identifier of the initiative label. The unique identifier of the label. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `initiatives.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/list-initiative-labels-for-initiative.html description: >- List all labels for an initiative via Plane API. HTTP request format, parameters, scopes, and example responses for list all labels for an initiative. --- # List all labels for an initiative List all labels associated with an initiative ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `initiatives.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/update-initiative-label-detail.html description: >- Update an initiative label via Plane API. HTTP request format, parameters, scopes, and example responses for update an initiative label. --- # Update an initiative label Update an initiative label by its ID ### Path Parameters The unique identifier of the initiative label. The unique identifier of the label. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Color. Sort order. ### Scopes `initiatives.labels:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "Example description", "color": "Example Name", "sort_order": 1 }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "Example description", "color": "Example Name", "sort_order": 1 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "Example description", color: "Example Name", sort_order: 1, }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/remove-labels-from-initiative.html description: >- Remove labels from initiative via Plane API. HTTP request format, parameters, scopes, and example responses for remove labels from initiative. --- # Remove labels from initiative Remove labels from an initiative by its ID ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `initiatives.labels:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/labels/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/initiative/delete-initiative-label.html description: >- Delete an initiative label via Plane API. HTTP request format, parameters, scopes, and example responses for delete an initiative label. --- # Delete an initiative label Delete an initiative label by its ID ### Path Parameters The unique identifier of the initiative label. The unique identifier of the label. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `initiatives.labels:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/labels/label-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/initiative/add-projects-to-initiative.html description: >- Add projects to initiative via Plane API. HTTP request format, parameters, scopes, and example responses for add projects to initiative. --- # Add projects to initiative Add projects to an initiative by its ID ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Project ids. ### Scopes `initiatives.projects:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "project_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/", headers={"X-API-Key": "your-api-key"}, json={ "project_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ project_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2, "project_lead": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/list-initiative-projects.html description: >- List all projects in an initiative via Plane API. HTTP request format, parameters, scopes, and example responses for list all projects in an initiative. --- # List all projects in an initiative List all projects associated with an initiative ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `initiatives.projects:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2 } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/remove-projects-from-initiative.html description: >- Remove projects from initiative via Plane API. HTTP request format, parameters, scopes, and example responses for remove projects from initiative. --- # Remove projects from initiative Remove projects from an initiative by its ID ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `initiatives.projects:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/projects/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/initiative/add-epics-to-initiative.html description: >- Add epics to initiative via Plane API. HTTP request format, parameters, scopes, and example responses for add epics to initiative. --- # Add epics to initiative Add epics to an initiative by its ID ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Epic ids. ### Scopes `initiatives.epics:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "epic_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/", headers={"X-API-Key": "your-api-key"}, json={ "epic_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ epic_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description" } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/list-initiative-epics.html description: >- List all epics for an initiative via Plane API. HTTP request format, parameters, scopes, and example responses for list all epics for an initiative. --- # List all epics for an initiative List all epics associated with an initiative ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `initiatives.epics:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "priority": "high", "sequence_id": 123, "state": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "group": "started" }, "assignees": [], "labels": [], "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/initiative/remove-epics-from-initiative.html description: >- Remove epics from initiative via Plane API. HTTP request format, parameters, scopes, and example responses for remove epics from initiative. --- # Remove epics from initiative Remove epics from an initiative by its ID ### Path Parameters The unique identifier of the initiative. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `initiatives.epics:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/initiatives/initiative-uuid/epics/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/release/overview.html' description: >- Plane Release API overview. Learn about endpoints, request/response format, and how to work with releases via REST API. --- # Overview Releases let you plan, track, and ship versions of your product. A release groups work items, carries a version tag, labels, links, comments, and a changelog, and can be scoped to the whole workspace or to a single project. [Learn more about Releases](https://docs.plane.so/releases) Releases are available on paid plans. Requests return `402 Payment Required` when the feature is not enabled for the workspace. ## The Release Object ### Attributes * `id` *uuid* Unique identifier for the release * `name` *string* **(required)** Name of the release. Must be unique within the workspace, otherwise a 400 `RELEASE_NAME_ALREADY_EXISTS` error is returned. * `description` *object* Read-only nested description object with `description_html`, `description_binary`, `description_stripped`, and `description_json` * `description_html` *string* (write-only) HTML description used to set the release's description on create/update * `description_json` *object* (write-only) JSON description used to set the release's description on create/update * `status` *string* Status of the release. Can be: `unreleased`, `released`, `cancelled`. Defaults to `unreleased` * `target_date` *date* Target date for the release in YYYY-MM-DD format * `release_date` *date* Actual release date in YYYY-MM-DD format * `lead` *uuid* User ID of the release lead * `tag` *uuid* ID of the release tag associated with the release. Must reference a global tag or a tag belonging to the same project, otherwise a 400 `{"tag": "RELEASE_TAG_OUT_OF_SCOPE"}` error is returned. * `is_latest` *boolean* Whether this is the latest release. Defaults to `false` * `is_prerelease` *boolean* Whether this release is a prerelease. Defaults to `false` * `project` *uuid* ID of the project the release belongs to. `null` for workspace-level releases. Immutable after creation; ignored on update. * `external_id` *string* External ID from an external source * `external_source` *string* External source identifier * `workspace` *uuid* Workspace UUID which is automatically saved * `created_at` *timestamp* The timestamp when the release was created * `updated_at` *timestamp* The timestamp when the release was last updated * `deleted_at` *timestamp* The timestamp when the release was deleted, if applicable * `created_by` *uuid* ID of the user who created the release * `updated_by` *uuid* ID of the user who last updated the release ## Related objects ### Release Tag * `id` *uuid* * `version` *string* **(required)** — must be unique within the workspace or project, otherwise 400 `{"version": "RELEASE_TAG_VERSION_ALREADY_EXISTS"}` * `description` *string* — plain text * `commit_hash` *string* * `git_tag` *string* * `project` *uuid* — `null` for a workspace-wide tag; immutable after creation * `workspace` *uuid* * `created_at` *timestamp* * `updated_at` *timestamp* * `deleted_at` *timestamp* * `created_by` *uuid* * `updated_by` *uuid* ### Release Label * `id` *uuid* * `name` *string* **(required)** — must be unique within the workspace or project, otherwise 400 `{"name": "RELEASE_LABEL_NAME_ALREADY_EXISTS"}` * `color` *string* — defaults to `#4E5355` * `sort_order` *integer* — defaults to `0` * `workspace` *uuid* * `project` *uuid* — `null` for a workspace-wide label; immutable after creation ### Release Comment * `id` *uuid* * `release` *uuid* (read-only) * `comment` *object* — read-only nested description object, same shape as the release's `description` * `comment_html` *string* (write-only) * `parent` *uuid* — parent comment for threaded replies * `edited_at` *timestamp* * `is_resolved` *boolean* — defaults to `false` * `is_hidden` *boolean* (read-only) * `workspace` *uuid* * `created_at` *timestamp* * `updated_at` *timestamp* * `deleted_at` *timestamp* * `created_by` *uuid* * `updated_by` *uuid* ### Release Link * `id` *uuid* * `release` *uuid* (read-only) * `title` *string* **(required)** * `url` *string* **(required)** — must be a valid URL, unique per release * `metadata` *object* — defaults to `{}` * `workspace` *uuid* * `created_at` *timestamp* * `updated_at` *timestamp* * `deleted_at` *timestamp* * `created_by` *uuid* * `updated_by` *uuid* ### Release Changelog * `id` *uuid* * `release` *uuid* (read-only) * `changelog` *object* — read-only nested description object, same shape as the release's `description` * `description_html` *string* (write-only) * `description_json` *object* (write-only) * `workspace` *uuid* * `created_at` *timestamp* * `updated_at` *timestamp* * `deleted_at` *timestamp* * `created_by` *uuid* * `updated_by` *uuid* ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "unreleased", "target_date": "2025-03-31", "release_date": null, "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-release.html' description: >- Create a release via Plane API. HTTP POST request format, required fields, and example responses. --- # Create a release Creates a new workspace-level release. `name` must be unique within the workspace, otherwise a 400 `RELEASE_NAME_ALREADY_EXISTS` error is returned. If `tag` is provided, it must reference a global release tag or a tag belonging to the same project, otherwise a 400 `{"tag": "RELEASE_TAG_OUT_OF_SCOPE"}` error is returned. Any `project` value in the request body is ignored; releases created through this endpoint are always workspace-level. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name of the release. HTML description of the release. JSON description of the release. Status of the release. Can be: `unreleased`, `released`, `cancelled`. Defaults to `unreleased`. Target date of the release in YYYY-MM-DD format. Actual release date of the release in YYYY-MM-DD format. ID of the user who leads the release. ID of the release tag associated with the release. Whether this is the latest release. Defaults to `false`. Whether this release is a prerelease. Defaults to `false`. External ID from an external source. External source identifier. ### Scopes `releases:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "v2.4.0", "description_html": "

Spring feature release.

", "status": "unreleased", "target_date": "2025-03-31", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "is_latest": false, "is_prerelease": false }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/", headers={"X-API-Key": "your-api-key"}, json={ 'name': 'v2.4.0', 'description_html': '

Spring feature release.

', 'status': 'unreleased', 'target_date': '2025-03-31', 'lead': '16c61a3a-512a-48ac-b0be-b6b46fe6f430', 'tag': '9f8e7d6c-5b4a-3210-fedc-ba0987654321', 'is_latest': False, 'is_prerelease': False } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/releases/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "v2.4.0", description_html: "

Spring feature release.

", status: "unreleased", target_date: "2025-03-31", lead: "16c61a3a-512a-48ac-b0be-b6b46fe6f430", tag: "9f8e7d6c-5b4a-3210-fedc-ba0987654321", is_latest: false, is_prerelease: false, }), }); const data = await response.json(); ``` ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "unreleased", "target_date": "2025-03-31", "release_date": null, "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-releases.html' description: >- List all releases via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List all releases Returns a list of all workspace-level releases (releases where `project` is `null`). ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/releases/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "unreleased", "target_date": "2025-03-31", "release_date": null, "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": null } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/release/get-release-detail.html' description: >- Get retrieve a release details via Plane API. Retrieve complete information for a specific resource. --- # Retrieve a release Retrieves the details of an existing workspace-level release by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Scopes `releases:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "unreleased", "target_date": "2025-03-31", "release_date": null, "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/update-release-detail.html' description: >- Update a release via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update a release Updates an existing workspace-level release by setting the values of the parameters passed. Any parameters not provided will be left unchanged. `name` must remain unique within the workspace, otherwise a 400 `RELEASE_NAME_ALREADY_EXISTS` error is returned. If `tag` is provided, it must reference a global release tag or a tag belonging to the same project, otherwise a 400 `{"tag": "RELEASE_TAG_OUT_OF_SCOPE"}` error is returned. `project` cannot be changed and is ignored if passed. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters Name of the release. HTML description of the release. JSON description of the release. Status of the release. Can be: `unreleased`, `released`, `cancelled`. Target date of the release in YYYY-MM-DD format. Actual release date of the release in YYYY-MM-DD format. ID of the user who leads the release. ID of the release tag associated with the release. Whether this is the latest release. Whether this release is a prerelease. External ID from an external source. External source identifier. ### Scopes `releases:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "released", "release_date": "2025-04-02" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", headers={"X-API-Key": "your-api-key"}, json={ 'status': 'released', 'release_date': '2025-04-02' } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ status: "released", release_date: "2025-04-02", }), }, ); const data = await response.json(); ``` ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-04-02T09:00:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "released", "target_date": "2025-03-31", "release_date": "2025-04-02", "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/delete-release.html' description: >- Permanently delete a release via Plane API. Removes the release. Returns 204 on success. --- # Delete a release Permanently deletes a workspace-level release. This action cannot be undone. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Scopes `releases:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-project-release.html' description: >- Create a release for a project via Plane API. HTTP POST request format, required fields, and example responses. --- # Create a project release Creates a new release scoped to the project. `project` is set from the URL and any `project` value in the request body is ignored. `name` must be unique within the workspace, otherwise a 400 `RELEASE_NAME_ALREADY_EXISTS` error is returned. If `tag` is provided, it must reference a global release tag or a tag belonging to this project, otherwise a 400 `{"tag": "RELEASE_TAG_OUT_OF_SCOPE"}` error is returned. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project. ### Body Parameters Name of the release. HTML description of the release. JSON description of the release. Status of the release. Can be: `unreleased`, `released`, `cancelled`. Defaults to `unreleased`. Target date of the release in YYYY-MM-DD format. Actual release date of the release in YYYY-MM-DD format. ID of the user who leads the release. ID of the release tag associated with the release. Whether this is the latest release. Defaults to `false`. Whether this release is a prerelease. Defaults to `false`. External ID from an external source. External source identifier. ### Scopes `releases:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "v2.4.0", "description_html": "

Spring feature release.

", "status": "unreleased", "target_date": "2025-03-31", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "is_latest": false, "is_prerelease": false }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/", headers={"X-API-Key": "your-api-key"}, json={ 'name': 'v2.4.0', 'description_html': '

Spring feature release.

', 'status': 'unreleased', 'target_date': '2025-03-31', 'lead': '16c61a3a-512a-48ac-b0be-b6b46fe6f430', 'tag': '9f8e7d6c-5b4a-3210-fedc-ba0987654321', 'is_latest': False, 'is_prerelease': False } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "v2.4.0", description_html: "

Spring feature release.

", status: "unreleased", target_date: "2025-03-31", lead: "16c61a3a-512a-48ac-b0be-b6b46fe6f430", tag: "9f8e7d6c-5b4a-3210-fedc-ba0987654321", is_latest: false, is_prerelease: false, }), }, ); const data = await response.json(); ``` ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "unreleased", "target_date": "2025-03-31", "release_date": null, "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": "8b2e1c4d-6f3a-4d5e-9c1b-2a3b4c5d6e7f" } ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-project-releases.html' description: >- List all releases for a project via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List all project releases Returns a list of all releases scoped to a project. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project. ### Query Parameters Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "unreleased", "target_date": "2025-03-31", "release_date": null, "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": "8b2e1c4d-6f3a-4d5e-9c1b-2a3b4c5d6e7f" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/release/get-project-release-detail.html description: >- Get retrieve a project release details via Plane API. Retrieve complete information for a specific resource. --- # Retrieve a project release Retrieves the details of an existing release scoped to a project by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project. The unique identifier for the release. ### Scopes `releases:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "unreleased", "target_date": "2025-03-31", "release_date": null, "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": "8b2e1c4d-6f3a-4d5e-9c1b-2a3b4c5d6e7f" } ``` --- --- url: >- https://developers.plane.so/api-reference/release/update-project-release-detail.html description: >- Update a project release via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update a project release Updates an existing release scoped to a project by setting the values of the parameters passed. Any parameters not provided will be left unchanged. `name` must remain unique within the workspace, otherwise a 400 `RELEASE_NAME_ALREADY_EXISTS` error is returned. If `tag` is provided, it must reference a global release tag or a tag belonging to this project, otherwise a 400 `{"tag": "RELEASE_TAG_OUT_OF_SCOPE"}` error is returned. `project` cannot be changed and is ignored if passed. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project. The unique identifier for the release. ### Body Parameters Name of the release. HTML description of the release. JSON description of the release. Status of the release. Can be: `unreleased`, `released`, `cancelled`. Target date of the release in YYYY-MM-DD format. Actual release date of the release in YYYY-MM-DD format. ID of the user who leads the release. ID of the release tag associated with the release. Whether this is the latest release. Whether this release is a prerelease. External ID from an external source. External source identifier. ### Scopes `releases:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "released", "release_date": "2025-04-02" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", headers={"X-API-Key": "your-api-key"}, json={ 'status': 'released', 'release_date': '2025-04-02' } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ status: "released", release_date: "2025-04-02", }), }, ); const data = await response.json(); ``` ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-04-02T09:00:00.000000Z", "deleted_at": null, "name": "v2.4.0", "description": { "description_html": "

Spring feature release.

", "description_binary": null, "description_stripped": "Spring feature release.", "description_json": {} }, "status": "released", "target_date": "2025-03-31", "release_date": "2025-04-02", "is_latest": false, "is_prerelease": false, "external_id": null, "external_source": null, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "tag": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "project": "8b2e1c4d-6f3a-4d5e-9c1b-2a3b4c5d6e7f" } ``` --- --- url: 'https://developers.plane.so/api-reference/release/delete-project-release.html' description: >- Permanently delete a project release via Plane API. Removes the release. Returns 204 on success. --- # Delete a project release Permanently deletes a release scoped to a project. This action cannot be undone. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project. The unique identifier for the release. ### Scopes `releases:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-release-work-items.html' description: >- Add work items to release via Plane API. HTTP POST request format, required fields, and example responses. --- # Add work items to release Adds one or more work items to a release. Work items already linked to the release are skipped. For a project-scoped release, every work item must belong to that project, otherwise the request fails with a 400 `RELEASE_WORK_ITEM_OUT_OF_SCOPE` error. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters Array of work item IDs to add to the release. ### Scopes `releases.work_items:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "work_item_ids": ["d4e5f6a7-b8c9-0123-defa-456789012345", "e5f6a7b8-c9d0-1234-efab-567890123456"] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/", headers={"X-API-Key": "your-api-key"}, json={ 'work_item_ids': ['d4e5f6a7-b8c9-0123-defa-456789012345', 'e5f6a7b8-c9d0-1234-efab-567890123456'] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ work_item_ids: [ "d4e5f6a7-b8c9-0123-defa-456789012345", "e5f6a7b8-c9d0-1234-efab-567890123456", ], }), }, ); const data = await response.json(); ``` ```json { "message": "Work items added successfully" } ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-release-work-items.html' description: >- List all work items in a release via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List all work items in a release Returns a list of all work items linked to a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Query Parameters Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases.work_items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 2, "total_pages": 1, "total_results": 2, "extra_stats": null, "results": [ { "id": "d4e5f6a7-b8c9-0123-defa-456789012345", "project_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "name": "Fix login redirect" }, { "id": "e5f6a7b8-c9d0-1234-efab-567890123456", "project_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "name": "Add SSO support for enterprise plan" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/release/remove-release-work-items.html description: >- Remove work items from release via Plane API. HTTP DELETE request for removing resources. --- # Remove work items from release Removes one or more work items from a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters Array of work item IDs to remove from the release. ### Scopes `releases.work_items:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "work_item_ids": ["d4e5f6a7-b8c9-0123-defa-456789012345"] }' ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/", headers={"X-API-Key": "your-api-key"}, json={ 'work_item_ids': ['d4e5f6a7-b8c9-0123-defa-456789012345'] } ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/work-items/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ work_item_ids: ["d4e5f6a7-b8c9-0123-defa-456789012345"], }), }, ); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-release-label.html' description: >- Create a release label via Plane API. HTTP POST request format, required fields, and example responses. --- # Create a release label Creates a new release label in the workspace label catalog. The label name must be unique per workspace (or per project, when scoped to one), otherwise the request fails with a 400 `RELEASE_LABEL_NAME_ALREADY_EXISTS` error. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name of the release label. Hex color code for the label. Defaults to `#4E5355`. Sort order for the label. Defaults to 0. ID of the project to scope the label to. When omitted, the label is workspace-wide. This cannot be changed after creation. ### Scopes `releases.labels:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Breaking change", "color": "#FF5630", "sort_order": 10 }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/", headers={"X-API-Key": "your-api-key"}, json={ 'name': 'Breaking change', 'color': '#FF5630', 'sort_order': 10 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Breaking change", color: "#FF5630", sort_order: 10, }), }, ); const data = await response.json(); ``` ```json { "id": "f6a7b8c9-d0e1-2345-fabc-678901234567", "name": "Breaking change", "color": "#FF5630", "sort_order": 10, "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-release-labels.html' description: >- List all release labels via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List all release labels Returns a list of all release labels in the workspace label catalog, ordered by `sort_order`. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Return labels owned by this project. When omitted, only workspace-wide labels are returned. Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "id": "f6a7b8c9-d0e1-2345-fabc-678901234567", "name": "Breaking change", "color": "#FF5630", "sort_order": 10, "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/release/get-release-label-detail.html description: >- Get retrieve a release label details via Plane API. Retrieve complete information for a specific resource. --- # Retrieve a release label Retrieves the details of an existing release label by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release label. ### Scopes `releases.labels:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "f6a7b8c9-d0e1-2345-fabc-678901234567", "name": "Breaking change", "color": "#FF5630", "sort_order": 10, "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ``` --- --- url: >- https://developers.plane.so/api-reference/release/update-release-label-detail.html description: >- Update a release label via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update a release label Updates an existing release label by setting the values of the parameters passed. Any parameters not provided will be left unchanged. The label name must remain unique per workspace (or per project), otherwise the request fails with a 400 `RELEASE_LABEL_NAME_ALREADY_EXISTS` error. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release label. ### Body Parameters Name of the release label. Hex color code for the label. Sort order for the label. ### Scopes `releases.labels:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "color": "#DE350B", "sort_order": 5 }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/", headers={"X-API-Key": "your-api-key"}, json={ 'color': '#DE350B', 'sort_order': 5 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ color: "#DE350B", sort_order: 5, }), }, ); const data = await response.json(); ``` ```json { "id": "f6a7b8c9-d0e1-2345-fabc-678901234567", "name": "Breaking change", "color": "#DE350B", "sort_order": 5, "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/delete-release-label.html' description: >- Permanently delete a release label via Plane API. Removes the label from the workspace catalog. Returns 204 on success. --- # Delete a release label Permanently deletes a release label from the workspace label catalog. This action cannot be undone. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release label. ### Scopes `releases.labels:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/labels/f6a7b8c9-d0e1-2345-fabc-678901234567/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-labels-to-release.html' description: >- Create labels to release via Plane API. HTTP POST request format, required fields, and example responses. --- # Add labels to release Adds one or more labels to a release. Labels must be workspace-wide or belong to the release's project, otherwise the request fails with a 400 `RELEASE_LABEL_OUT_OF_SCOPE` error. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters Array of release label IDs to add to the release. ### Scopes `releases:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "label_ids": ["f6a7b8c9-d0e1-2345-fabc-678901234567"] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/", headers={"X-API-Key": "your-api-key"}, json={ 'label_ids': ['f6a7b8c9-d0e1-2345-fabc-678901234567'] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ label_ids: ["f6a7b8c9-d0e1-2345-fabc-678901234567"], }), }, ); const data = await response.json(); ``` ```json [ { "id": "f6a7b8c9-d0e1-2345-fabc-678901234567", "name": "Breaking change", "color": "#FF5630", "sort_order": 10, "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ] ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-labels-for-release.html' description: >- List labels for a release via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List labels for a release Returns a list of all labels attached to a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Query Parameters Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "id": "f6a7b8c9-d0e1-2345-fabc-678901234567", "name": "Breaking change", "color": "#FF5630", "sort_order": 10, "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/release/remove-labels-from-release.html description: >- Delete labels from release via Plane API. HTTP DELETE request for removing resources. --- # Remove labels from release Removes one or more labels from a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters Array of release label IDs to remove from the release. ### Scopes `releases:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "label_ids": ["f6a7b8c9-d0e1-2345-fabc-678901234567"] }' ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/", headers={"X-API-Key": "your-api-key"}, json={ 'label_ids': ['f6a7b8c9-d0e1-2345-fabc-678901234567'] } ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/labels/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ label_ids: ["f6a7b8c9-d0e1-2345-fabc-678901234567"], }), }, ); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-release-tag.html' description: >- Create a release tag via Plane API. HTTP POST request format, required fields, and example responses. --- # Create a release tag Creates a new release tag in a workspace. `version` must be unique per workspace; if `project` is set, it must be unique per project. Duplicate versions return a 400 with code `RELEASE_TAG_VERSION_ALREADY_EXISTS`. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Version string for the release tag, for example `2.4.0`. Plain text description of the release tag. Commit hash the release tag points to. Name of the corresponding git tag, for example `v2.4.0`. ID of the project to scope this release tag to. When omitted, the release tag is workspace-wide. ### Scopes `releases.tags:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": "2.4.0", "description": "Spring release tag", "commit_hash": "9fceb02d0ae598e95dc970b74767f19372d61af8", "git_tag": "v2.4.0" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/", headers={"X-API-Key": "your-api-key"}, json={ 'version': '2.4.0', 'description': 'Spring release tag', 'commit_hash': '9fceb02d0ae598e95dc970b74767f19372d61af8', 'git_tag': 'v2.4.0' } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ version: "2.4.0", description: "Spring release tag", commit_hash: "9fceb02d0ae598e95dc970b74767f19372d61af8", git_tag: "v2.4.0", }), }); const data = await response.json(); ``` ```json { "id": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-01T10:00:00.000000Z", "deleted_at": null, "version": "2.4.0", "description": "Spring release tag", "commit_hash": "9fceb02d0ae598e95dc970b74767f19372d61af8", "git_tag": "v2.4.0", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-release-tags.html' description: >- List all release tags via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List all release tags Returns a list of all release tags in a workspace. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Return release tags owned by this project. When omitted, only workspace-wide release tags are returned. Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases.tags:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "id": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "version": "2.4.0", "description": "Spring release tag", "commit_hash": "9fceb02d0ae598e95dc970b74767f19372d61af8", "git_tag": "v2.4.0", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/release/get-release-tag-detail.html' description: >- Get retrieve a release tag details via Plane API. Retrieve complete information for a specific resource. --- # Retrieve a release tag Retrieves the details of an existing release tag by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release tag. ### Scopes `releases.tags:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-05T14:30:00.000000Z", "deleted_at": null, "version": "2.4.0", "description": "Spring release tag", "commit_hash": "9fceb02d0ae598e95dc970b74767f19372d61af8", "git_tag": "v2.4.0", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ``` --- --- url: >- https://developers.plane.so/api-reference/release/update-release-tag-detail.html description: >- Update a release tag via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update a release tag Updates an existing release tag by setting the values of the parameters passed. Any parameters not provided will be left unchanged. `project` is immutable after creation and cannot be changed here. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release tag. ### Body Parameters Version string for the release tag, for example `2.4.0`. Plain text description of the release tag. Commit hash the release tag points to. Name of the corresponding git tag, for example `v2.4.0`. ### Scopes `releases.tags:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "commit_hash": "a3f9c1d8e7b6054321fedcba9876543210abcd1", "git_tag": "v2.4.0" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/", headers={"X-API-Key": "your-api-key"}, json={ 'commit_hash': 'a3f9c1d8e7b6054321fedcba9876543210abcd1', 'git_tag': 'v2.4.0' } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ commit_hash: "a3f9c1d8e7b6054321fedcba9876543210abcd1", git_tag: "v2.4.0", }), }, ); const data = await response.json(); ``` ```json { "id": "9f8e7d6c-5b4a-3210-fedc-ba0987654321", "created_at": "2025-03-01T10:00:00.000000Z", "updated_at": "2025-03-06T08:45:00.000000Z", "deleted_at": null, "version": "2.4.0", "description": "Spring release tag", "commit_hash": "a3f9c1d8e7b6054321fedcba9876543210abcd1", "git_tag": "v2.4.0", "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "project": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/delete-release-tag.html' description: >- Permanently delete a release tag via Plane API. Removes the release tag. Returns 204 on success. --- # Delete a release tag Permanently deletes a release tag from a workspace. This action cannot be undone. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release tag. ### Scopes `releases.tags:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/tags/tag-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-release-comment.html' description: >- Create a release comment via Plane API. HTTP POST request format, required fields, and example responses. --- # Create a release comment Creates a new comment on a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters HTML-formatted content of the comment. ID of the parent comment, for threaded replies. Whether the comment has been resolved. ### Scopes `releases.comments:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "comment_html": "

QA sign-off complete.

", "parent": null, "is_resolved": false }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/", headers={"X-API-Key": "your-api-key"}, json={ 'comment_html': '

QA sign-off complete.

', 'parent': None, 'is_resolved': False } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ comment_html: "

QA sign-off complete.

", parent: null, is_resolved: false, }), }, ); const data = await response.json(); ``` ```json { "id": "c0ffee00-1111-2222-3333-444444444444", "created_at": "2025-03-02T09:15:00.000000Z", "updated_at": "2025-03-02T09:15:00.000000Z", "deleted_at": null, "comment": { "description_html": "

QA sign-off complete.

", "description_binary": null, "description_stripped": "QA sign-off complete.", "description_json": {} }, "is_hidden": false, "edited_at": null, "is_resolved": false, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "parent": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-release-comments.html' description: >- List all release comments via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List all release comments Returns a list of all comments on a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Query Parameters Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases.comments:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "id": "c0ffee00-1111-2222-3333-444444444444", "created_at": "2025-03-02T09:15:00.000000Z", "updated_at": "2025-03-02T09:15:00.000000Z", "deleted_at": null, "comment": { "description_html": "

QA sign-off complete.

", "description_binary": null, "description_stripped": "QA sign-off complete.", "description_json": {} }, "is_hidden": false, "edited_at": null, "is_resolved": false, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "parent": null } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/release/get-release-comment-detail.html description: >- Get retrieve a release comment details via Plane API. Retrieve complete information for a specific resource. --- # Retrieve a release comment Retrieves the details of an existing release comment by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. The unique identifier for the comment. ### Scopes `releases.comments:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "c0ffee00-1111-2222-3333-444444444444", "created_at": "2025-03-02T09:15:00.000000Z", "updated_at": "2025-03-02T09:15:00.000000Z", "deleted_at": null, "comment": { "description_html": "

QA sign-off complete.

", "description_binary": null, "description_stripped": "QA sign-off complete.", "description_json": {} }, "is_hidden": false, "edited_at": null, "is_resolved": false, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "parent": null } ``` --- --- url: >- https://developers.plane.so/api-reference/release/update-release-comment-detail.html description: >- Update a release comment via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update a release comment Updates an existing release comment by setting the values of the parameters passed. Any parameters not provided will be left unchanged. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. The unique identifier for the comment. ### Body Parameters HTML-formatted content of the comment. ID of the parent comment, for threaded replies. Whether the comment has been resolved. Timestamp when the comment was last edited, in ISO 8601 format. ### Scopes `releases.comments:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "comment_html": "

QA sign-off complete. Retested after hotfix.

", "is_resolved": true, "edited_at": "2025-03-02T10:05:00.000000Z" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/", headers={"X-API-Key": "your-api-key"}, json={ 'comment_html': '

QA sign-off complete. Retested after hotfix.

', 'is_resolved': True, 'edited_at': '2025-03-02T10:05:00.000000Z' } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ comment_html: "

QA sign-off complete. Retested after hotfix.

", is_resolved: true, edited_at: "2025-03-02T10:05:00.000000Z", }), }, ); const data = await response.json(); ``` ```json { "id": "c0ffee00-1111-2222-3333-444444444444", "created_at": "2025-03-02T09:15:00.000000Z", "updated_at": "2025-03-02T10:05:00.000000Z", "deleted_at": null, "comment": { "description_html": "

QA sign-off complete. Retested after hotfix.

", "description_binary": null, "description_stripped": "QA sign-off complete. Retested after hotfix.", "description_json": {} }, "is_hidden": false, "edited_at": "2025-03-02T10:05:00.000000Z", "is_resolved": true, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "parent": null } ``` --- --- url: 'https://developers.plane.so/api-reference/release/delete-release-comment.html' description: >- Permanently delete a release comment via Plane API. Removes the comment. Returns 204 on success. --- # Delete a release comment Permanently deletes a comment from a release. This action cannot be undone. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. The unique identifier for the comment. ### Scopes `releases.comments:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/comments/c0ffee00-1111-2222-3333-444444444444/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/add-release-link.html' description: >- Create a release link via Plane API. HTTP POST request format, required fields, and example responses. --- # Create a release link Creates a new link attached to a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters Title or description of the link. URL of the external resource. Must be unique within the release. Arbitrary metadata to store alongside the link. Defaults to `{}`. ### Scopes `releases.links:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Release notes", "url": "https://example.com/releases/v2.4.0", "metadata": {} }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/", headers={"X-API-Key": "your-api-key"}, json={ 'title': 'Release notes', 'url': 'https://example.com/releases/v2.4.0', 'metadata': {} } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ title: "Release notes", url: "https://example.com/releases/v2.4.0", metadata: {}, }), }, ); const data = await response.json(); ``` ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2025-03-03T11:00:00.000000Z", "updated_at": "2025-03-03T11:00:00.000000Z", "deleted_at": null, "title": "Release notes", "url": "https://example.com/releases/v2.4.0", "metadata": {}, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` --- --- url: 'https://developers.plane.so/api-reference/release/list-release-links.html' description: >- List all release links via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List all release links Returns a list of all links attached to a release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Query Parameters Number of results per page. Defaults to 20. Cursor string for pagination in the format `value:offset:is_prev`. ### Scopes `releases.links:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "next_cursor": "20:1:0", "prev_cursor": "", "next_page_results": false, "prev_page_results": false, "count": 1, "total_pages": 1, "total_results": 1, "extra_stats": null, "results": [ { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2025-03-03T11:00:00.000000Z", "updated_at": "2025-03-03T11:00:00.000000Z", "deleted_at": null, "title": "Release notes", "url": "https://example.com/releases/v2.4.0", "metadata": {}, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/release/get-release-link-detail.html' description: >- Get retrieve a release link details via Plane API. Retrieve complete information for a specific resource. --- # Retrieve a release link Retrieves the details of an existing release link by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. The unique identifier for the link. ### Scopes `releases.links:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2025-03-03T11:00:00.000000Z", "updated_at": "2025-03-03T11:00:00.000000Z", "deleted_at": null, "title": "Release notes", "url": "https://example.com/releases/v2.4.0", "metadata": {}, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` --- --- url: >- https://developers.plane.so/api-reference/release/update-release-link-detail.html description: >- Update a release link via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update a release link Updates an existing release link by setting the values of the parameters passed. Any parameters not provided will be left unchanged. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. The unique identifier for the link. ### Body Parameters Title or description of the link. URL of the external resource. Must be unique within the release. Arbitrary metadata to store alongside the link. ### Scopes `releases.links:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Release notes (updated)", "url": "https://example.com/releases/v2.4.0/notes" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/", headers={"X-API-Key": "your-api-key"}, json={ 'title': 'Release notes (updated)', 'url': 'https://example.com/releases/v2.4.0/notes' } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ title: "Release notes (updated)", url: "https://example.com/releases/v2.4.0/notes", }), }, ); const data = await response.json(); ``` ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012", "created_at": "2025-03-03T11:00:00.000000Z", "updated_at": "2025-03-03T11:20:00.000000Z", "deleted_at": null, "title": "Release notes (updated)", "url": "https://example.com/releases/v2.4.0/notes", "metadata": {}, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` --- --- url: 'https://developers.plane.so/api-reference/release/delete-release-link.html' description: >- Permanently delete a release link via Plane API. Removes the link. Returns 204 on success. --- # Delete a release link Permanently deletes a link from a release. This action cannot be undone. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. The unique identifier for the link. ### Scopes `releases.links:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/links/b2c3d4e5-f6a7-8901-bcde-f23456789012/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json // 204 No Content ``` --- --- url: 'https://developers.plane.so/api-reference/release/get-release-changelog.html' description: >- Get retrieve a release changelog details via Plane API. Retrieve complete information for a specific resource. --- # Retrieve a release changelog Retrieves the changelog for a release. The changelog is created empty the first time it is accessed, so this request never returns a 404 for an existing release. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Scopes `releases:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/release-uuid/changelog/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/release-uuid/changelog/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/release-uuid/changelog/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "5d4c3b2a-1908-4756-8fed-cba987654321", "created_at": "2025-03-01T10:05:00.000000Z", "updated_at": "2025-03-05T14:40:00.000000Z", "deleted_at": null, "changelog": { "description_html": "

Added dark mode. Fixed login redirect loop.

", "description_binary": null, "description_stripped": "Added dark mode. Fixed login redirect loop.", "description_json": {} }, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` --- --- url: >- https://developers.plane.so/api-reference/release/update-release-changelog.html description: >- Update a release changelog via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update a release changelog Updates the changelog for a release by setting the values of the parameters passed. Any parameters not provided will be left unchanged. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier for the release. ### Body Parameters HTML content for the changelog. Write-only; the stored value is returned in the response as the nested `changelog` object. Rich-text JSON content for the changelog. Write-only; the stored value is returned in the response as the nested `changelog` object. ### Scopes `releases:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/releases/release-uuid/changelog/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description_html": "

Added dark mode. Fixed login redirect loop.

", "description_json": {} }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/release-uuid/changelog/", headers={"X-API-Key": "your-api-key"}, json={ 'description_html': '

Added dark mode. Fixed login redirect loop.

', 'description_json': {} } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/releases/release-uuid/changelog/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ description_html: "

Added dark mode. Fixed login redirect loop.

", description_json: {}, }), }, ); const data = await response.json(); ``` ```json { "id": "5d4c3b2a-1908-4756-8fed-cba987654321", "created_at": "2025-03-01T10:05:00.000000Z", "updated_at": "2025-03-06T09:10:00.000000Z", "deleted_at": null, "changelog": { "description_html": "

Added dark mode. Fixed login redirect loop.

", "description_binary": null, "description_stripped": "Added dark mode. Fixed login redirect loop.", "description_json": {} }, "created_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "updated_by": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "release": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` --- --- url: 'https://developers.plane.so/api-reference/customer/overview.html' description: >- Plane Customer API overview. Learn about endpoints, request/response format, and how to work with customer via REST API. --- # Overview Customers allow you to manage customer relationships and track customer-related work items, requests, and custom properties within a workspace. [Learn more about Customers](https://docs.plane.so/customers) ## The Customer Object ### Attributes * `id` *uuid* Unique identifier for the customer * `name` *string* **(required)** Name of the customer * `email` *string* Email address of the customer * `workspace` *uuid* Workspace UUID which is automatically saved * `created_at` *timestamp* The timestamp when the customer was created * `updated_at` *timestamp* The timestamp when the customer was last updated * `created_by` *uuid* ID of the user who created the customer * `updated_by` *uuid* ID of the user who last updated the customer ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2023-11-19T11:56:55.176802Z", "updated_at": "2023-11-19T11:56:55.176809Z", "name": "Acme Corporation", "email": "contact@acme.com", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/customer/add-customer.html' description: >- Create a customer via Plane API. HTTP request format, parameters, scopes, and example responses for create a customer. --- # Create a customer Create a new customer in the specified workspace. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Description html. Description stripped. Email. Website url. Logo props. Domain. Employees. Stage. Contract status. Revenue. Archived at. Created by. Updated by. Logo asset. ### Scopes `customers:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/customers/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "example-value", description_html: "

Example content

", description_stripped: "Example description", email: "Example Name", website_url: "https://example.com/resource", logo_props: "example-value", domain: "Example Name", employees: 1, stage: "Example Name", contract_status: "Example Name", revenue: "Example Name", archived_at: "2024-01-01T00:00:00Z", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", logo_asset: "550e8400-e29b-41d4-a716-446655440000", }), }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "example-value", "deleted_at": "2024-01-01T00:00:00Z", "customer_request_count": 1, "logo_url": "Example Name", "description_html": "

Example content

", "description_stripped": "Example description", "description_binary": "Example description", "email": "Example Name" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/link-work-items-to-customer.html description: >- Link work items to customer via Plane API. HTTP request format, parameters, scopes, and example responses for link work items to customer. --- # Link work items to customer Link one or more issues to a customer, optionally within a specific customer request. ### Path Parameters The unique identifier of the customer. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Array of issue IDs to link ### Scopes `customers.work_items:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issue_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/", headers={"X-API-Key": "your-api-key"}, json={ "issue_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ issue_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "detail": "Issues linked successfully" } ``` --- --- url: 'https://developers.plane.so/api-reference/customer/list-customers.html' description: >- List all customers via Plane API. HTTP request format, parameters, scopes, and example responses for list all customers. --- # List all customers List all customers in a workspace with optional search filtering ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/customers/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/customer/get-customer-detail.html' description: >- Retrieve a customer via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a customer. --- # Retrieve a customer Get a specific customer by ID ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "example-value", "deleted_at": "2024-01-01T00:00:00Z", "customer_request_count": 1, "logo_url": "Example Name", "description_html": "

Example content

", "description_stripped": "Example description", "description_binary": "Example description", "email": "Example Name" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/list-customer-work-items.html description: >- List all customer work items via Plane API. HTTP request format, parameters, scopes, and example responses for list all customer work items. --- # List all customer work items List all issues linked to a customer, with filtering by request ### Path Parameters The unique identifier of the customer. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.work_items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "detail": "Customer issues retrieved successfully" } ``` --- --- url: 'https://developers.plane.so/api-reference/customer/update-customer-detail.html' description: >- Update a customer via Plane API. HTTP request format, parameters, scopes, and example responses for update a customer. --- # Update a customer Update an existing customer with the provided fields. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Description html. Description stripped. Email. Website url. Logo props. Domain. Employees. Stage. Contract status. Revenue. Archived at. Created by. Updated by. Logo asset. ### Scopes `customers:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "example-value", description_html: "

Example content

", description_stripped: "Example description", email: "Example Name", website_url: "https://example.com/resource", logo_props: "example-value", domain: "Example Name", employees: 1, stage: "Example Name", contract_status: "Example Name", revenue: "Example Name", archived_at: "2024-01-01T00:00:00Z", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", logo_asset: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "example-value", "deleted_at": "2024-01-01T00:00:00Z", "customer_request_count": 1, "logo_url": "Example Name", "description_html": "

Example content

", "description_stripped": "Example description", "description_binary": "Example description", "email": "Example Name" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/unlink-work-item-from-customer.html description: >- Unlink work item from customer via Plane API. HTTP request format, parameters, scopes, and example responses for unlink work item from customer. --- # Unlink work item from customer Remove the link between an issue and a customer/customer request. ### Path Parameters The unique identifier of the customer. The unique identifier of the work item. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.work_items:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/work-item-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/work-item-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/issues/work-item-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/customer/delete-customer.html' description: >- Delete a customer via Plane API. HTTP request format, parameters, scopes, and example responses for delete a customer. --- # Delete a customer Permanently delete a customer from the workspace. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/customer/add-customer-property.html' description: >- Create a customer property via Plane API. HTTP request format, parameters, scopes, and example responses for create a customer property. --- # Create a customer property ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Display name. Description. Logo props. Sort order. * `TEXT` - Text * `DATETIME` - Datetime * `DECIMAL` - Decimal * `BOOLEAN` - Boolean * `OPTION` - Option * `RELATION` - Relation * `URL` - URL * `EMAIL` - Email * `FILE` - File - `ISSUE` - Issue - `USER` - User Is required. Default value. Settings. Is active. Is multi. Validation rules. External source. External id. Created by. Updated by. ### Scopes `customers.properties:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "display_name": "Example Name", "description": "Example description", "logo_props": "example-value", "sort_order": 1, "property_type": "TEXT", "relation_type": "ISSUE", "is_required": true, "default_value": [ "Example Name" ], "settings": "example-value", "is_active": true, "is_multi": true, "validation_rules": "example-value", "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/", headers={"X-API-Key": "your-api-key"}, json={ "display_name": "Example Name", "description": "Example description", "logo_props": "example-value", "sort_order": 1, "property_type": "TEXT", "relation_type": "ISSUE", "is_required": true, "default_value": [ "Example Name" ], "settings": "example-value", "is_active": true, "is_multi": true, "validation_rules": "example-value", "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ display_name: "Example Name", description: "Example description", logo_props: "example-value", sort_order: 1, property_type: "TEXT", relation_type: "ISSUE", is_required: true, default_value: ["Example Name"], settings: "example-value", is_active: true, is_multi: true, validation_rules: "example-value", external_source: "github", external_id: "550e8400-e29b-41d4-a716-446655440000", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "display_name": "Example Name", "description": "Example description", "property_type": "TEXT", "deleted_at": "2024-01-01T00:00:00Z", "logo_props": "example-value", "sort_order": 1, "relation_type": "ISSUE", "is_required": true } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/list-customer-properties.html description: >- List all customer properties via Plane API. HTTP request format, parameters, scopes, and example responses for list all customer properties. --- # List all customer properties List all customer properties in a workspace. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.properties:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/get-customer-property-detail.html description: >- Retrieve a customer property via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a customer property. --- # Retrieve a customer property Retrieve a specific customer property by ID. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.properties:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "display_name": "Example Name", "description": "Example description", "property_type": "TEXT", "deleted_at": "2024-01-01T00:00:00Z", "logo_props": "example-value", "sort_order": 1, "relation_type": "ISSUE", "is_required": true } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/list-customer-property-values.html description: >- List all customer property values via Plane API. HTTP request format, parameters, scopes, and example responses for list all customer property values. --- # List all customer property values Retrieve all property values for a specific customer. ### Path Parameters The unique identifier of the customer. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.property_values:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "detail": "Customer property values retrieved successfully" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/get-customer-property-value.html description: >- Retrieve a customer property value via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a customer property value. --- # Retrieve a customer property value Retrieve values for a specific property of a customer. ### Path Parameters The unique identifier of the customer. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.property_values:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/property-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/property-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "detail": "Property values retrieved successfully" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/update-customer-property-detail.html description: >- Update a customer property via Plane API. HTTP request format, parameters, scopes, and example responses for update a customer property. --- # Update a customer property Update an existing customer property with the provided fields. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Display name. Description. Logo props. Sort order. * `TEXT` - Text * `DATETIME` - Datetime * `DECIMAL` - Decimal * `BOOLEAN` - Boolean * `OPTION` - Option * `RELATION` - Relation * `URL` - URL * `EMAIL` - Email * `FILE` - File - `ISSUE` - Issue - `USER` - User Is required. Default value. Settings. Is active. Is multi. Validation rules. External source. External id. Created by. Updated by. ### Scopes `customers.properties:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "display_name": "Example Name", "description": "Example description", "logo_props": "example-value", "sort_order": 1, "property_type": "TEXT", "relation_type": "ISSUE", "is_required": true, "default_value": [ "Example Name" ], "settings": "example-value", "is_active": true, "is_multi": true, "validation_rules": "example-value", "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "display_name": "Example Name", "description": "Example description", "logo_props": "example-value", "sort_order": 1, "property_type": "TEXT", "relation_type": "ISSUE", "is_required": true, "default_value": [ "Example Name" ], "settings": "example-value", "is_active": true, "is_multi": true, "validation_rules": "example-value", "external_source": "github", "external_id": "550e8400-e29b-41d4-a716-446655440000", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ display_name: "Example Name", description: "Example description", logo_props: "example-value", sort_order: 1, property_type: "TEXT", relation_type: "ISSUE", is_required: true, default_value: ["Example Name"], settings: "example-value", is_active: true, is_multi: true, validation_rules: "example-value", external_source: "github", external_id: "550e8400-e29b-41d4-a716-446655440000", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "name": "Example Name", "display_name": "Example Name", "description": "Example description", "property_type": "TEXT", "deleted_at": "2024-01-01T00:00:00Z", "logo_props": "example-value", "sort_order": 1, "relation_type": "ISSUE", "is_required": true } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/update-customer-property-value.html description: >- Update a customer property value via Plane API. HTTP request format, parameters, scopes, and example responses for update a customer property value. --- # Update a customer property value Update values for a specific property of a customer. ### Path Parameters The unique identifier of the customer. The unique identifier of the property. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Array of values for the property ### Scopes `customers.property_values:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "values": [ "Example Name" ] }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/property-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "values": [ "Example Name" ] } ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/property-values/property-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ values: ["Example Name"], }), }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/customer/delete-customer-property.html description: >- Delete a customer property via Plane API. HTTP request format, parameters, scopes, and example responses for delete a customer property. --- # Delete a customer property Permanently delete a customer property from the workspace. ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.properties:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customer-properties/property-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/customer/add-customer-request.html' description: >- Create a customer request via Plane API. HTTP request format, parameters, scopes, and example responses for create a customer request. --- # Create a customer request Create a new request for the specified customer. ### Path Parameters The unique identifier of the customer. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Description html. Link. Work item ids. ### Scopes `customers.requests:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "link": "https://example.com/resource", "work_item_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "link": "https://example.com/resource", "work_item_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "example-value", description_html: "

Example content

", link: "https://example.com/resource", work_item_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: 'https://developers.plane.so/api-reference/customer/list-customer-requests.html' description: >- List all customer requests via Plane API. HTTP request format, parameters, scopes, and example responses for list all customer requests. --- # List all customer requests List all requests for a customer with optional search filtering ### Path Parameters The unique identifier of the customer. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.requests:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/get-customer-request-detail.html description: >- Retrieve a customer request via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a customer request. --- # Retrieve a customer request Get a specific customer request by ID ### Path Parameters The unique identifier of the customer. The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.requests:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/update-customer-request-detail.html description: >- Update a customer request via Plane API. HTTP request format, parameters, scopes, and example responses for update a customer request. --- # Update a customer request Update an existing customer request with the provided fields. ### Path Parameters The unique identifier of the customer. The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Name. Description. Description html. Link. Work item ids. ### Scopes `customers.requests:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "link": "https://example.com/resource", "work_item_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "link": "https://example.com/resource", "work_item_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Example Name", description: "example-value", description_html: "

Example content

", link: "https://example.com/resource", work_item_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "email": "Example Name", "website_url": "https://example.com/resource", "logo_props": "example-value", "domain": "Example Name", "employees": 1, "stage": "Example Name", "contract_status": "Example Name", "revenue": "Example Name", "archived_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "logo_asset": "550e8400-e29b-41d4-a716-446655440000" } ``` --- --- url: >- https://developers.plane.so/api-reference/customer/delete-customer-request.html description: >- Delete a customer request via Plane API. HTTP request format, parameters, scopes, and example responses for delete a customer request. --- # Delete a customer request Permanently delete a customer request and unlink any linked issue ### Path Parameters The unique identifier of the customer. The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `customers.requests:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/customers/customer-uuid/requests/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/teamspace/overview.html' description: >- Plane Teamspace API overview. Learn about endpoints, request/response format, and how to work with teamspace via REST API. --- # Overview Teamspaces allow you to organize teams, projects, and members within a workspace, providing a way to group related work and manage access at a team level. [Learn more about Teamspaces](https://docs.plane.so/core-concepts/workspaces/teamspaces) ## The Teamspace Object ### Attributes * `id` *uuid* Unique identifier for the teamspace. * `name` *string* **(required)** Name of the teamspace. * `description_json` *object* JSON representation of the teamspace description. * `description_html` *string* HTML-formatted description of the teamspace. * `description_stripped` *string* Stripped version of the HTML description. * `description_binary` *string* Binary representation of the description. * `logo_props` *object* Logo properties for the teamspace. * `lead` *uuid* ID of the user who leads the teamspace. * `workspace` *uuid* ID of the workspace containing the teamspace. * `created_at` *timestamp* Time at which the teamspace was created. * `updated_at` *timestamp* Time at which the teamspace was last updated. * `created_by` *uuid* ID of the user who created the teamspace. * `updated_by` *uuid* ID of the user who last updated the teamspace. ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2023-11-19T11:56:55.176802Z", "updated_at": "2023-11-19T11:56:55.176809Z", "name": "Engineering Team", "description_html": "

Engineering team workspace

", "lead": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4" } ``` --- --- url: 'https://developers.plane.so/api-reference/teamspace/add-teamspace.html' description: >- Create a teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for create a teamspace. --- # Create a teamspace Create a new teamspace in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Logo props. Name. Description json. Description html. Description stripped. Created by. Updated by. Lead. ### Scopes `teamspaces:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "logo_props": "example-value", "name": "Example Name", "description_json": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/", headers={"X-API-Key": "your-api-key"}, json={ "logo_props": "example-value", "name": "Example Name", "description_json": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ logo_props: "example-value", name: "Example Name", description_json: "example-value", description_html: "

Example content

", description_stripped: "Example description", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", lead: "550e8400-e29b-41d4-a716-446655440000", }), }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description" } ``` --- --- url: 'https://developers.plane.so/api-reference/teamspace/list-teamspaces.html' description: >- List all teamspaces via Plane API. HTTP request format, parameters, scopes, and example responses for list all teamspaces. --- # List all teamspaces List all teamspaces in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `teamspaces:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "created_at": "2024-01-01T00:00:00Z" } ] } ] ``` --- --- url: 'https://developers.plane.so/api-reference/teamspace/get-teamspace-detail.html' description: >- Retrieve a teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a teamspace. --- # Retrieve a teamspace Retrieve a teamspace by its ID ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Scopes `teamspaces:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description" } ``` --- --- url: >- https://developers.plane.so/api-reference/teamspace/update-teamspace-detail.html description: >- Update a teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for update a teamspace. --- # Update a teamspace Update a teamspace by its ID ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Body Parameters Logo props. Name. Description json. Description html. Description stripped. Created by. Updated by. Lead. ### Scopes `teamspaces:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "logo_props": "example-value", "name": "Example Name", "description_json": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "logo_props": "example-value", "name": "Example Name", "description_json": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000", "lead": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ logo_props: "example-value", name: "Example Name", description_json: "example-value", description_html: "

Example content

", description_stripped: "Example description", created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", lead: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description" } ``` --- --- url: 'https://developers.plane.so/api-reference/teamspace/delete-teamspace.html' description: >- Delete a teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for delete a teamspace. --- # Delete a teamspace Delete a teamspace by its ID ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Scopes `teamspaces:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/teamspace/list-teamspace-members.html description: >- List all teamspace members via Plane API. HTTP request format, parameters, scopes, and example responses for list all teamspace members. --- # List all teamspace members List all members in a teamspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Query Parameters Pagination cursor for getting next set of results Number of results per page (default: 20, max: 100) ### Scopes `teamspaces.members:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/?cursor=20:1:0&per_page=20" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/?cursor=20:1:0&per_page=20", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/?cursor=20:1:0&per_page=20", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "created_at": "2024-01-01T00:00:00Z" } ] } ``` --- --- url: 'https://developers.plane.so/api-reference/teamspace/add-teamspace-members.html' description: >- Add members to teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for add members to teamspace. --- # Add members to teamspace Add members to a teamspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Body Parameters Member ids. ### Scopes `teamspaces.members:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "member_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/", headers={"X-API-Key": "your-api-key"}, json={ "member_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ member_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "email": "user@example.com", "avatar": "https://example.com/assets/example-image.png", "avatar_url": "https://example.com/assets/example-image.png", "display_name": "Example Name" } ``` --- --- url: >- https://developers.plane.so/api-reference/teamspace/remove-teamspace-members.html description: >- Remove members from teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for remove members from teamspace. --- # Remove members from teamspace Delete members from a teamspace by its ID ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Scopes `teamspaces.members:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/members/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/teamspace/list-teamspace-projects.html description: >- List teamspace projects via Plane API. HTTP request format, parameters, scopes, and example responses for list teamspace projects. --- # List teamspace projects List all projects in a teamspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Scopes `teamspaces.projects:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2 } ] } ``` --- --- url: >- https://developers.plane.so/api-reference/teamspace/add-projects-to-teamspace.html description: >- Add projects to teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for add projects to teamspace. --- # Add projects to teamspace Add projects to a teamspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Body Parameters Project ids. ### Scopes `teamspaces.projects:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "project_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/", headers={"X-API-Key": "your-api-key"}, json={ "project_ids": [ "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ project_ids: ["550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description": "Example description", "identifier": "PROJ-123", "network": 2, "project_lead": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/teamspace/remove-projects-from-teamspace.html description: >- Remove projects from teamspace via Plane API. HTTP request format, parameters, scopes, and example responses for remove projects from teamspace. --- # Remove projects from teamspace Remove projects from a teamspace by its ID ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the teamspace. ### Scopes `teamspaces.projects:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/teamspaces/teamspace-uuid/projects/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/sticky/overview.html' description: >- Plane Sticky API overview. Learn about endpoints, request/response format, and how to work with sticky via REST API. --- # Overview Stickies are workspace-level notes that allow you to capture quick thoughts, ideas, or important reminders. [Learn more about Stickies](https://docs.plane.so/core-concepts/stickies) ## The Sticky Object ### Attributes * `id` *uuid* Unique identifier for the sticky * `name` *string* Name of the sticky * `description` *object* JSON description of the sticky * `description_html` *string* HTML description of the sticky * `description_stripped` *string* Stripped version of the HTML description * `description_binary` *string* Binary description of the sticky * `logo_props` *object* Logo properties for the sticky * `color` *string* Color of the sticky * `background_color` *string* Background color of the sticky * `workspace` *uuid* Workspace UUID which is automatically saved * `owner` *uuid* User ID of the sticky owner * `sort_order` *number* Sort order for the sticky * `created_at` *timestamp* The timestamp when the sticky was created * `updated_at` *timestamp* The timestamp when the sticky was last updated * `created_by` *uuid* ID of the user who created the sticky * `updated_by` *uuid* ID of the user who last updated the sticky ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "created_at": "2023-11-19T11:56:55.176802Z", "updated_at": "2023-11-19T11:56:55.176809Z", "name": "Important Note", "description": {}, "description_html": "

This is an important note

", "color": "#FF5733", "background_color": "#FFF9E6", "sort_order": 1000.0, "workspace": "cd4ab5a2-1a5f-4516-a6c6-8da1a9fa5be4", "owner": "16c61a3a-512a-48ac-b0be-b6b46fe6f430" } ``` --- --- url: 'https://developers.plane.so/api-reference/sticky/add-sticky.html' description: >- Create a sticky via Plane API. HTTP request format, parameters, scopes, and example responses for create a sticky. --- # Create a sticky Create a new sticky in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Deleted at. Name. Description. Description html. Description stripped. Logo props. Color. Background color. Sort order. Created by. Updated by. ### Scopes `stickies:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "deleted_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "logo_props": "example-value", "color": "Example Name", "background_color": "Example Name", "sort_order": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/", headers={"X-API-Key": "your-api-key"}, json={ "deleted_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "logo_props": "example-value", "color": "Example Name", "background_color": "Example Name", "sort_order": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/stickies/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ deleted_at: "2024-01-01T00:00:00Z", name: "Example Name", description: "example-value", description_html: "

Example content

", description_stripped: "Example description", logo_props: "example-value", color: "Example Name", background_color: "Example Name", sort_order: 1, created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", }), }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description_html": "

Example content

", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/sticky/list-stickies.html' description: >- List all stickies via Plane API. HTTP request format, parameters, scopes, and example responses for list all stickies. --- # List all stickies List all stickies in the workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `stickies:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/stickies/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json [ { "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": true, "prev_page_results": false, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": null, "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description_html": "

Example content

", "created_at": "2024-01-01T00:00:00Z" } ] } ] ``` --- --- url: 'https://developers.plane.so/api-reference/sticky/get-sticky-detail.html' description: >- Retrieve a sticky via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve a sticky. --- # Retrieve a sticky Retrieve a sticky by its ID ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `stickies:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description_html": "

Example content

", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/sticky/update-sticky-detail.html' description: >- Update a sticky via Plane API. HTTP request format, parameters, scopes, and example responses for update a sticky. --- # Update a sticky Update a sticky by its ID ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Deleted at. Name. Description. Description html. Description stripped. Logo props. Color. Background color. Sort order. Created by. Updated by. ### Scopes `stickies:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "deleted_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "logo_props": "example-value", "color": "Example Name", "background_color": "Example Name", "sort_order": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "deleted_at": "2024-01-01T00:00:00Z", "name": "Example Name", "description": "example-value", "description_html": "

Example content

", "description_stripped": "Example description", "logo_props": "example-value", "color": "Example Name", "background_color": "Example Name", "sort_order": 1, "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ deleted_at: "2024-01-01T00:00:00Z", name: "Example Name", description: "example-value", description_html: "

Example content

", description_stripped: "Example description", logo_props: "example-value", color: "Example Name", background_color: "Example Name", sort_order: 1, created_by: "550e8400-e29b-41d4-a716-446655440000", updated_by: "550e8400-e29b-41d4-a716-446655440000", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "description_html": "

Example content

", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/sticky/delete-sticky.html' description: >- Delete a sticky via Plane API. HTTP request format, parameters, scopes, and example responses for delete a sticky. --- # Delete a sticky Delete a sticky by its ID ### Path Parameters The unique identifier of the resource. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `stickies:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/stickies/resource-id-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/workspace-features/overview.html' description: >- Plane Workspace Features API overview. Learn how to inspect and update workspace feature flags with the Plane API. --- # Overview Workspace features control which major Plane capabilities are enabled for a workspace. [Learn more about using the Plane API](https://developers.plane.so/api-reference/introduction) ## The Workspace Feature Object ### Attributes * `project_grouping` *boolean* Project grouping. * `initiatives` *boolean* Initiatives. * `teams` *boolean* Teams. * `customers` *boolean* Customers. * `wiki` *boolean* Wiki. * `pi` *boolean* Pi. ```json { "project_grouping": true, "initiatives": true, "teams": true, "customers": true, "wiki": true, "pi": true } ``` --- --- url: >- https://developers.plane.so/api-reference/workspace-features/get-workspace-features.html description: >- Get workspace features via Plane API. HTTP request format, parameters, scopes, and example responses for get workspace features. --- # Get workspace features Get the features of a workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `workspaces.features:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/features/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/features/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/features/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json { "project_grouping": true, "initiatives": true, "teams": true, "customers": true, "wiki": true, "pi": true } ``` --- --- url: >- https://developers.plane.so/api-reference/workspace-features/update-workspace-features.html description: >- Update workspace features via Plane API. HTTP request format, parameters, scopes, and example responses for update workspace features. --- # Update workspace features Update the features of a workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Project grouping. Initiatives. Teams. Customers. Wiki. Pi. ### Scopes `workspaces.features:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/features/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "project_grouping": true, "initiatives": true, "teams": true, "customers": true, "wiki": true, "pi": true }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/features/", headers={"X-API-Key": "your-api-key"}, json={ "project_grouping": true, "initiatives": true, "teams": true, "customers": true, "wiki": true, "pi": true } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/features/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ project_grouping: true, initiatives: true, teams: true, customers: true, wiki: true, pi: true, }), }); const data = await response.json(); ``` ```json { "project_grouping": true, "initiatives": true, "teams": true, "customers": true, "wiki": true, "pi": true } ``` --- --- url: 'https://developers.plane.so/api-reference/workspace-invitations/overview.html' description: >- Plane Workspace Invitations API overview. Learn how to create and manage workspace invitations with the Plane API. --- # Overview Workspace invitations let admins invite users to join a workspace with specific access settings. [Learn more about Members](https://developers.plane.so/api-reference/members/overview) ## The Workspace Invitation Object ### Attributes * `id` *string* Id. * `email` *string* Email. * `role` *integer* * `20` - Admin - `15` - Member - `5` - Guest * `created_at` *string* Created at. * `updated_at` *string* Updated at. * `responded_at` *string* Responded at. * `accepted` *boolean* Accepted. ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "Example Name", "role": 20, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "responded_at": "2024-01-01T00:00:00Z", "accepted": true } ``` --- --- url: >- https://developers.plane.so/api-reference/workspace-invitations/add-workspace-invitation.html description: >- Create workspace invitation via Plane API. HTTP request format, parameters, scopes, and example responses for create workspace invitation. --- # Create workspace invitation Create a workspace invite ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Email. * `20` - Admin * `15` - Member * `5` - Guest ### Scopes Workspace admin or owner permission required. ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "Example Name", "role": 20 }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/", headers={"X-API-Key": "your-api-key"}, json={ "email": "Example Name", "role": 20 } ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/invitations/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ email: "Example Name", role: 20, }), }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "Example Name", "role": 20, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "responded_at": "2024-01-01T00:00:00Z", "accepted": true } ``` --- --- url: >- https://developers.plane.so/api-reference/workspace-invitations/list-workspace-invitations.html description: >- List workspace invitations via Plane API. HTTP request format, parameters, scopes, and example responses for list workspace invitations. --- # List workspace invitations List all workspace invites for a workspace ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes Workspace admin or owner permission required. ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/invitations/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "Example Name", "role": 20, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "responded_at": "2024-01-01T00:00:00Z", "accepted": true } ] ``` --- --- url: >- https://developers.plane.so/api-reference/workspace-invitations/get-workspace-invitation-detail.html description: >- Get workspace invitation via Plane API. HTTP request format, parameters, scopes, and example responses for get workspace invitation. --- # Get workspace invitation Get a workspace invite by ID ### Path Parameters The unique identifier of the invitation. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes Workspace admin or owner permission required. ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "Example Name", "role": 20, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "responded_at": "2024-01-01T00:00:00Z", "accepted": true } ``` --- --- url: >- https://developers.plane.so/api-reference/workspace-invitations/update-workspace-invitation.html description: >- Update workspace invitation via Plane API. HTTP request format, parameters, scopes, and example responses for update workspace invitation. --- # Update workspace invitation Update a workspace invite ### Path Parameters The unique identifier of the invitation. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Email. * `20` - Admin * `15` - Member * `5` - Guest ### Scopes Workspace admin or owner permission required. ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "Example Name", "role": 20 }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "email": "Example Name", "role": 20 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ email: "Example Name", role: 20, }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "Example Name", "role": 20, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "responded_at": "2024-01-01T00:00:00Z", "accepted": true } ``` --- --- url: >- https://developers.plane.so/api-reference/workspace-invitations/delete-workspace-invitation.html description: >- Delete workspace invitation via Plane API. HTTP request format, parameters, scopes, and example responses for delete workspace invitation. --- # Delete workspace invitation Delete a workspace invite ### Path Parameters The unique identifier of the invitation. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes Workspace admin or owner permission required. ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/invitations/invitation-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/members/overview.html' description: >- Plane Members API overview. Learn about endpoints, request/response format, and how to work with members via REST API. --- # Overview Members represent users who belong to a workspace or project. The Members API allows you to retrieve information about workspace and project members. [Learn more about Members](https://docs.plane.so/core-concepts/workspaces/members) ## The Members Object ### Attributes * `id` *string* Unique identifier for the Member * `first_name` *string* First name of the Member * `last_name` *string* Last name of the Member * `email` *string* Email address of the Member * `avatar` *string* Optional avatar image file reference * `avatar_url` *string* Publicly accessible URL for the avatar image * `display_name` *string* Display name shown across the application * `role` *integer* Role of the Member in the Workspace or Project ```json { "id": "00000000-0000-0000-0000-000000000001", "first_name": "User", "last_name": "One", "email": "user1@example.com", "avatar": "", "avatar_url": null, "display_name": "user1", "role": 15 } ``` --- --- url: 'https://developers.plane.so/api-reference/members/get-workspace-members.html' description: >- Get all workspace members via Plane API. HTTP request format, parameters, scopes, and example responses for get all workspace members. --- # Get all workspace members Retrieve all users who are members of the specified workspace. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `workspaces.members:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/members/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/members/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/workspaces/my-workspace/members/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json [ { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "display_name": "Example Name", "email": "user@example.com", "avatar": "https://example.com/assets/example-image.png", "role": 20 }, { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jane", "last_name": "Smith", "display_name": "Example Name", "email": "user@example.com", "avatar": "https://example.com/assets/example-image.png", "role": 15 } ] ``` --- --- url: 'https://developers.plane.so/api-reference/members/get-project-members.html' description: >- List all project members via Plane API. HTTP request format, parameters, scopes, and example responses for list all project members. --- # List all project members Retrieve all users who are members of the specified project. ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.members:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ [ { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "display_name": "Example Name", "email": "user@example.com", "avatar": "https://example.com/assets/example-image.png" }, { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jane", "last_name": "Smith", "display_name": "Example Name", "email": "user@example.com", "avatar": "https://example.com/assets/example-image.png" } ] ] ``` --- --- url: 'https://developers.plane.so/api-reference/members/add-project-member.html' description: >- Create project member via Plane API. HTTP request format, parameters, scopes, and example responses for create project member. --- # Create project member Create a new project member ### Path Parameters The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Member. * `20` - Admin * `15` - Member * `5` - Guest ### Scopes `projects.members:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "member": "550e8400-e29b-41d4-a716-446655440000", "role": 20 }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/", headers={"X-API-Key": "your-api-key"}, json={ "member": "550e8400-e29b-41d4-a716-446655440000", "role": 20 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ member: "550e8400-e29b-41d4-a716-446655440000", role: 20, }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "member": "550e8400-e29b-41d4-a716-446655440000", "role": 20 } ``` --- --- url: >- https://developers.plane.so/api-reference/members/get-project-member-detail.html description: >- Get project member via Plane API. HTTP request format, parameters, scopes, and example responses for get project member. --- # Get project member Retrieve a project member by ID. ### Path Parameters The unique identifier of the member. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.members:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "member": "550e8400-e29b-41d4-a716-446655440000", "role": 20 } ``` --- --- url: 'https://developers.plane.so/api-reference/members/update-project-member.html' description: >- Update project member via Plane API. HTTP request format, parameters, scopes, and example responses for update project member. --- # Update project member Update a project member ### Path Parameters The unique identifier of the member. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Member. * `20` - Admin * `15` - Member * `5` - Guest ### Scopes `projects.members:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "member": "550e8400-e29b-41d4-a716-446655440000", "role": 20 }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/", headers={"X-API-Key": "your-api-key"}, json={ "member": "550e8400-e29b-41d4-a716-446655440000", "role": 20 } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ member: "550e8400-e29b-41d4-a716-446655440000", role: 20, }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "member": "550e8400-e29b-41d4-a716-446655440000", "role": 20 } ``` --- --- url: 'https://developers.plane.so/api-reference/members/delete-project-member.html' description: >- Delete project member via Plane API. HTTP request format, parameters, scopes, and example responses for delete project member. --- # Delete project member Delete a project member ### Path Parameters The unique identifier of the member. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `projects.members:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/project-members/member-uuid/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/members/remove-workspace-member.html' description: >- Remove workspace member via Plane API. HTTP request format, parameters, scopes, and example responses for remove workspace member. --- # Remove workspace member Remove a member from the workspace, deactivate them from all projects, and reduce the seat count. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Scopes `workspaces.members:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/members/remove/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/members/remove/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/members/remove/", { method: "POST", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/api-reference/user/overview.html' description: >- Plane User API overview. Learn about endpoints, request/response format, and how to work with user via REST API. --- # Overview Users represent the people who use Plane. The Users API allows you to retrieve information about the current authenticated user. ## The User Object ### Attributes * `id` *uuid* Unique identifier for the user * `first_name` *string* First name of the user * `last_name` *string* Last name of the user * `email` *string* Email address of the user * `avatar` *string* Avatar identifier for the user * `avatar_url` *string* URL of the user's avatar image * `display_name` *string* Display name of the user ```json { "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430", "first_name": "John", "last_name": "Doe", "email": "john.doe@example.com", "avatar": "avatar-123", "avatar_url": "https://example.com/avatars/avatar-123.png", "display_name": "John Doe" } ``` --- --- url: 'https://developers.plane.so/api-reference/user/get-current-user.html' description: >- Retrieve current user via Plane API. HTTP request format, parameters, scopes, and example responses for retrieve current user. --- # Retrieve current user Retrieve the authenticated user's profile information including basic details. ### Scopes `profile:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/users/me/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/users/me/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch("https://api.plane.so/api/v1/users/me/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "email": "user@example.com", "avatar": "https://example.com/assets/example-image.png", "avatar_url": "https://example.com/assets/example-image.png", "display_name": "Example Name" } ``` --- --- url: 'https://developers.plane.so/api-reference/idp-group-sync/overview.html' description: >- Plane IDP Group Sync API overview. Learn about endpoints, request/response format, and how to manage IdP group sync configuration and mappings via REST API. --- # Overview IDP Group Sync lets workspace admins programmatically manage how IdP groups map to Plane projects and workspaces. All endpoints are workspace-scoped and require workspace admin permissions. ### The Group Sync Config Object ### Attributes * `id` *uuid* Unique identifier of the config. * `is_enabled` *boolean* Whether IdP group sync is enabled for the workspace. * `sync_on_login` *boolean* Sync group memberships automatically on user login. * `auto_remove` *boolean* Automatically remove users from projects/workspace when they are removed from the IdP group. * `sync_offline` *boolean* Allow sync to run outside of login events. * `group_attribute_key` *string* The IdP claim key that contains group membership data (e.g. `groups`). * `default_workspace_role` *string* Role slug assigned to users when added to the workspace via group sync. * `created_at` *timestamp* The timestamp of when the config was created. * `updated_at` *timestamp* The timestamp of when the config was last updated. ### The Project Group Mapping Object ### Attributes * `id` *uuid* Unique identifier of the mapping. * `idp_group_name` *string* The name of the IdP group to map. * `project` *string* Project identifier the group is mapped to. Mutually exclusive with `all_projects`. * `all_projects` *boolean* When `true`, maps the group to all projects in the workspace. Mutually exclusive with `project`. * `role` *string* Role slug assigned to group members within the project. * `created_at` *timestamp* The timestamp of when the mapping was created. * `updated_at` *timestamp* The timestamp of when the mapping was last updated. ### The Workspace Group Mapping Object ### Attributes * `id` *uuid* Unique identifier of the mapping. * `idp_group_name` *string* The name of the IdP group to map. * `role` *string* Role slug assigned to group members within the workspace. * `created_at` *timestamp* The timestamp of when the mapping was created. * `updated_at` *timestamp* The timestamp of when the mapping was last updated. ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "is_enabled": true, "sync_on_login": true, "auto_remove": false, "sync_offline": false, "group_attribute_key": "groups", "default_workspace_role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` ```json { "id": "661f9511-f30c-52e5-b827-557766551111", "idp_group_name": "engineering", "project": "ENG", "all_projects": false, "role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` ```json { "id": "772g0622-g41d-63f6-c938-668877662222", "idp_group_name": "leadership", "role": "admin", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/get-group-sync-config.html description: >- Get IdP group sync configuration via Plane API. HTTP request format, parameters, scopes, and example responses for get group sync config. --- # Get group sync config Retrieve the IdP group sync configuration for the workspace. Auto-creates the config with defaults on first access. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Response Attributes Unique identifier of the config. Whether IdP group sync is enabled for the workspace. Sync group memberships automatically on user login. Automatically remove users from projects or workspace when removed from their IdP group. Allow sync to run outside of login events. The IdP claim key that contains group membership data (e.g. `groups`). Role slug assigned to users when added to the workspace via group sync. The timestamp of when the config was created. The timestamp of when the config was last updated. ### Scopes `workspaces.group_sync:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/config/" \ -H "X-API-Key: $PLANE_API_KEY" # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/config/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/config/", { headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "is_enabled": true, "sync_on_login": true, "auto_remove": false, "sync_offline": false, "group_attribute_key": "groups", "default_workspace_role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/update-group-sync-config.html description: >- Update IdP group sync configuration via Plane API. HTTP request format, parameters, scopes, and example responses for update group sync config. --- # Update group sync config Update the IdP group sync configuration for the workspace. Supports partial updates — only include fields you want to change. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Enable or disable IdP group sync for the workspace. Sync group memberships automatically on user login. Automatically remove users from projects or workspace when removed from their IdP group. Allow sync to run outside of login events. The IdP claim key that contains group membership data (e.g. `groups`). Role slug assigned to users when added to the workspace via group sync. ### Scopes `workspaces.group_sync:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/config/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "is_enabled": true, "sync_on_login": true, "auto_remove": false, "sync_offline": false, "group_attribute_key": "groups", "default_workspace_role": "member" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/config/", headers={"X-API-Key": "your-api-key"}, json={ "is_enabled": True, "sync_on_login": True, "auto_remove": False, "sync_offline": False, "group_attribute_key": "groups", "default_workspace_role": "member" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/config/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ is_enabled: true, sync_on_login: true, auto_remove: false, sync_offline: false, group_attribute_key: "groups", default_workspace_role: "member", }), }, ); const data = await response.json(); ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "is_enabled": true, "sync_on_login": true, "auto_remove": false, "sync_offline": false, "group_attribute_key": "groups", "default_workspace_role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/list-project-mappings.html description: >- List project group mappings via Plane API. HTTP request format, parameters, scopes, and example responses for list project group mappings. --- # List project group mappings Retrieve all IdP group → project mappings for the workspace. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Response Attributes Unique identifier of the mapping. The name of the IdP group. Project identifier the group is mapped to. `null` when `all_projects` is `true`. Whether the group is mapped to all projects in the workspace. Role slug assigned to group members within the project. The timestamp of when the mapping was created. The timestamp of when the mapping was last updated. ### Scopes `workspaces.group_sync:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/" \ -H "X-API-Key: $PLANE_API_KEY" # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/", { headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "661f9511-f30c-52e5-b827-557766551111", "idp_group_name": "engineering", "project": "ENG", "all_projects": false, "role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" }, { "id": "772g0622-g41d-63f6-c938-668877662222", "idp_group_name": "all-staff", "project": null, "all_projects": true, "role": "guest", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ] ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/create-project-mapping.html description: >- Create a project group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for create project group mapping. --- # Create project group mapping Create a new IdP group → project mapping. Use `project` to map to a specific project, or `all_projects: true` to map to all projects in the workspace. These two fields are mutually exclusive. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters The name of the IdP group to map. Project role slug to assign to members of the IdP group (e.g. `member`, `admin`, `guest`). Project identifier to map the group to (e.g. `ENG`). Mutually exclusive with `all_projects`. When `true`, maps the group to all projects in the workspace. Mutually exclusive with `project`. ### Scopes `workspaces.group_sync:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idp_group_name": "engineering", "project": "ENG", "role": "member" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/", headers={"X-API-Key": "your-api-key"}, json={ "idp_group_name": "engineering", "project": "ENG", "role": "member" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ idp_group_name: "engineering", project: "ENG", role: "member", }), }, ); const data = await response.json(); ``` ```json { "id": "661f9511-f30c-52e5-b827-557766551111", "idp_group_name": "engineering", "project": "ENG", "all_projects": false, "role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/get-project-mapping.html description: >- Get a project group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for get project group mapping. --- # Get project group mapping Retrieve a single IdP group → project mapping by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project group mapping. ### Response Attributes Unique identifier of the mapping. The name of the IdP group. Project identifier the group is mapped to. `null` when `all_projects` is `true`. Whether the group is mapped to all projects in the workspace. Role slug assigned to group members within the project. The timestamp of when the mapping was created. The timestamp of when the mapping was last updated. ### Scopes `workspaces.group_sync:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/" \ -H "X-API-Key: $PLANE_API_KEY" # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/", { headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "661f9511-f30c-52e5-b827-557766551111", "idp_group_name": "engineering", "project": "ENG", "all_projects": false, "role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/update-project-mapping.html description: >- Update a project group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for update project group mapping. --- # Update project group mapping Update an existing IdP group → project mapping. Supports partial updates. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project group mapping. ### Body Parameters The name of the IdP group to map. Project role slug to assign to members of the IdP group (e.g. `member`, `admin`, `guest`). Project identifier to map the group to (e.g. `ENG`). Mutually exclusive with `all_projects`. When `true`, maps the group to all projects in the workspace. Mutually exclusive with `project`. ### Scopes `workspaces.group_sync:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "role": "admin" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/", headers={"X-API-Key": "your-api-key"}, json={"role": "admin"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ role: "admin" }), }, ); const data = await response.json(); ``` ```json { "id": "661f9511-f30c-52e5-b827-557766551111", "idp_group_name": "engineering", "project": "ENG", "all_projects": false, "role": "admin", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/delete-project-mapping.html description: >- Delete a project group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for delete project group mapping. --- # Delete project group mapping Delete an IdP group → project mapping. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the project group mapping. ### Scopes `workspaces.group_sync:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/" \ -H "X-API-Key: $PLANE_API_KEY" # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/project-mappings/661f9511-f30c-52e5-b827-557766551111/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/list-workspace-mappings.html description: >- List workspace group mappings via Plane API. HTTP request format, parameters, scopes, and example responses for list workspace group mappings. --- # List workspace group mappings Retrieve all IdP group → workspace role mappings for the workspace. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Response Attributes Unique identifier of the mapping. The name of the IdP group. Role slug assigned to group members within the workspace. The timestamp of when the mapping was created. The timestamp of when the mapping was last updated. ### Scopes `workspaces.group_sync:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/" \ -H "X-API-Key: $PLANE_API_KEY" # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/", { headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json [ { "id": "772g0622-g41d-63f6-c938-668877662222", "idp_group_name": "leadership", "role": "admin", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" }, { "id": "883h1733-h52e-74g7-d049-779988773333", "idp_group_name": "engineering", "role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ] ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/create-workspace-mapping.html description: >- Create a workspace group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for create workspace group mapping. --- # Create workspace group mapping Create a new IdP group → workspace role mapping. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters The name of the IdP group to map. Workspace role slug to assign to members of the IdP group (e.g. `member`, `admin`). ### Scopes `workspaces.group_sync:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idp_group_name": "leadership", "role": "admin" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/", headers={"X-API-Key": "your-api-key"}, json={ "idp_group_name": "leadership", "role": "admin" } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ idp_group_name: "leadership", role: "admin", }), }, ); const data = await response.json(); ``` ```json { "id": "772g0622-g41d-63f6-c938-668877662222", "idp_group_name": "leadership", "role": "admin", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/get-workspace-mapping.html description: >- Get a workspace group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for get workspace group mapping. --- # Get workspace group mapping Retrieve a single IdP group → workspace role mapping by its ID. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the workspace group mapping. ### Response Attributes Unique identifier of the mapping. The name of the IdP group. Role slug assigned to group members within the workspace. The timestamp of when the mapping was created. The timestamp of when the mapping was last updated. ### Scopes `workspaces.group_sync:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/" \ -H "X-API-Key: $PLANE_API_KEY" # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/", { headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "772g0622-g41d-63f6-c938-668877662222", "idp_group_name": "leadership", "role": "admin", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/update-workspace-mapping.html description: >- Update a workspace group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for update workspace group mapping. --- # Update workspace group mapping Update an existing IdP group → workspace role mapping. Supports partial updates. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the workspace group mapping. ### Body Parameters The name of the IdP group to map. Workspace role slug to assign to members of the IdP group (e.g. `member`, `admin`). ### Scopes `workspaces.group_sync:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "role": "member" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/", headers={"X-API-Key": "your-api-key"}, json={"role": "member"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ role: "member" }), }, ); const data = await response.json(); ``` ```json { "id": "772g0622-g41d-63f6-c938-668877662222", "idp_group_name": "leadership", "role": "member", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/idp-group-sync/delete-workspace-mapping.html description: >- Delete a workspace group mapping via Plane API. HTTP request format, parameters, scopes, and example responses for delete workspace group mapping. --- # Delete workspace group mapping Delete an IdP group → workspace role mapping. ### Path Parameters The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. The unique identifier of the workspace group mapping. ### Scopes `workspaces.group_sync:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/" \ -H "X-API-Key: $PLANE_API_KEY" # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/", headers={"X-API-Key": "your-api-key"} ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/group-sync/workspace-mappings/772g0622-g41d-63f6-c938-668877662222/", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); console.log(response.status); ``` No response body. --- --- url: 'https://developers.plane.so/dev-tools/agents/overview.html' description: >- Learn how to build AI agents that integrate with Plane workspaces, enabling automated task handling, intelligent responses, and seamless collaboration. --- # Agents overview ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. ::: ## What are agents in Plane? Agents in Plane are AI-powered applications that can interact with your workspace similar to how human users do. They can be @mentioned in work item comments, receive prompts from users, and respond with intelligent actions. Agents enable automation and AI assistance directly within your project management workflow. Key capabilities of Plane agents: * **Mentionable** — Users can @mention agents in work item comments to trigger interactions * **Contextual awareness** — Agents receive full context about the work item, project, and conversation * **Activity tracking** — All agent interactions are tracked through the Agent Run system * **Real-time responses** — Agents can send thoughts, actions, and responses back to users ## Agent installation Agents are installed as OAuth applications in your Plane workspace. When you create an OAuth app with the **Enable App Mentions** option enabled, it becomes an agent that users can mention and interact with. ### Installation flow 1. A workspace admin installs your agent via the OAuth consent flow 2. Plane creates a **bot user** for your agent in that workspace 3. The bot user ID is returned in the app installation details 4. Users can now @mention your agent in work item comments When installed, agents appear in the mention picker alongside regular workspace members. Users can tag them in comments just like any team member. ::: info Agents installed in your workspace do not count as billable users. ::: ## Agent Run lifecycle The Agent Run system tracks the complete lifecycle of an agent interaction, from when a user mentions the agent to when the agent completes its task. ### What is an Agent Run? An **Agent Run** represents a single interaction session between a user and an agent. When a user @mentions an agent in a comment, Plane automatically creates an Agent Run to track: * The triggering comment and work item * The conversation thread between user and agent * All activities (thoughts, actions, responses) from the agent * The current status of the interaction ### What is an Agent Run Activity? An **Agent Run Activity** is a single unit of communication within an Agent Run. Activities can be: * **Prompt** — A message from a user to the agent * **Thought** — Internal reasoning from the agent (ephemeral) * **Action** — A tool invocation by the agent (ephemeral) * **Response** — A final response from the agent (creates a comment) * **Elicitation** — A question from the agent requesting user input * **Error** — An error message from the agent ### How Agent Run works The Agent Run flow consists of three main phases: ```mermaid sequenceDiagram participant User participant Plane participant Agent User->>Plane: @mentions agent in comment Plane->>Plane: Create AgentRun + Activity (prompt) Plane->>Agent: Send webhook (agent_run_activity) loop Agent Processing Agent->>Plane: Create Activity (thought/action) Agent->>Agent: Process and reason end Agent->>Plane: Create Activity (response) Plane->>User: Show response as comment ``` #### Phase 1: Trigger 1. User @mentions the agent in a work item comment 2. Plane detects the mention and creates a new **Agent Run** 3. An **Agent Run Activity** is created with the user's prompt 4. The Agent Run status is set to `created` #### Phase 2: Webhook 1. Plane triggers a webhook to your agent's webhook URL 2. The webhook payload includes: * The Agent Run details * The triggering activity (user prompt) * Work item and project context * Workspace information #### Phase 3: Agent response 1. Your agent processes the webhook and starts working 2. The agent sends activities back to Plane via the API: * `thought` activities to show reasoning (ephemeral, don't create comments) * `action` activities to show tool usage (ephemeral) * `response` or `elicitation` when complete (creates a comment) 3. Plane updates the Agent Run status based on activities 4. Non-ephemeral activities (response, elicitation) create comment replies visible to users ### Agent Run states Agent Runs transition through various states based on activities: | Status | Description | | ------------- | ----------------------------------------------------------------- | | `created` | The run has been initiated but not yet started processing | | `in_progress` | The agent is actively processing the request | | `awaiting` | The agent is waiting for additional input from the user | | `completed` | The agent has successfully finished processing | | `stopping` | A stop request has been received and is being processed | | `stopped` | The run has been successfully stopped | | `failed` | The run encountered an error and cannot continue | | `stale` | The run has not been updated in 5 minutes and is considered stale | ### Continuing a conversation When a user replies to an agent's response: 1. If an active Agent Run exists for that thread, the reply is added as a new activity 2. The webhook is triggered again with the updated context 3. The agent can continue the conversation with full history This enables multi-turn conversations where users and agents can have back-and-forth interactions. ## Next steps Ready to build your own agent? Continue to [Building an Agent](/dev-tools/agents/building-an-agent) to learn how to create and deploy your first Plane agent. --- --- url: 'https://developers.plane.so/dev-tools/agents/building-an-agent.html' description: >- Step-by-step guide to creating a Plane agent, including OAuth setup, webhook handling, and activity creation. --- # Building an agent ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. ::: ## Prerequisites Before building an agent, make sure you have completed the following: 1. **Build a Plane app** — Follow the [Build a Plane App](/dev-tools/build-plane-app/overview) guide to understand OAuth flows, deployment, and webhook handling. 2. **Get your bot token** — Complete the [Bot Token Flow](/dev-tools/build-plane-app/choose-token-flow) to obtain a `bot_token` for your agent. This token is used for all API calls. 3. **Set up webhook handling** — Ensure your server can [receive and verify webhooks](/dev-tools/build-plane-app/webhooks) from Plane. ::: info This guide assumes you have a working OAuth app with webhook handling. If not, complete the [Build a Plane App](/dev-tools/build-plane-app/overview) guide first. ::: ## Creating an agent Building a Plane agent involves three main steps: 1. Create an OAuth application with agent capabilities enabled 2. Implement the OAuth flow to install your agent in workspaces 3. Handle webhooks and create activities to respond to users ### OAuth app creation To create an agent, you first need to [register an OAuth application](/dev-tools/build-plane-app/create-oauth-application) with the **Enable App Mentions** checkbox enabled. 1. Navigate to `https://app.plane.so//settings/integrations/` 2. Click on **Build your own** button 3. Fill out the required details: * **Setup URL**: The URL users are redirected to when installing your app * **Redirect URIs**: Where Plane sends the authorization code after consent * **Webhook URL Endpoint**: Your service's webhook endpoint for receiving events 4. **Enable the "Enable App Mentions" checkbox** — This is required for agents 5. **Choose the "Agent Run" scopes** — This is required for agents to be able to create run activities and get run details. See [OAuth Scopes](/dev-tools/build-plane-app/oauth-scopes#agent-run-scopes) for more information on the available scopes. 6. Save and securely store your **Client ID** and **Client Secret** ::: info The "Enable App Mentions" checkbox is what transforms a regular OAuth app into an agent that can be @mentioned in work items. ::: ### Setting is mentionable When you enable app mentions during OAuth app creation, your application becomes mentionable in work item comments. This means: * Users will see your agent in the mention picker when typing `@` * Your agent can be assigned or delegated work items * Webhooks will be triggered when users interact with your agent After installation, your agent appears alongside workspace members in the mention autocomplete. ## Agent interaction Once your agent is installed via the [OAuth consent flow](/dev-tools/build-plane-app/choose-token-flow) and users start mentioning it, you need to handle the interactions through Agent Runs and Activities. ### AgentRun An **AgentRun** tracks a complete interaction session between a user and your agent. #### Key fields | Field | Type | Description | | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `id` | UUID | Unique identifier for the agent run | | `agent_user` | UUID | The bot user ID representing your agent | | `issue` | UUID | The work item where the interaction started | | `project` | UUID | The project containing the work item | | `workspace` | UUID | The workspace where the agent is installed | | `comment` | UUID | The comment thread for this run | | `source_comment` | UUID | The original comment that triggered the run | | `creator` | UUID | The user who initiated the run | | `status` | String | Current status (`created`, `in_progress`, `awaiting`, `completed`, `stopping`, `stopped`, `failed`, `stale`) | | `started_at` | DateTime | When the run started | | `ended_at` | DateTime | When the run ended (if applicable) | | `stopped_at` | DateTime | When a stop was requested | | `stopped_by` | UUID | User who requested the stop | | `external_link` | URL | Optional link to external dashboard/logs | | `error_metadata` | JSON | Error details if the run failed | | `type` | String | Type of run (currently `comment_thread`) | ### AgentRunActivity An **AgentRunActivity** represents a single message or action within an Agent Run. #### Key fields | Field | Type | Description | | ------------------ | ------- | ------------------------------------------------------------------------------------ | | `id` | UUID | Unique identifier for the activity | | `agent_run` | UUID | The parent Agent Run | | `type` | String | Activity type (`prompt`, `thought`, `action`, `response`, `elicitation`, `error`) | | `content` | JSON | The activity content (structure varies by type) | | `content_metadata` | JSON | Additional metadata about the content | | `ephemeral` | Boolean | If true, the activity is temporary and won't create a comment | | `signal` | String | Signal for how to handle the activity (`continue`, `stop`, `auth_request`, `select`) | | `signal_metadata` | JSON | Additional signal data | | `actor` | UUID | The user or bot that created the activity | | `comment` | UUID | Associated comment (for non-ephemeral activities) | ### Creating activities Your agent communicates back to users by creating activities. We recommend using the official SDKs which provide typed helpers and insulate your code from API changes. #### Install the SDK :::tabs key:language \== Node.js {#nodejs} ```bash npm install @makeplane/plane-node-sdk ``` \== Python {#python} ```bash pip install plane-sdk ``` ::: #### Activity examples :::tabs key:language \== TypeScript {#typescript} ```typescript import { PlaneClient } from "@makeplane/plane-node-sdk"; // Initialize the client with your bot token const planeClient = new PlaneClient({ baseUrl: process.env.PLANE_API_URL || "https://api.plane.so", accessToken: botToken, }); // Create a thought activity (ephemeral - shows agent's reasoning) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "thought", content: { type: "thought", body: "Analyzing the user's request about weather data...", }, }); // Create an action activity (ephemeral - shows tool usage) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "action", content: { type: "action", action: "getWeather", parameters: { location: "San Francisco" }, }, }); // Create a response activity (creates a visible comment) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "response", content: { type: "response", body: "The weather in San Francisco is currently 68°F with partly cloudy skies.", }, signal: "continue", }); // Create an elicitation activity (asks user for input) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "elicitation", content: { type: "elicitation", body: "Which city would you like me to check the weather for?", }, }); // Create an error activity await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "error", content: { type: "error", body: "Unable to fetch weather data. Please try again later.", }, }); ``` \== Python {#python} ```python from plane import PlaneClient from plane.models.agent_runs import CreateAgentRunActivity # Initialize the client with your bot token plane_client = PlaneClient( base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), access_token=bot_token, ) # Create a thought activity (ephemeral - shows agent's reasoning) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="thought", content={ "type": "thought", "body": "Analyzing the user's request about weather data...", }, ), ) # Create an action activity (ephemeral - shows tool usage) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="action", content={ "type": "action", "action": "getWeather", "parameters": {"location": "San Francisco"}, }, ), ) # Create a response activity (creates a visible comment) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="response", content={ "type": "response", "body": "The weather in San Francisco is currently 68°F with partly cloudy skies.", }, signal="continue", ), ) # Create an elicitation activity (asks user for input) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="elicitation", content={ "type": "elicitation", "body": "Which city would you like me to check the weather for?", }, ), ) # Create an error activity plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="error", content={ "type": "error", "body": "Unable to fetch weather data. Please try again later.", }, ), ) ``` ::: ### Content payload types The `content` field structure varies based on the activity type: ::: details Thought Internal reasoning from the agent. Automatically marked as ephemeral. ```json { "type": "thought", "body": "The user is asking about weather data for their location." } ``` ::: ::: details Action A tool invocation. Automatically marked as ephemeral. You can include results after execution. ```json { "type": "action", "action": "searchDatabase", "parameters": { "query": "weather API", "limit": "10" } } ``` With result: ```json { "type": "action", "action": "searchDatabase", "parameters": { "query": "weather API", "result": "Found 3 matching records" } } ``` ::: ::: details Response A final response to the user. Creates a comment reply. ```json { "type": "response", "body": "Here's the weather forecast for San Francisco..." } ``` ::: ::: details Elicitation A question requesting user input. Creates a comment and sets run to `awaiting`. ```json { "type": "elicitation", "body": "Could you please specify which date range you're interested in?" } ``` ::: ::: details Error An error message. Creates a comment and sets run to `failed`. ```json { "type": "error", "body": "I encountered an error while processing your request." } ``` ::: ### Signals Signals provide additional context about how an activity should be interpreted: | Signal | Description | | -------------- | --------------------------------------------------------- | | `continue` | Default signal, indicates the conversation can continue | | `stop` | User requested to stop the agent run | | `auth_request` | Agent needs user to authenticate with an external service | | `select` | Agent is presenting options for user to select from | See [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for detailed information. ### Ephemeral activities Activities with `ephemeral: true` are temporary and don't create comments. They're useful for showing agent progress without cluttering the conversation. The following activity types are automatically marked as ephemeral: * `thought` * `action` * `error` Ephemeral activities are displayed temporarily in the UI and replaced when the next activity arrives. ## AgentRun webhooks Your agent receives webhooks when users interact with it. There are two main webhook events: ### AgentRun create webhook Triggered when a new Agent Run is created (user first mentions your agent). **Event:** `agent_run_create` **Payload:** ```json { "action": "created", "agent_run": { "id": "uuid", "agent_user": "uuid", "issue": "uuid", "project": "uuid", "workspace": "uuid", "status": "created", "type": "comment_thread", "started_at": "2025-01-15T10:30:00Z" }, "agent_user_id": "uuid", "app_client_id": "your-client-id", "issue_id": "uuid", "project_id": "uuid", "workspace_id": "uuid", "comment_id": "uuid", "type": "agent_run" } ``` ### AgentRun activity webhook Triggered when a user sends a prompt to your agent (initial mention or follow-up). **Event:** `agent_run_user_prompt` **Payload:** ```json { "action": "prompted", "agent_run_activity": { "id": "uuid", "agent_run": "uuid", "type": "prompt", "content": { "type": "prompt", "body": "What's the weather like in San Francisco?" }, "ephemeral": false, "signal": "continue", "actor": "uuid", "workspace": "uuid" }, "agent_run": { "id": "uuid", "agent_user": "uuid", "issue": "uuid", "project": "uuid", "workspace": "uuid", "status": "in_progress" }, "agent_user_id": "uuid", "app_client_id": "your-client-id", "comment_id": "uuid", "issue_id": "uuid", "project_id": "uuid", "workspace_id": "uuid", "type": "agent_run_activity" } ``` ### Handling webhooks Here's a complete example of handling agent webhooks using the SDKs: :::tabs key:language \== TypeScript {#typescript} ```typescript import { PlaneClient } from "@makeplane/plane-node-sdk"; interface AgentRunActivityWebhook { action: string; agent_run_activity: { id: string; content: { type: string; body?: string }; signal: string; }; agent_run: { id: string; status: string; }; workspace_id: string; project_id: string; type: string; } async function handleWebhook( webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }, ) { // Only handle agent_run_activity webhooks if (webhook.type !== "agent_run_activity") { return; } const planeClient = new PlaneClient({ baseUrl: process.env.PLANE_API_URL || "https://api.plane.so", accessToken: credentials.bot_token, }); const agentRunId = webhook.agent_run.id; const userPrompt = webhook.agent_run_activity.content.body || ""; const signal = webhook.agent_run_activity.signal; // Check for stop signal if (signal === "stop") { await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "response", content: { type: "response", body: "Stopping as requested." }, }); return; } // Send initial thought (ephemeral - shows processing status) await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "thought", content: { type: "thought", body: "Processing your request..." }, }); // Process the request (implement your logic here) const response = await processUserRequest(userPrompt); // Send the response (creates a visible comment) await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "response", content: { type: "response", body: response }, }); } ``` \== Python {#python} ```python from plane import PlaneClient from plane.models.agent_runs import CreateAgentRunActivity def handle_webhook(webhook: dict, credentials: dict): """Handle incoming agent webhook.""" # Only handle agent_run_activity webhooks if webhook.get("type") != "agent_run_activity": return plane_client = PlaneClient( base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), access_token=credentials["bot_token"], ) agent_run_id = webhook["agent_run"]["id"] user_prompt = webhook["agent_run_activity"]["content"].get("body", "") signal = webhook["agent_run_activity"]["signal"] # Check for stop signal if signal == "stop": plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="response", content={"type": "response", "body": "Stopping as requested."}, ), ) return # Send initial thought (ephemeral - shows processing status) plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="thought", content={"type": "thought", "body": "Processing your request..."}, ), ) # Process the request (implement your logic here) response = process_user_request(user_prompt) # Send the response (creates a visible comment) plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="response", content={"type": "response", "body": response}, ), ) ``` ::: ## Next steps * Learn about [Best Practices](/dev-tools/agents/best-practices) for building responsive agents * Explore [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling --- --- url: 'https://developers.plane.so/dev-tools/agents/best-practices.html' description: >- Guidelines for building responsive, user-friendly Plane agents that provide a seamless experience. Covers error handling, response patterns, and user communication. --- # Best practices ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. ::: ## Overview Building a great agent experience requires thoughtful design around responsiveness, error handling, and user communication. This guide covers best practices to ensure your agent feels native to Plane and provides a seamless experience for users. ## Sending immediate thought activity When your agent receives a webhook, users are waiting for a response. The most important best practice is to **acknowledge the request immediately**. ### Why immediate acknowledgment matters * Users see that your agent is active and processing their request * Prevents the Agent Run from being marked as `stale` (5-minute timeout) * Builds trust that the agent received and understood the request * Provides visual feedback during potentially long processing times ### Implementation Send a `thought` activity within the first few seconds of receiving a webhook: :::tabs key:language \== TypeScript {#typescript} ```typescript import { PlaneClient } from "@makeplane/plane-node-sdk"; async function handleWebhook( webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }, ) { const planeClient = new PlaneClient({ baseUrl: process.env.PLANE_API_URL || "https://api.plane.so", accessToken: credentials.bot_token, }); const agentRunId = webhook.agent_run.id; // IMMEDIATELY acknowledge receipt await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "thought", content: { type: "thought", body: "Received your request. Analyzing..." }, }); // Now proceed with actual processing // This can take longer since user knows agent is working const result = await processRequest(webhook); // ... rest of the logic } ``` \== Python {#python} ```python from plane import PlaneClient from plane.models.agent_runs import CreateAgentRunActivity def handle_webhook(webhook: dict, credentials: dict): plane_client = PlaneClient( base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), access_token=credentials["bot_token"], ) agent_run_id = webhook["agent_run"]["id"] # IMMEDIATELY acknowledge receipt plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="thought", content={"type": "thought", "body": "Received your request. Analyzing..."}, ), ) # Now proceed with actual processing result = process_request(webhook) # ... rest of the logic ``` ::: ### Thought activity best practices * Keep thoughts concise but informative * Update thoughts as you progress through different stages * Use thoughts to explain what the agent is doing, not technical details **Good examples:** * "Analyzing your question about project timelines..." * "Searching for relevant work items..." * "Preparing response with the requested data..." **Avoid:** * "Initializing LLM context with temperature 0.7..." * "Executing database query SELECT \* FROM..." * Generic messages like "Working..." repeated multiple times ## Acknowledging important signals Signals communicate user intent beyond the message content. Your agent **must** handle the `stop` signal appropriately. ### The stop signal When a user wants to stop an agent run, Plane sends a `stop` signal with the activity. Your agent should: 1. **Recognize the signal immediately** 2. **Stop any ongoing processing** 3. **Send a confirmation response** :::tabs key:language \== TypeScript {#typescript} ```typescript async function handleWebhook( webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }, ) { const planeClient = new PlaneClient({ baseUrl: process.env.PLANE_API_URL || "https://api.plane.so", accessToken: credentials.bot_token, }); const signal = webhook.agent_run_activity.signal; const agentRunId = webhook.agent_run.id; // ALWAYS check for stop signal first if (signal === "stop") { // Cancel any ongoing work cancelOngoingTasks(agentRunId); // Acknowledge the stop await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "response", content: { type: "response", body: "Understood. I've stopped processing your previous request.", }, }); return; // Exit early } // Continue with normal processing... } ``` \== Python {#python} ```python def handle_webhook(webhook: dict, credentials: dict): plane_client = PlaneClient( base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), access_token=credentials["bot_token"], ) signal = webhook["agent_run_activity"]["signal"] agent_run_id = webhook["agent_run"]["id"] # ALWAYS check for stop signal first if signal == "stop": # Cancel any ongoing work cancel_ongoing_tasks(agent_run_id) # Acknowledge the stop plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="response", content={ "type": "response", "body": "Understood. I've stopped processing your previous request.", }, ), ) return # Exit early # Continue with normal processing... ``` ::: ### Signal considerations | Signal | How to Handle | | ---------- | ----------------------------------------- | | `continue` | Default behavior, proceed with processing | | `stop` | Immediately halt and confirm | ## Progress communication For long-running tasks, keep users informed with progress updates. ### Multi-step operations When your agent performs multiple steps, send thought activities for each: ```typescript // Step 1: Acknowledge await createThought("Understanding your request..."); // Step 2: First action await createAction("searchDocuments", { query: userQuery }); const searchResults = await searchDocuments(userQuery); // Step 3: Processing await createThought("Found relevant information. Analyzing..."); // Step 4: Additional work await createAction("generateSummary", { data: searchResults }); const summary = await generateSummary(searchResults); // Step 5: Final response await createResponse(`Here's what I found: ${summary}`); ``` ### Avoiding information overload While progress updates are important, too many can be overwhelming: * **Don't** send a thought for every internal function call * **Do** send thoughts for user-meaningful milestones * **Don't** expose technical implementation details * **Do** explain what value is being created for the user ## Error handling Graceful error handling is crucial for a good user experience. ### Always catch and report errors ```typescript async function handleWebhook( webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }, ) { const planeClient = new PlaneClient({ baseUrl: process.env.PLANE_API_URL || "https://api.plane.so", accessToken: credentials.bot_token, }); const agentRunId = webhook.agent_run.id; try { await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "thought", content: { type: "thought", body: "Processing your request..." }, }); // Your logic here... const result = await processRequest(webhook); await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "response", content: { type: "response", body: result }, }); } catch (error) { // ALWAYS inform the user about errors await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "error", content: { type: "error", body: getUserFriendlyErrorMessage(error), }, }); } } function getUserFriendlyErrorMessage(error: Error): string { // Map technical errors to user-friendly messages if (error.message.includes("rate limit")) { return "I'm receiving too many requests right now. Please try again in a few minutes."; } if (error.message.includes("timeout")) { return "The operation took too long. Please try a simpler request or try again later."; } // Generic fallback return "I encountered an unexpected error. Please try again or contact support if the issue persists."; } ``` ### Error message guidelines **Do:** * Use clear, non-technical language * Suggest next steps when possible * Be honest about what went wrong (at a high level) **Don't:** * Expose stack traces or technical details * Blame the user for errors * Leave users without any feedback ## Handling conversation context For multi-turn conversations, maintain context from previous activities. ### Fetching previous activities ```typescript // Get all activities for context const activities = await planeClient.agentRuns.activities.list( credentials.workspace_slug, agentRunId, ); // Build conversation history const history = activities.results .filter((a) => a.type === "prompt" || a.type === "response") .map((a) => ({ role: a.type === "prompt" ? "user" : "assistant", content: a.content.body, })); // Use history in your LLM call or logic const response = await processWithContext(newPrompt, history); ``` ### Context best practices * Retrieve relevant history, not every single activity * Filter to meaningful exchanges (prompts and responses) * Consider summarizing long histories to save tokens/processing * Don't assume infinite context availability ## Rate limiting and timeouts Be mindful of Plane's API limits and your own processing time. ### Stale run prevention Agent Runs are marked as `stale` after 5 minutes of inactivity. For long operations: ```typescript async function longRunningTask(agentRunId: string) { const HEARTBEAT_INTERVAL = 60000; // 1 minute const heartbeat = setInterval(async () => { await createThought("Still working on your request..."); }, HEARTBEAT_INTERVAL); try { const result = await performLongOperation(); return result; } finally { clearInterval(heartbeat); } } ``` ### Webhook response time * Return HTTP 200 from your webhook handler quickly (within seconds) * Process the actual agent logic asynchronously * Don't block the webhook response waiting for LLM calls ```typescript // Good: Respond immediately, process async app.post("/webhook", async (req, res) => { res.status(200).json({ received: true }); // Process in background processWebhookAsync(req.body).catch(console.error); }); ``` ## Summary checklist **Responsiveness** * Send thought within seconds of webhook * Return webhook response quickly * Send heartbeats for long operations **Signal handling** * Always check for `stop` signal first * Handle all signal types appropriately * Confirm when stopping **Error handling** * Wrap processing in try/catch * Always send error activity on failure * Use friendly error messages **User experience** * Progress updates for long tasks * Clear, non-technical communication * Maintain conversation context ## Next steps * Learn about [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling * Review the [Building an Agent](/dev-tools/agents/building-an-agent) guide for implementation details --- --- url: 'https://developers.plane.so/dev-tools/agents/signals-content-payload.html' description: >- Detailed reference for activity signals and content payload structures in Plane agents. Includes signal types, payload formats, and content examples. --- # Signals and content payload ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. ::: ## Overview Agent activities consist of two key components: 1. **Content** — The message or action being communicated 2. **Signal** — Metadata indicating how the activity should be interpreted Understanding these components is essential for building agents that communicate effectively with users. ## Signals Signals are metadata that modify how an activity should be interpreted or handled. They provide additional context about the sender's intent—guiding how the activity should be processed or responded to. ### Available signals | Signal | Description | Use Case | | -------------- | -------------------------------------- | ---------------------------------------- | | `continue` | Default signal, indicates normal flow | Standard responses, ongoing conversation | | `stop` | User requested to stop the agent | Cancellation, abort operations | | `auth_request` | Agent needs external authentication | OAuth flows, API key collection | | `select` | Agent presenting options for selection | Multiple choice questions | ### Signal: `continue` The default signal for most activities. Indicates normal conversation flow where the agent can continue processing. ```json { "type": "response", "content": { "type": "response", "body": "Here's the information you requested." }, "signal": "continue" } ``` ### Signal: `stop` Sent by Plane when a user requests to stop the agent. Your agent should: 1. Immediately halt any ongoing processing 2. Clean up resources if needed 3. Send a confirmation response **Incoming webhook with stop signal:** ```json { "action": "prompted", "agent_run_activity": { "type": "prompt", "content": { "type": "prompt", "body": "Stop" }, "signal": "stop" } } ``` **Handling the stop signal:** ```typescript if (webhook.agent_run_activity.signal === "stop") { // Cancel ongoing work await cancelAllPendingTasks(); // Acknowledge the stop - this transitions run to "stopped" status await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "response", content: { type: "response", body: "I've stopped working on your request.", }, }); return; } ``` ### Signal: `auth_request` Used when your agent needs the user to authenticate with an external service. Requires a URL in the `signal_metadata`. **Creating an auth request:** ```typescript await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "elicitation", content: { type: "elicitation", body: "I need access to your GitHub account to proceed. Please authenticate using the link below.", }, signal: "auth_request", signal_metadata: { url: "https://your-agent.com/auth/github?session=abc123", }, }); ``` **Requirements:** * The URL must start with `https://` * The URL should be a secure endpoint on your agent's server * After authentication, redirect the user back or notify completion ### Signal: `select` Used when presenting options for the user to choose from. Useful for disambiguation or multi-choice scenarios. ```typescript await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "elicitation", content: { type: "elicitation", body: "Which project would you like me to search?\n\n1. Frontend App\n2. Backend API\n3. Mobile App", }, signal: "select", signal_metadata: { options: [ { id: "frontend", label: "Frontend App" }, { id: "backend", label: "Backend API" }, { id: "mobile", label: "Mobile App" }, ], }, }); ``` ## Content payload types The `content` field contains the actual message or action. Its structure varies based on the activity type. ### Type: `thought` Internal reasoning or progress updates from the agent. Automatically marked as ephemeral (won't create a comment). **Structure:** ```typescript interface ThoughtContent { type: "thought"; body: string; // The thought message } ``` **Example:** ```json { "type": "thought", "body": "The user is asking about deployment status. I'll check the CI/CD pipeline." } ``` **Creating a thought activity:** :::tabs key:language \== TypeScript {#typescript} ```typescript await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "thought", content: { type: "thought", body: "Analyzing the codebase for potential issues...", }, }); ``` \== Python {#python} ```python from plane.models.agent_runs import CreateAgentRunActivity plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="thought", content={ "type": "thought", "body": "Analyzing the codebase for potential issues...", }, ), ) ``` ::: **Best practices for thoughts:** * Keep them concise and user-meaningful * Use to show progress, not internal implementation * Update as you move through stages of processing ### Type: `action` Describes a tool invocation or external action. Automatically marked as ephemeral. **Structure:** ```typescript interface ActionContent { type: "action"; action: string; // Name of the tool/action parameters: { // Key-value pairs of parameters [key: string]: string; }; } ``` **Example - Starting an action:** ```json { "type": "action", "action": "searchDatabase", "parameters": { "query": "bug reports", "status": "open" } } ``` **Example - Action with result:** ```json { "type": "action", "action": "searchDatabase", "parameters": { "query": "bug reports", "status": "open", "result": "Found 12 matching work items" } } ``` **Creating action activities:** :::tabs key:language \== TypeScript {#typescript} ```typescript // Before executing the action await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "action", content: { type: "action", action: "fetchWeather", parameters: { location: "San Francisco", }, }, }); // Execute the actual action const weatherData = await fetchWeather("San Francisco"); // After execution, report the result await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { type: "action", content: { type: "action", action: "fetchWeather", parameters: { location: "San Francisco", result: `Temperature: ${weatherData.temp}°F, Conditions: ${weatherData.conditions}`, }, }, content_metadata: { result: weatherData, // Store full result in metadata }, }); ``` \== Python {#python} ```python from plane.models.agent_runs import CreateAgentRunActivity # Before executing the action plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="action", content={ "type": "action", "action": "fetchWeather", "parameters": {"location": "San Francisco"}, }, ), ) # Execute the actual action weather_data = fetch_weather("San Francisco") # After execution, report the result plane_client.agent_runs.activities.create( workspace_slug=credentials["workspace_slug"], run_id=agent_run_id, data=CreateAgentRunActivity( type="action", content={ "type": "action", "action": "fetchWeather", "parameters": { "location": "San Francisco", "result": f"Temperature: {weather_data['temp']}°F, Conditions: {weather_data['conditions']}", }, }, content_metadata={"result": weather_data}, ), ) ``` ::: **Parameter requirements:** * All parameter keys must be strings * All parameter values must be strings * Use `content_metadata` to store complex/structured data ### Type: `response` A final response to the user. Creates a comment reply visible to users. **Structure:** ```typescript interface ResponseContent { type: "response"; body: string; // The response message (supports Markdown) } ``` **Example:** ```json { "type": "response", "body": "Based on my analysis, here are the top 3 issues affecting your sprint:\n\n1. **AUTH-123**: Login timeout affecting 15% of users\n2. **API-456**: Rate limiting too aggressive\n3. **UI-789**: Dashboard loading slowly\n\nWould you like me to provide more details on any of these?" } ``` **Creating a response:** :::tabs key:language \== TypeScript {#typescript} ```typescript await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "response", content: { type: "response", body: "Here's the weather in San Francisco:\n\n**68°F** - Partly Cloudy\n\nExpect mild conditions throughout the day.", }, signal: "continue", }); ``` \== Python {#python} ```python from plane.models.agent_runs import CreateAgentRunActivity plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="response", content={ "type": "response", "body": "Here's the weather in San Francisco:\n\n**68°F** - Partly Cloudy\n\nExpect mild conditions throughout the day.", }, signal="continue", ), ) ``` ::: **Response best practices:** * Use Markdown for formatting * Be clear and concise * Include relevant context * End with a call-to-action if appropriate ### Type: `elicitation` Requests clarification or input from the user. Creates a comment and sets the Agent Run status to `awaiting`. **Structure:** ```typescript interface ElicitationContent { type: "elicitation"; body: string; // The question or request (supports Markdown) } ``` **Example:** ```json { "type": "elicitation", "body": "I found multiple projects matching your query. Which one would you like me to focus on?\n\n1. Project Alpha (12 open work items)\n2. Project Beta (8 open work items)\n3. Project Gamma (23 open work items)" } ``` **Creating an elicitation:** :::tabs key:language \== TypeScript {#typescript} ```typescript await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "elicitation", content: { type: "elicitation", body: "To generate the report, I need a few details:\n\n- What date range should I cover?\n- Should I include completed work items or only open ones?", }, }); ``` \== Python {#python} ```python from plane.models.agent_runs import CreateAgentRunActivity plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="elicitation", content={ "type": "elicitation", "body": "To generate the report, I need a few details:\n\n- What date range should I cover?\n- Should I include completed work items or only open ones?", }, ), ) ``` ::: **Elicitation best practices:** * Ask specific, answerable questions * Provide options when possible * Don't ask too many questions at once * Consider using `select` signal for multiple choice ### Type: `error` Reports an error or failure. Creates a comment and sets the Agent Run status to `failed`. **Structure:** ```typescript interface ErrorContent { type: "error"; body: string; // The error message (supports Markdown) } ``` **Example:** ```json { "type": "error", "body": "I couldn't complete your request due to a connection issue with the external service. Please try again in a few minutes." } ``` **Creating an error activity:** :::tabs key:language \== TypeScript {#typescript} ```typescript await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "error", content: { type: "error", body: "I was unable to access the GitHub repository. Please ensure the integration is properly configured.", }, signal_metadata: { error_code: "GITHUB_ACCESS_DENIED", retryable: true, }, }); ``` \== Python {#python} ```python from plane.models.agent_runs import CreateAgentRunActivity plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="error", content={ "type": "error", "body": "I was unable to access the GitHub repository. Please ensure the integration is properly configured.", }, signal_metadata={ "error_code": "GITHUB_ACCESS_DENIED", "retryable": True, }, ), ) ``` ::: **Error best practices:** * Use friendly, non-technical language * Suggest next steps when possible * Store detailed error info in `signal_metadata` * Don't expose stack traces or sensitive information ### Type: `prompt` This type is **user-generated only**. Your agent cannot create prompt activities—they're created by Plane when a user sends a message. **Structure:** ```typescript interface PromptContent { type: "prompt"; body: string; // The user's message } ``` **Example received in webhook:** ```json { "type": "prompt", "body": "Can you check the status of our deployment pipeline?" } ``` ## Ephemeral activities Ephemeral activities are temporary and won't create comment replies. They're useful for showing agent progress without cluttering the conversation thread. ### Automatically ephemeral types The following activity types are automatically marked as ephemeral: * `thought` * `action` * `error` ### Ephemeral behavior * Ephemeral activities appear temporarily in the Agent UI * They're replaced when the next activity arrives * They don't create permanent comment replies * Useful for real-time progress updates ### Visual example ``` User: @WeatherBot What's the weather in Tokyo? [Ephemeral - disappears when next activity arrives] Analyzing your request... [Ephemeral - disappears when next activity arrives] getCoordinates("Tokyo") [Ephemeral - disappears when next activity arrives] getWeather(35.6762, 139.6503) → 72°F, Clear [Permanent - stays as comment] The weather in Tokyo is currently 72°F with clear skies. ``` ## Content metadata Use `content_metadata` to store additional structured data about an activity: :::tabs key:language \== TypeScript {#typescript} ```typescript await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "action", content: { type: "action", action: "analyzeCode", parameters: { file: "src/index.ts", result: "Found 3 potential issues", }, }, content_metadata: { analysis_results: { issues: [ { line: 42, severity: "warning", message: "Unused variable" }, { line: 78, severity: "error", message: "Type mismatch" }, { line: 156, severity: "info", message: "Consider refactoring" }, ], processing_time_ms: 1250, }, }, }); ``` \== Python {#python} ```python from plane.models.agent_runs import CreateAgentRunActivity plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="action", content={ "type": "action", "action": "analyzeCode", "parameters": { "file": "src/index.ts", "result": "Found 3 potential issues", }, }, content_metadata={ "analysis_results": { "issues": [ {"line": 42, "severity": "warning", "message": "Unused variable"}, {"line": 78, "severity": "error", "message": "Type mismatch"}, {"line": 156, "severity": "info", "message": "Consider refactoring"}, ], "processing_time_ms": 1250, }, }, ), ) ``` ::: ## Signal metadata Use `signal_metadata` to provide additional context for signals: ```typescript // Auth request with URL { signal: "auth_request", signal_metadata: { url: "https://your-agent.com/auth/connect?session=xyz", provider: "github", scopes: ["repo", "read:user"], } } // Select with options { signal: "select", signal_metadata: { options: [ { id: "opt1", label: "Option 1", description: "First choice" }, { id: "opt2", label: "Option 2", description: "Second choice" }, ], allow_multiple: false, } } // Error with details { signal: "continue", signal_metadata: { error_code: "RATE_LIMIT_EXCEEDED", retry_after: 60, retryable: true, } } ``` ## Complete activity creation reference Here's a comprehensive example showing all activity types: :::tabs key:language \== TypeScript {#typescript} ```typescript import { PlaneClient } from "@makeplane/plane-node-sdk"; const planeClient = new PlaneClient({ baseUrl: process.env.PLANE_API_URL || "https://api.plane.so", accessToken: botToken, }); // 1. Thought - Show reasoning (ephemeral) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "thought", content: { type: "thought", body: "Analyzing the request..." }, }); // 2. Action - Tool invocation (ephemeral) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "action", content: { type: "action", action: "searchWorkItems", parameters: { query: "bug", status: "open" }, }, }); // 3. Response - Final answer (creates comment) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "response", content: { type: "response", body: "Found 5 open bugs." }, signal: "continue", }); // 4. Elicitation - Ask for input (creates comment, awaits response) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "elicitation", content: { type: "elicitation", body: "Which bug should I prioritize?" }, signal: "select", signal_metadata: { options: [ { id: "bug-1", label: "AUTH-123: Login timeout" }, { id: "bug-2", label: "API-456: Rate limiting" }, ], }, }); // 5. Error - Report failure (creates comment) await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { type: "error", content: { type: "error", body: "Unable to access the database." }, signal_metadata: { error_code: "DB_CONNECTION_FAILED" }, }); ``` \== Python {#python} ```python from plane import PlaneClient from plane.models.agent_runs import CreateAgentRunActivity plane_client = PlaneClient( base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), access_token=bot_token, ) # 1. Thought - Show reasoning (ephemeral) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="thought", content={"type": "thought", "body": "Analyzing the request..."}, ), ) # 2. Action - Tool invocation (ephemeral) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="action", content={ "type": "action", "action": "searchWorkItems", "parameters": {"query": "bug", "status": "open"}, }, ), ) # 3. Response - Final answer (creates comment) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="response", content={"type": "response", "body": "Found 5 open bugs."}, signal="continue", ), ) # 4. Elicitation - Ask for input (creates comment, awaits response) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="elicitation", content={"type": "elicitation", "body": "Which bug should I prioritize?"}, signal="select", signal_metadata={ "options": [ {"id": "bug-1", "label": "AUTH-123: Login timeout"}, {"id": "bug-2", "label": "API-456: Rate limiting"}, ], }, ), ) # 5. Error - Report failure (creates comment) plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, run_id=agent_run_id, data=CreateAgentRunActivity( type="error", content={"type": "error", "body": "Unable to access the database."}, signal_metadata={"error_code": "DB_CONNECTION_FAILED"}, ), ) ``` ::: ## Next steps * Review [Best Practices](/dev-tools/agents/best-practices) for building responsive agents * See [Building an Agent](/dev-tools/agents/building-an-agent) for implementation examples * Check the [Overview](/dev-tools/agents/overview) for Agent Run lifecycle details --- --- url: 'https://developers.plane.so/dev-tools/build-plane-app/overview.html' description: >- Build and integrate an app with Plane using OAuth 2.0 authentication. Covers bot tokens, user tokens, webhooks, and API access for custom integrations. --- # Build a Plane app ::: info Plane apps are currently in **Beta**. Please send any feedback to support@plane.so. ::: ## Overview Plane uses OAuth 2.0 to allow applications to access workspace data on behalf of users or as an autonomous bot. This comprehensive guide covers everything you need to build, integrate, and deploy apps that extend Plane's functionality. ## What you can build Plane apps enable you to: * **AI Agents** - Create intelligent agents that respond to @mentions in work item comments * **Workflow Automation** - Build bots that automate repetitive tasks across your workspace * **Integrations** - Connect Plane with external tools and services * **Custom Dashboards** - Build analytics and reporting tools using Plane's data * **Webhook Handlers** - React to events in real-time as they happen in Plane ## Key concepts ### OAuth 2.0 flows Plane supports two authentication flows: * **Bot Token Flow** (Client Credentials) - For autonomous apps, agents, and webhooks that act independently * **User Token Flow** (Authorization Code) - For apps that need to act on behalf of specific users Most integrations should use the **Bot Token flow**. See [Choose Your Flow](/dev-tools/build-plane-app/choose-token-flow) for detailed implementation guides. ### App components A complete Plane app typically includes: 1. **OAuth Application** - Registered in Plane with Client ID and Secret 2. **Setup URL** - Entry point where users begin the installation process 3. **Redirect URI** - Callback endpoint that receives authorization codes 4. **Webhook URL** - Endpoint for receiving real-time event notifications 5. **API Integration** - Code that interacts with Plane's REST API ## Getting started Follow these steps to build your first Plane app: ### 1. Create an OAuth application Register your app in Plane to get credentials: * Navigate to **Workspace Settings** → **Integrations** * Configure your app's URLs and permissions * Store your **Client ID** and **Client Secret** securely [Learn more →](/dev-tools/build-plane-app/create-oauth-application) ### 2. Choose your authentication flow Decide between Bot Token or User Token based on your use case: * **Bot Token** - For agents, webhooks, and automation * **User Token** - For user-specific actions and permissions [Learn more →](/dev-tools/build-plane-app/choose-token-flow) ### 3. Implement OAuth Set up the OAuth flow to obtain access tokens: * Redirect users to Plane's consent screen * Handle the callback with authorization code * Exchange code for access tokens * Store tokens securely for API calls [Learn more →](/dev-tools/build-plane-app/choose-token-flow) ### 4. Handle webhooks Set up webhook handlers to receive real-time events: * Verify webhook signatures for security * Process events like work item updates, comments, and more * Respond to events with automated actions [Learn more →](/dev-tools/build-plane-app/webhooks) ## Development tools ::: tip Local Development For local development, use [ngrok](https://ngrok.com) to expose your server: ```bash ngrok http 3000 ``` Use the generated URL (e.g., `https://abc123.ngrok.io`) for your Setup URL, Redirect URI, and Webhook URL. Free ngrok URLs change on restart. Update your app settings when the URL changes. ::: ### Official SDKs Speed up development with official SDKs for Node.js and Python: * OAuth helpers for token management * Typed API clients for all endpoints * Built-in error handling and retries [Learn more →](/dev-tools/build-plane-app/sdks) ### Complete examples See full working implementations: * TypeScript (Express) example * Python (Flask) example * OAuth flow, webhooks, and API integration [Learn more →](/dev-tools/build-plane-app/examples) ## Quick links * [API Reference](/api-reference/introduction) - Explore all available endpoints * [Build an Agent](/dev-tools/agents/overview) - Create AI agents for Plane * [Webhook Events](/dev-tools/intro-webhooks) - All webhook event types --- --- url: >- https://developers.plane.so/dev-tools/build-plane-app/create-oauth-application.html description: >- Register your Plane OAuth application to get Client ID and Secret. Configure setup URL, redirect URI, and webhook endpoints for your integration. --- # Create an OAuth Application 1. Navigate to **Workspace Settings** → **Integrations**. ```text https://app.plane.so//settings/integrations/ ``` 2. Click **Build your own**. 3. Fill in the required details: | Field | Description | | ---------------- | ------------------------------------------------------------------------------------------------------ | | **App Name** | Display name shown to users | | **Setup URL** | Entry point when users install your app. Your app redirects users to Plane's consent screen from here. | | **Redirect URI** | Callback URL where Plane sends users after they approve access, along with the authorization code. | | **Webhook URL** | Endpoint for receiving event notifications | 4. For agents that respond to @mentions, enable **"Enable App Mentions"**. 5. Save and store your **Client ID** and **Client Secret** securely. 6. Select the scopes you need for your app from the **Scopes & Permissions** section. See [OAuth Scopes](/dev-tools/build-plane-app/oauth-scopes) for more information on the available scopes. ::: warning Never expose your Client Secret in client-side code or commit it to version control. ::: --- --- url: 'https://developers.plane.so/dev-tools/build-plane-app/choose-token-flow.html' description: >- Decide between Bot Token and User Token flows for your Plane app. Compare client credentials and authorization code grant types for different use cases. --- # Choose token flow Plane supports two OAuth flows: | Flow | Use When | Token Type | | ----------------------------------- | ---------------------------------------------- | -------------- | | **Bot Token** (Client Credentials) | Agents, webhooks, automation, background tasks | `bot_token` | | **User Token** (Authorization Code) | Actions on behalf of a specific user | `access_token` | ::: info Most integrations should use the **Bot Token flow**. Use User Token only when you need to perform actions as a specific user. ::: *** ## Bot token flow Use this flow for agents, webhook handlers, and automation that acts autonomously. ```mermaid sequenceDiagram participant User participant Plane participant YourApp User->>YourApp: Clicks "Install" YourApp->>Plane: Redirects to consent screen Plane->>User: Shows consent screen User->>Plane: Approves Plane->>YourApp: Redirects with app_installation_id YourApp->>Plane: POST /auth/o/token/ (client_credentials) Plane->>YourApp: Returns bot_token YourApp->>YourApp: Store credentials ``` ### 1. Redirect to authorization When a user clicks "Install", redirect them to Plane's consent screen: ``` GET https://api.plane.so/auth/o/authorize-app/ ?client_id=YOUR_CLIENT_ID &response_type=code &redirect_uri=https://your-app.com/callback &scope=scopeA scopeB scopeC ``` ### 2. Handle the callback After the user approves, Plane redirects to your Redirect URI with: | Parameter | Description | | --------------------- | ----------------------------------------- | | `app_installation_id` | Unique identifier for this installation | | `code` | Authorization code (not used in bot flow) | ### 3. Exchange for bot token ``` POST https://api.plane.so/auth/o/token/ Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(client_id:client_secret) grant_type=client_credentials &app_installation_id=APP_INSTALLATION_ID &scope=scopeA scopeB scopeC ``` **Response:** ```json { "access_token": "pln_bot_xxxxxxxxxxxx", "token_type": "Bearer", "expires_in": 86400, "scope": "scopeA scopeB scopeC" } ``` ### 4. Get workspace details ``` GET https://api.plane.so/auth/o/app-installation/?id=APP_INSTALLATION_ID Authorization: Bearer YOUR_BOT_TOKEN ``` **Response:** ```json [ { "id": "installation-uuid", "workspace": "workspace-uuid", "workspace_detail": { "name": "My Workspace", "slug": "my-workspace" }, "app_bot": "bot-user-uuid", "status": "installed" } ] ``` Store the `workspace_detail.slug` for API calls and `app_installation_id` for token refresh. ### 5. Refresh bot token Bot tokens expire. Request a new one using the stored `app_installation_id`: ``` POST https://api.plane.so/auth/o/token/ Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(client_id:client_secret) grant_type=client_credentials &app_installation_id=APP_INSTALLATION_ID &scope=scopeA scopeB scopeC ``` *** ## User token flow Use this flow when your app needs to act on behalf of a specific user. ```mermaid sequenceDiagram participant User participant Plane participant YourApp User->>YourApp: Clicks "Connect" YourApp->>Plane: Redirects to consent screen Plane->>User: Shows consent screen User->>Plane: Approves Plane->>YourApp: Redirects with code YourApp->>Plane: POST /auth/o/token/ (authorization_code) Plane->>YourApp: Returns access_token + refresh_token YourApp->>YourApp: Store tokens for user ``` ### 1. Redirect to authorization ``` GET https://api.plane.so/auth/o/authorize-app/ ?client_id=YOUR_CLIENT_ID &response_type=code &redirect_uri=https://your-app.com/callback &state=RANDOM_STATE_VALUE &scope=scopeA scopeB scopeC ``` ::: info Include a random `state` parameter to prevent CSRF attacks. Verify it matches when handling the callback. ::: ### 2. Handle the callback After approval, Plane redirects to your Redirect URI with: | Parameter | Description | | --------- | ------------------------------------------ | | `code` | Authorization code to exchange for tokens | | `state` | Your state parameter (verify this matches) | ### 3. Exchange code for tokens ``` POST https://api.plane.so/auth/o/token/ Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=AUTHORIZATION_CODE &client_id=YOUR_CLIENT_ID &client_secret=YOUR_CLIENT_SECRET &redirect_uri=https://your-app.com/callback ``` **Response:** ```json { "access_token": "pln_xxxxxxxxxxxx", "refresh_token": "pln_refresh_xxxxxxxxxxxx", "token_type": "Bearer", "expires_in": 86400, "scope": "scopeA scopeB scopeC" } ``` ### 4. Refresh user token ``` POST https://api.plane.so/auth/o/token/ Content-Type: application/x-www-form-urlencoded grant_type=refresh_token &refresh_token=YOUR_REFRESH_TOKEN &client_id=YOUR_CLIENT_ID &client_secret=YOUR_CLIENT_SECRET ``` *** ## Making API requests Include the token in the `Authorization` header: ``` GET https://api.plane.so/api/v1/workspaces/{workspace_slug}/projects/ Authorization: Bearer YOUR_TOKEN ``` See the [API Reference](/api-reference/introduction) for available endpoints. --- --- url: 'https://developers.plane.so/dev-tools/build-plane-app/webhooks.html' description: >- Receive and verify webhooks from Plane in your application. Learn about webhook headers, signature verification, event types, and payload handling. --- # Handling webhooks When events occur in Plane, webhooks are sent to your Webhook URL. ## Webhook headers | Header | Description | | ------------------- | ------------------------------------------- | | `X-Plane-Delivery` | Unique delivery ID | | `X-Plane-Event` | Event type (e.g., `issue`, `issue_comment`) | | `X-Plane-Signature` | HMAC-SHA256 signature for verification | ## Verify signature Always verify the `X-Plane-Signature` header: :::tabs key:language \== Python {#python} ```python import hmac import hashlib def verify_signature(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` \== TypeScript {#typescript} ```typescript import crypto from "crypto"; function verifySignature(payload: string, signature: string, secret: string): boolean { const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex"); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } ``` ::: ## Webhook payload ```json { "event": "issue", "action": "created", "webhook_id": "webhook-uuid", "workspace_id": "workspace-uuid", "data": { ... }, "activity": { "actor": { "id": "user-uuid", "display_name": "John Doe" } } } ``` See [Webhook Events](/dev-tools/intro-webhooks) for all event types. --- --- url: 'https://developers.plane.so/dev-tools/build-plane-app/oauth-scopes.html' description: >- Complete reference of all OAuth scopes available when building a Plane app. Includes read and write permissions for projects, work items, cycles, modules, and more. --- # OAuth scopes This document lists all OAuth scopes available when building a Plane app. Request only the scopes your app needs. ## Project scopes | Scope | Description | | ------------------------------------------- | -------------------------------------------- | | `projects:read` | Read projects | | `projects:write` | Create and update projects | | `projects.features:read` | Read project features | | `projects.features:write` | Create and update project features | | `projects.members:read` | Read project members | | `projects.members:write` | Manage project members | | `projects.states:read` | Read project states | | `projects.states:write` | Create and update project states | | `projects.labels:read` | Read project labels | | `projects.labels:write` | Create and update project labels | | `projects.intakes:read` | Read project intakes | | `projects.intakes:write` | Create and update project intakes | | `projects.epics:read` | Read project epics | | `projects.epics:write` | Create and update project epics | | `projects.cycles:read` | Read project cycles | | `projects.cycles:write` | Create and update project cycles | | `projects.pages:read` | Read project pages | | `projects.pages:write` | Create and update project pages | | `projects.modules:read` | Read project modules | | `projects.modules:write` | Create and update project modules | | `projects.work_items:read` | Read project work items | | `projects.work_items:write` | Create and update project work items | | `projects.work_items.comments:read` | Read work item comments | | `projects.work_items.comments:write` | Create and update work item comments | | `projects.work_items.attachments:read` | Read work item attachments | | `projects.work_items.attachments:write` | Create and update work item attachments | | `projects.work_items.links:read` | Read work item links | | `projects.work_items.links:write` | Create and update work item links | | `projects.work_items.relations:read` | Read work item relations | | `projects.work_items.relations:write` | Create and update work item relations | | `projects.work_items.activities:read` | Read work item activities | | `projects.work_items.activities:write` | Create and update work item activities | | `projects.work_items.worklogs:read` | Read work item worklogs | | `projects.work_items.worklogs:write` | Create and update work item worklogs | | `projects.work_item_types:read` | Read work item types | | `projects.work_item_types:write` | Create and update work item types | | `projects.work_item_properties:read` | Read work item properties | | `projects.work_item_properties:write` | Create and update work item properties | | `projects.work_item_property_options:read` | Read work item property options | | `projects.work_item_property_options:write` | Create and update work item property options | | `projects.work_item_property_values:read` | Read work item property values | | `projects.work_item_property_values:write` | Create and update work item property values | | `projects.milestones:read` | Read project milestones | | `projects.milestones:write` | Create and update project milestones | ## Wiki scopes | Scope | Description | | ------------------ | ---------------------------- | | `wiki.pages:read` | Read wiki pages | | `wiki.pages:write` | Create and update wiki pages | ## Customer scopes | Scope | Description | | --------------------------------- | ------------------------------------------ | | `customers:read` | Read customers | | `customers:write` | Create and update customers | | `customers.requests:read` | Read customer requests | | `customers.requests:write` | Create and update customer requests | | `customers.properties:read` | Read customer properties | | `customers.properties:write` | Create and update customer properties | | `customers.property_values:read` | Read customer property values | | `customers.property_values:write` | Create and update customer property values | | `customers.work_items:read` | Read customer work items | | `customers.work_items:write` | Create and update customer work items | ## Initiatives scopes | Scope | Description | | ---------------------------- | ------------------------------------- | | `initiatives:read` | Read initiatives | | `initiatives:write` | Create and update initiatives | | `initiatives.projects:read` | Read initiative projects | | `initiatives.projects:write` | Create and update initiative projects | | `initiatives.epics:read` | Read initiative epics | | `initiatives.epics:write` | Create and update initiative epics | | `initiatives.labels:read` | Read initiative labels | | `initiatives.labels:write` | Create and update initiative labels | ## Workspace scopes | Scope | Description | | --------------------------- | ------------------------------------ | | `workspaces.members:read` | Read workspace members | | `workspaces.members:write` | Manage workspace members | | `workspaces.features:read` | Read workspace features | | `workspaces.features:write` | Create and update workspace features | ## Stickies scopes | Scope | Description | | ---------------- | -------------------------- | | `stickies:read` | Read stickies | | `stickies:write` | Create and update stickies | ## Teamspaces scopes | Scope | Description | | --------------------------- | ------------------------------------ | | `teamspaces:read` | Read teamspaces | | `teamspaces:write` | Create and update teamspaces | | `teamspaces.projects:read` | Read teamspace projects | | `teamspaces.projects:write` | Create and update teamspace projects | | `teamspaces.members:read` | Read teamspace members | | `teamspaces.members:write` | Create and update teamspace members | ## Profile scopes | Scope | Description | | -------------- | ----------------- | | `profile:read` | Read user profile | ## Assets scopes | Scope | Description | | -------------- | ------------------------ | | `assets:read` | Read assets | | `assets:write` | Create and update assets | ## Agent Run scopes | Scope | Description | | ----------------------------- | -------------------------------------- | | `agents.runs:read` | Read agent runs | | `agents.runs:write` | Create and update agent runs | | `agents.run_activities:read` | Read agent run activities | | `agents.run_activities:write` | Create and update agent run activities | --- --- url: 'https://developers.plane.so/dev-tools/build-plane-app/sdks.html' description: >- Official Plane SDKs for Node.js and Python. Get typed API clients and OAuth helpers to build Plane integrations faster. --- # SDKs Official SDKs provide OAuth helpers and typed API clients: | Language | Package | | -------- | ------------------------------------------------------------------------------------ | | Node.js | [@makeplane/plane-node-sdk](https://www.npmjs.com/package/@makeplane/plane-node-sdk) | | Python | [plane-sdk](https://pypi.org/project/plane-sdk/) | ```bash npm install @makeplane/plane-node-sdk # or pip install plane-sdk ``` #### OAuth helper methods ::: code-group ```typescript [Node.js] import { OAuthClient } from "@makeplane/plane-node-sdk"; const oauth = new OAuthClient({ clientId: "your_client_id", clientSecret: "your_client_secret", redirectUri: "https://your-app.com/callback", }); // Generate authorization URL const authUrl = oauth.getAuthorizationUrl("code", "state"); // Exchange for bot token const token = await oauth.getBotToken(appInstallationId); // Exchange code for user token const userToken = await oauth.exchangeCodeForToken(code); // Refresh user token const newToken = await oauth.getRefreshToken(refreshToken); ``` ```python [Python] from plane.client import OAuthClient oauth = OAuthClient( client_id="your_client_id", client_secret="your_client_secret", ) # Generate authorization URL auth_url = oauth.get_authorization_url(redirect_uri="...", state="state") # Exchange for bot token token = oauth.get_client_credentials_token(app_installation_id=app_installation_id) # Exchange code for user token user_token = oauth.exchange_code(code=code, redirect_uri=redirect_uri) # Refresh user token new_token = oauth.refresh_token(refresh_token) ``` ::: --- --- url: 'https://developers.plane.so/dev-tools/build-plane-app/examples.html' description: >- Full code examples for building Plane OAuth apps with Node.js (Express) and Python (Flask). Includes bot token flow, user token flow, and webhook handling. --- # Complete examples ::: code-group ```typescript [TypeScript (Express)] import express from "express"; import axios from "axios"; import crypto from "crypto"; const app = express(); const CLIENT_ID = process.env.PLANE_CLIENT_ID!; const CLIENT_SECRET = process.env.PLANE_CLIENT_SECRET!; const REDIRECT_URI = process.env.PLANE_REDIRECT_URI!; const WEBHOOK_SECRET = process.env.PLANE_WEBHOOK_SECRET!; const PLANE_API_URL = process.env.PLANE_API_URL || "https://api.plane.so"; // In-memory storage (use a database in production) const installations = new Map< string, { botToken: string; workspaceSlug: string; appInstallationId: string; } >(); // Setup URL - redirect to Plane's consent screen app.get("/oauth/setup", (req, res) => { const params = new URLSearchParams({ client_id: CLIENT_ID, response_type: "code", redirect_uri: REDIRECT_URI, }); res.redirect(`${PLANE_API_URL}/auth/o/authorize-app/?${params}`); }); // OAuth callback - exchange app_installation_id for bot token app.get("/oauth/callback", async (req, res) => { const appInstallationId = req.query.app_installation_id as string; if (!appInstallationId) { return res.status(400).send("Missing app_installation_id"); } try { const basicAuth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64"); // Exchange for bot token const tokenRes = await axios.post( `${PLANE_API_URL}/auth/o/token/`, new URLSearchParams({ grant_type: "client_credentials", app_installation_id: appInstallationId, }).toString(), { headers: { Authorization: `Basic ${basicAuth}`, "Content-Type": "application/x-www-form-urlencoded", }, }, ); const botToken = tokenRes.data.access_token; // Get workspace details const installRes = await axios.get( `${PLANE_API_URL}/auth/o/app-installation/?id=${appInstallationId}`, { headers: { Authorization: `Bearer ${botToken}` }, }, ); const installation = installRes.data[0]; const workspaceId = installation.workspace; const workspaceSlug = installation.workspace_detail.slug; // Store credentials installations.set(workspaceId, { botToken, workspaceSlug, appInstallationId }); console.log(`Installed in workspace: ${workspaceSlug}`); res.send("Installation successful! You can close this window."); } catch (error) { console.error("OAuth error:", error); res.status(500).send("Installation failed"); } }); // Webhook handler app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { const signature = req.headers["x-plane-signature"] as string; const payload = req.body.toString(); // Verify signature const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(signature || ""), Buffer.from(expected))) { return res.status(403).send("Invalid signature"); } const event = JSON.parse(payload); console.log(`Received: ${event.event} ${event.action}`); // Get credentials for this workspace const creds = installations.get(event.workspace_id); if (creds) { // Process the event with creds.botToken } res.status(200).send("OK"); }); app.listen(3000, () => console.log("Server running on http://localhost:3000")); ``` ```python [Python (Flask)] import os import hmac import hashlib import base64 import requests as http_requests from flask import Flask, request, redirect from urllib.parse import urlencode app = Flask(__name__) CLIENT_ID = os.getenv("PLANE_CLIENT_ID") CLIENT_SECRET = os.getenv("PLANE_CLIENT_SECRET") REDIRECT_URI = os.getenv("PLANE_REDIRECT_URI") WEBHOOK_SECRET = os.getenv("PLANE_WEBHOOK_SECRET") PLANE_API_URL = os.getenv("PLANE_API_URL", "https://api.plane.so") # In-memory storage (use a database in production) installations = {} @app.route("/oauth/setup") def oauth_setup(): """Redirect to Plane's consent screen.""" params = urlencode({ "client_id": CLIENT_ID, "response_type": "code", "redirect_uri": REDIRECT_URI, }) return redirect(f"{PLANE_API_URL}/auth/o/authorize-app/?{params}") @app.route("/oauth/callback") def oauth_callback(): """Exchange app_installation_id for bot token.""" app_installation_id = request.args.get("app_installation_id") if not app_installation_id: return "Missing app_installation_id", 400 try: # Exchange for bot token credentials = f"{CLIENT_ID}:{CLIENT_SECRET}" basic_auth = base64.b64encode(credentials.encode()).decode() token_response = http_requests.post( f"{PLANE_API_URL}/auth/o/token/", data={ "grant_type": "client_credentials", "app_installation_id": app_installation_id, }, headers={ "Authorization": f"Basic {basic_auth}", "Content-Type": "application/x-www-form-urlencoded", }, ) token_response.raise_for_status() bot_token = token_response.json()["access_token"] # Get workspace details install_response = http_requests.get( f"{PLANE_API_URL}/auth/o/app-installation/", params={"id": app_installation_id}, headers={"Authorization": f"Bearer {bot_token}"}, ) install_response.raise_for_status() installation = install_response.json()[0] workspace_id = installation["workspace"] workspace_slug = installation["workspace_detail"]["slug"] # Store credentials installations[workspace_id] = { "bot_token": bot_token, "workspace_slug": workspace_slug, "app_installation_id": app_installation_id, } print(f"Installed in workspace: {workspace_slug}") return "Installation successful! You can close this window." except Exception as e: print(f"OAuth error: {e}") return "Installation failed", 500 @app.route("/webhook", methods=["POST"]) def webhook(): """Handle incoming webhooks.""" signature = request.headers.get("X-Plane-Signature", "") payload = request.get_data() # Verify signature expected = hmac.new( WEBHOOK_SECRET.encode(), payload, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, signature): return "Invalid signature", 403 event = request.get_json() print(f"Received: {event['event']} {event['action']}") # Get credentials for this workspace creds = installations.get(event["workspace_id"]) if creds: # Process the event with creds["bot_token"] pass return "OK", 200 if __name__ == "__main__": app.run(port=3000) ``` ::: ## Next Steps * [Build an Agent](/dev-tools/agents/overview) - Create AI agents that respond to @mentions * [API Reference](/api-reference/introduction) - Explore the full Plane API * [Webhook Events](/dev-tools/intro-webhooks) - All webhook event types * [Example: PRD Agent](https://github.com/makeplane/prd-agent) - Complete agent implementation ## Publish to Marketplace Apps can be listed on the [Plane Marketplace](https://plane.so/marketplace/integrations). Contact to list your app. --- --- url: 'https://developers.plane.so/dev-tools/mcp-server.html' description: >- Connect Claude, ChatGPT, Codex, Cursor, VS Code, Windsurf, Zed, and Antigravity to Plane over MCP. Endpoints, OAuth and access-token auth, per-client setup, security, and troubleshooting. --- # MCP server Use Plane from the AI tool you already work in to create work items, plan cycles, and query projects in natural language. The server is [open source](https://github.com/makeplane/plane-mcp-server) under the MIT license. ::: tip Hosted server Connect to `https://mcp.plane.so/http/mcp` and sign in with your Plane account. ::: ::: tip Just want to connect your AI tool? Use the [short setup guide](https://docs.plane.so/ai/mcp-server). ::: ## How it works [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard for how AI clients discover and call external tools. The Plane MCP server sits between your client and Plane's REST API, then acts as the signed-in user. Version 0.3.0 exposes **28 tools, one per resource, covering 183 actions**. Pass `action` to select an operation: ```python workitem(action="create", project_id=..., name="Fix login") workitem(action="list", project_id=..., pql='stateGroup = "started"') cycle(action="archive", project_id=..., cycle_id=...) ``` Every tool description lists its actions and marks parameters as required or optional. Tools also carry MCP `readOnlyHint` and `destructiveHint` annotations derived from their actions. ### Hosted or self-hosted Plane Cloud users can connect to `mcp.plane.so`. For self-hosted Plane, run locally with `PLANE_BASE_URL` set to your instance, or [deploy your own server](/dev-tools/mcp-server-self-host). ## What you can do * [Work items](/dev-tools/mcp-server-tools#work-items): create, update, search, comment, attach, link, relate, nest, and log time. * [Types, properties, and estimates](/dev-tools/mcp-server-tools#types-properties-and-estimates): manage types, custom properties, and estimates. * [Planning](/dev-tools/mcp-server-tools#planning): plan cycles, modules, milestones, and initiatives. * [Releases](/dev-tools/mcp-server-tools#releases): manage tags, labels, work items, and changelogs. * [Projects and workspace](/dev-tools/mcp-server-tools#projects-and-workspace): manage projects, states, labels, members, pages, features, and intake. * [Customers](/dev-tools/mcp-server-tools#customers): manage customers, requests, properties, and linked work. * [Query](/dev-tools/mcp-server-tools#query): retrieve the PQL language reference before composing filters. ### Query with PQL `workitem list`, `workitem list_archived`, `workitem count`, `cycle list_workitems`, and `module list_workitems` accept `pql`. UUID-backed fields require UUIDs, so resolve names first. Call `get_pql_reference` with `detail="brief"` or `detail="full"`; see [Plane Query Language](https://docs.plane.so/core-concepts/issues/plane-query-language). There are no separate epic tools. Follow the [epics recipe](/dev-tools/mcp-server-tools#epics). ## Endpoints and authentication | Endpoint | Auth | Use it for | | --------------------------------------- | --------------------- | ---------------------------------------------------- | | `https://mcp.plane.so/http/mcp` | OAuth | Streamable HTTP; recommended for interactive use | | `https://mcp.plane.so/http/api-key/mcp` | PAT headers | Automations, CI, headless agents, shared team setups | | `uvx plane-mcp-server stdio` | Environment variables | Self-hosted Plane and local or offline development | | `https://mcp.plane.so/sse` | OAuth | Deprecated clients that still require HTTP+SSE | ### OAuth Your client redirects you to Plane, where you sign in and choose a workspace. The server validates the resulting token with `/api/v1/users/me/`, and the connection stays bound to that workspace. The default redirect allowlist covers Cursor, VS Code, Antigravity, Claude.ai, ChatGPT, and localhost callbacks. A self-hosted server can add other clients through `PLANE_OAUTH_ALLOWED_REDIRECT_URIS`. Re-authenticate from your client's connector controls. In Claude Code, run `/mcp`; with `mcp-remote`, clear its cache: ```bash rm -rf ~/.mcp-auth ``` This removes cached OAuth credentials for every `mcp-remote` server, not only Plane. To keep Plane's cache separate, set `MCP_REMOTE_CONFIG_DIR` in that server's `env` and remove that directory instead. ### Personal access token Send both headers on every request to the PAT endpoint: | Header | Value | | ------------------ | ------------------ | | `Authorization` | `Bearer ` | | `x-workspace-slug` | `` | ::: warning Changed Earlier versions of this page showed an `x-api-key` header. The server reads the standard `Authorization: Bearer` header; update existing configs. ::: #### Get a token Create a personal access token under **Profile Settings → Personal Access Tokens** and copy it when shown. For automations, you can instead create a workspace access token under **Workspace Settings → Access Tokens**. #### Find your workspace slug The slug is the segment after `app.plane.so/` in your Plane URL. In `https://app.plane.so/acme-corp/`, it is `acme-corp`. ### Local (stdio) Local mode requires Python 3.10+ and [`uv`](https://docs.astral.sh/uv/). On macOS or Linux: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` On Windows: ```powershell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` | Variable | Required | Description | | ---------------------- | -------- | --------------------------------------------------------------------------------- | | `PLANE_API_KEY` | Yes | Your Plane personal or workspace access token | | `PLANE_WORKSPACE_SLUG` | Yes | The workspace slug | | `PLANE_BASE_URL` | No | Defaults to `https://api.plane.so`; set it to your self-hosted Plane instance URL | Prefer stdio when the client runs on the same machine, you need a self-hosted or private Plane instance, or you do not want to expose an MCP HTTP service. ### SSE (deprecated) The MCP specification deprecated the older HTTP+SSE transport. Keep `https://mcp.plane.so/sse` only for an existing client that cannot use Streamable HTTP, and migrate when that client supports it. ## Connect a client Replace `mcp.plane.so` with your own host if you self-host the server. Tabs stay in sync across this page. ### General These are the common shapes. Some clients use `serverUrl`, `servers`, or `context_servers`; use the client-specific schema below. :::tabs key:mcp-auth \== OAuth {#general-oauth} ```json { "mcpServers": { "plane": { "url": "https://mcp.plane.so/http/mcp" } } } ``` \== Access token {#general-token} ```json { "mcpServers": { "plane": { "url": "https://mcp.plane.so/http/api-key/mcp", "headers": { "Authorization": "Bearer ", "x-workspace-slug": "" } } } } ``` \== Local (stdio) {#general-stdio} ```json { "mcpServers": { "plane": { "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { "PLANE_API_KEY": "", "PLANE_WORKSPACE_SLUG": "" } } } } ``` ::: ### Claude :::tabs key:mcp-auth \== OAuth {#claude-oauth} On Claude Desktop or claude.ai: 1. Open **Settings → Connectors → Add custom connector**. 2. Paste `https://mcp.plane.so/http/mcp`, select **Add**, then **Connect**. 3. Sign in to Plane. In a chat, choose **+ → Connectors** to enable Plane. Free plans allow one custom connector. On Team or Enterprise, an Owner adds it under **Organization settings → Connectors**, then members select **Connect**. \== Access token {#claude-token} Desktop users who need a token instead of OAuth can bridge with `mcp-remote` (Node.js 22+ recommended): ```json { "mcpServers": { "plane": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.plane.so/http/api-key/mcp", "--header", "Authorization:${PLANE_AUTH_HEADER}", "--header", "x-workspace-slug:${PLANE_WORKSPACE_SLUG}" ], "env": { "PLANE_AUTH_HEADER": "Bearer ", "PLANE_WORKSPACE_SLUG": "" } } } } ``` \== Local (stdio) {#claude-stdio} Use **Settings → Developer → Edit Config**, or edit `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows: ```json { "mcpServers": { "plane": { "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { "PLANE_API_KEY": "", "PLANE_WORKSPACE_SLUG": "" } } } } ``` Quit and relaunch Claude Desktop. This file supports stdio only: never put `url` or `type: http` in it. ::: ### Claude Code :::tabs key:mcp-auth \== OAuth {#claude-code-oauth} ```bash claude mcp add --transport http plane https://mcp.plane.so/http/mcp # In a session, run /mcp and authenticate (or run: claude mcp login plane). claude mcp list ``` \== Access token {#claude-code-token} Put `--header` after the URL: ```bash claude mcp add --transport http plane https://mcp.plane.so/http/api-key/mcp \ --header "Authorization: Bearer " \ --header "x-workspace-slug: " ``` \== Local (stdio) {#claude-code-stdio} ```bash claude mcp add --transport stdio plane \ --env PLANE_API_KEY= \ --env PLANE_WORKSPACE_SLUG= \ -- uvx plane-mcp-server stdio ``` ::: Use `--scope local|project|user`; project scope writes a shareable `.mcp.json` with `mcpServers`, `type: "http"`, and `url`. PAT entries add `headers`, and `${PLANE_PAT}` expands from the environment. Claude Code's SSE transport is deprecated. ### ChatGPT ChatGPT supports OAuth on Plus, Pro, Business, Enterprise, and Edu plans: 1. Open **Settings → Security and login** and turn on **Developer mode**. Business, Enterprise, and Edu workspaces require an admin to allow it. 2. Open **chatgpt.com/plugins**, select **+**, name the connection "Plane", enter `https://mcp.plane.so/http/mcp` under **Connection**, select **Create**, then sign in to Plane. 3. In a chat, open **+ → Developer mode** and enable Plane. The exact menu names may differ by workspace. ChatGPT does not accept custom headers, so use OAuth. ### Codex The CLI, IDE extension, and ChatGPT desktop app share `~/.codex/config.toml`. :::tabs key:mcp-auth \== OAuth {#codex-oauth} ```bash codex mcp add plane --url https://mcp.plane.so/http/mcp codex mcp login plane codex mcp list ``` You can also run `/mcp` inside Codex. No experimental flag is required. \== Access token {#codex-token} ```toml [mcp_servers.plane] url = "https://mcp.plane.so/http/api-key/mcp" bearer_token_env_var = "PLANE_PAT" http_headers = { "x-workspace-slug" = "" } ``` `bearer_token_env_var` sends `Authorization: Bearer $PLANE_PAT`. The CLI supports `codex mcp add … --bearer-token-env-var PLANE_PAT`; arbitrary headers are config-file only. \== Local (stdio) {#codex-stdio} ```toml [mcp_servers.plane] command = "uvx" args = ["plane-mcp-server", "stdio"] [mcp_servers.plane.env] PLANE_API_KEY = "" PLANE_WORKSPACE_SLUG = "" ``` ::: ### Cursor Use `~/.cursor/mcp.json` globally or `.cursor/mcp.json` in a project. :::tabs key:mcp-auth \== OAuth {#cursor-oauth} [![Install in Cursor](/images/mcp/install-in-cursor.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=plane\&config=eyJ1cmwiOiJodHRwczovL21jcC5wbGFuZS5zby9odHRwL21jcCJ9) Or add the server manually: ```json { "mcpServers": { "plane": { "url": "https://mcp.plane.so/http/mcp" } } } ``` Cursor shows **Login** or **Needs authentication** and completes OAuth. Manage servers from **Customize**. \== Access token {#cursor-token} ```json { "mcpServers": { "plane": { "url": "https://mcp.plane.so/http/api-key/mcp", "headers": { "Authorization": "Bearer ${env:PLANE_PAT}", "x-workspace-slug": "" } } } } ``` \== Local (stdio) {#cursor-stdio} ```json { "mcpServers": { "plane": { "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { "PLANE_API_KEY": "${env:PLANE_API_KEY}", "PLANE_WORKSPACE_SLUG": "" } } } } ``` ::: Remote entries use `url` and must not include a `type` key. ### VS Code Use `.vscode/mcp.json` for a workspace, or run **MCP: Open User Configuration** for the user file. :::tabs key:mcp-auth \== OAuth {#vs-code-oauth} [![Install in VS Code](/images/mcp/install-in-vscode.svg)](https://vscode.dev/redirect/mcp/install?name=plane\&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.plane.so%2Fhttp%2Fmcp%22%7D) [Install in VS Code Insiders](https://insiders.vscode.dev/redirect/mcp/install?name=plane\&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.plane.so%2Fhttp%2Fmcp%22%7D\&quality=insiders), or add it from the CLI: ```bash code --add-mcp '{"name":"plane","type":"http","url":"https://mcp.plane.so/http/mcp"}' ``` Trust the server on first start, verify it with **MCP: List Servers**, and use Copilot Chat in **Agent** mode. Copilot Business and Enterprise organizations must enable the "MCP servers in Copilot" policy. \== Access token {#vs-code-token} ```json { "inputs": [ { "type": "promptString", "id": "plane-pat", "description": "Plane personal access token", "password": true }, { "type": "promptString", "id": "plane-slug", "description": "Plane workspace slug" } ], "servers": { "plane": { "type": "http", "url": "https://mcp.plane.so/http/api-key/mcp", "headers": { "Authorization": "Bearer ${input:plane-pat}", "x-workspace-slug": "${input:plane-slug}" } } } } ``` \== Local (stdio) {#vs-code-stdio} ```json { "servers": { "plane": { "type": "stdio", "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { "PLANE_API_KEY": "", "PLANE_WORKSPACE_SLUG": "" } } } } ``` ::: ### Windsurf Current vendor docs call Windsurf **Devin Desktop**. Its configuration remains at `~/.codeium/windsurf/mcp_config.json`; open Cascade's **MCPs → Manage MCPs** or **Settings → Cascade → MCP Servers**. :::tabs key:mcp-auth \== OAuth {#windsurf-oauth} ```json { "mcpServers": { "plane": { "serverUrl": "https://mcp.plane.so/http/mcp" } } } ``` If the OAuth sign-in does not complete, use the access-token configuration instead. \== Access token {#windsurf-token} ```json { "mcpServers": { "plane": { "serverUrl": "https://mcp.plane.so/http/api-key/mcp", "headers": { "Authorization": "Bearer ${env:PLANE_PAT}", "x-workspace-slug": "" } } } } ``` \== Local (stdio) {#windsurf-stdio} ```json { "mcpServers": { "plane": { "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { "PLANE_API_KEY": "${env:PLANE_API_KEY}", "PLANE_WORKSPACE_SLUG": "" } } } } ``` ::: Remote entries use `serverUrl`. Refresh the server list after saving. ### Zed Use **Settings → AI → MCP Servers → Add Server**, or edit `~/.config/zed/settings.json`. :::tabs key:mcp-auth \== OAuth {#zed-oauth} ```json { "context_servers": { "plane": { "url": "https://mcp.plane.so/http/mcp" } } } ``` Zed prompts for OAuth through an allowlisted loopback callback. \== Access token {#zed-token} ```json { "context_servers": { "plane": { "url": "https://mcp.plane.so/http/api-key/mcp", "headers": { "Authorization": "Bearer ", "x-workspace-slug": "" } } } } ``` \== Local (stdio) {#zed-stdio} ```json { "context_servers": { "plane": { "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { "PLANE_API_KEY": "", "PLANE_WORKSPACE_SLUG": "" } } } } ``` ::: Zed uses this flat schema; the old nested `command.path` and `source: custom` shape is outdated. ### Antigravity The IDE and CLI share `~/.gemini/config/mcp_config.json` globally or `.agents/mcp_config.json` in a workspace. :::tabs key:mcp-auth \== OAuth {#antigravity-oauth} ```json { "mcpServers": { "plane": { "serverUrl": "https://mcp.plane.so/http/mcp" } } } ``` OAuth is automatic. In the IDE, open **… → MCP Servers → Manage MCP Servers**. In Antigravity 2.0, use **Settings → Customizations → Installed MCP Servers → Add MCP**; in the CLI, run `/mcp`. \== Access token {#antigravity-token} ```json { "mcpServers": { "plane": { "serverUrl": "https://mcp.plane.so/http/api-key/mcp", "headers": { "Authorization": "Bearer ${env:PLANE_PAT}", "x-workspace-slug": "" } } } } ``` \== Local (stdio) {#antigravity-stdio} ```json { "mcpServers": { "plane": { "command": "uvx", "args": ["plane-mcp-server", "stdio"], "env": { "PLANE_API_KEY": "${env:PLANE_API_KEY}", "PLANE_WORKSPACE_SLUG": "" } } } } ``` ::: Remote entries require `serverUrl`; `url` and `httpUrl` are unsupported. ### Other clients For a stdio-only client, use `mcp-remote` with Node.js 22+ recommended. A client with native remote-MCP support only needs the OAuth URL. :::tabs key:mcp-auth \== OAuth {#other-clients-oauth} ```json { "mcpServers": { "plane": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.plane.so/http/mcp"] } } } ``` \== Access token {#other-clients-token} ```json { "mcpServers": { "plane": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.plane.so/http/api-key/mcp", "--header", "Authorization:${PLANE_AUTH_HEADER}", "--header", "x-workspace-slug:${PLANE_WORKSPACE_SLUG}" ], "env": { "PLANE_AUTH_HEADER": "Bearer ", "PLANE_WORKSPACE_SLUG": "" } } } } ``` ::: `mcp-remote` reads headers from its `--header` arguments; a `headers` key on this stdio entry is ignored. Keep the header values in `env` and write the arguments without spaces around the colon: on Windows, spaces inside `args` can be mangled by some clients. To reset cached OAuth state, remove `~/.mcp-auth` (or the directory `MCP_REMOTE_CONFIG_DIR` points to). ## Common workflows **What's on my plate** ```text List work items assigned to me that are in progress or overdue, grouped by project. ``` *Trace: `member me` → `workitem list` without `project_id`, using `pql='assignee = currentUser() AND (stateGroup = "started" OR isOverdue())'`.* **File a bug** ```text Create a high-priority bug in ENG called "Login times out on Safari 17". Description: the OAuth callback lands on a blank page. Assign it to me and add the "auth" label. ``` *Trace: `project list` → `member me` → `label list` → `workitem create`.* **Roll over a sprint** ```text Create Sprint 15 in ENG from June 2 to June 15, move everything unfinished from Sprint 14 into it, and give me a count by priority. ``` *Trace: `cycle create` with `owned_by` → `cycle list` to find Sprint 14 → `cycle transfer_workitems` → `workitem count` with `pql` and `group_by="priority"`.* **Close the loop** ```text Log 90 minutes on ENG-42 with the note "Implemented retry logic", mark it Done, and comment "Fixed in abc1234, needs QA". ``` *Trace: `workitem retrieve_by_identifier` → `work_log create` → `state list` → `workitem update` → `workitem_comment create`.* ## Permissions and sessions * The server acts as the authenticated user. Plane enforces workspace and project roles, so a Guest cannot do more through MCP. * OAuth requests `read` and `write` scopes. The workspace chosen at consent binds that connection; reconnect to switch workspaces. * A PAT connection is scoped by `x-workspace-slug`. * Hosted OAuth tokens are stored server-side in Redis or Valkey. A self-hosted server without Redis falls back to in-memory storage. * Revoke access by disconnecting the connector in your client, deleting a PAT in Plane, or clearing the `mcp-remote` cache. ## Security best practices * Use only `https://mcp.plane.so` or your own trusted host, and check the URL on Plane's consent screen. * Treat work item titles, descriptions, comments, and attachments as untrusted model input. Prefer clients that confirm writes; destructive actions are flagged with `destructiveHint`. * Keep PATs out of shared or committed configs. Use environment variables or `${input:...}`, and never commit a token in a project-scoped `.mcp.json`. * Use a workspace access token with the minimum role needed for automations. * Revoke tokens in Plane settings and audit API token events in the workspace audit log. * Server logs are structured JSON with tool name, duration, status, opaque user ID, and workspace slug. Display names are logged only when `LOG_USER_INFO=true`, because they are PII. ## Self-hosted Plane The hosted `mcp.plane.so` service cannot reach private Plane instances. In stdio mode, set `PLANE_BASE_URL` to your instance URL, then test the token against Plane's REST API. Read the key into a shell variable first so it stays out of your command history: ```bash read -rs PLANE_API_KEY # paste the key and press Enter; nothing is echoed curl -H "x-api-key: $PLANE_API_KEY" \ "https://plane.yourcompany.com/api/v1/users/me/" ``` A `200` response confirms the key and URL. That header is the Plane REST API header, not the MCP PAT header. ::: tip Running your own MCP server? Follow the [self-hosting guide](/dev-tools/mcp-server-self-host) for Docker, Helm, OAuth, storage, and operations. The OAuth transport needs Plane's OAuth application registration, which is available on Plane Cloud and Plane Commercial Edition; on Community Edition, use stdio mode. ::: ## Upgrading ### From per-operation tools (0.2.x → 0.3.0) The 177 per-operation tools became 28 resource tools. Of the 177 names, 169 still resolve as hidden aliases and keep their original parameter names, so saved prompts and scripts continue to work; `get_pql_reference` is unchanged; and seven cannot be mapped and return a message naming their replacement. See [retired tool names](/dev-tools/mcp-server-tools#retired-tool-names). `project list` is now paginated by default. Follow `next_cursor` or pass `per_page`. Archive actions now return an explicit status object. ### From the Node.js server The `@makeplane/plane-mcp-server` npm package is deprecated. Update environment variables, then use the stdio configuration shown above: | Node.js server | Python server | | ---------------------- | ---------------------- | | `PLANE_API_KEY` | `PLANE_API_KEY` | | `PLANE_API_HOST_URL` | `PLANE_BASE_URL` | | `PLANE_WORKSPACE_SLUG` | `PLANE_WORKSPACE_SLUG` | Replace the old Node.js `command` and `args` with `uvx plane-mcp-server stdio`. ## Troubleshooting | Symptom | Cause | Fix | | ----------------------------------- | ------------------------------------------- | -------------------------------------------------------------------- | | 401 with PAT | Token is wrong, revoked, or uses old header | Use `Authorization: Bearer ` instead of `x-api-key` | | 401 with OAuth | Token expired | Re-authenticate from the client | | "workspace slug missing" | PAT config omits the workspace header | Add `x-workspace-slug` | | 404 | Workspace slug or resource ID is wrong | Check the slug or ID | | 403 | Your Plane role is too low | Ask for the required workspace or project role | | 400 | An argument is missing or invalid | Read the error; permitted enum values are in the tool description | | "not available on your plan" or 402 | The Plane plan does not include the feature | Enable the feature or use an available action | | `mcp-remote` fails to start | Node.js is too old | Use Node.js 22+ and run `npx -y mcp-remote@latest` | | Server is not listed | JSON or client schema is invalid | Remove trailing commas; apply the client-specific schema notes above | | Only the first page of projects | `project list` is paginated | Follow `next_cursor` or pass `per_page` | | Tools look stale or out of order | Pinned tool order or client cache is stale | Restart the client after upgrades | For a server that is not listed, remember that Claude Desktop's JSON file cannot contain `url`, Windsurf and Antigravity require `serverUrl`, and a Cursor remote entry must not contain `type`. Debug with: ```bash claude --debug claude mcp list PLANE_API_KEY= PLANE_WORKSPACE_SLUG= uvx plane-mcp-server stdio curl -X POST http://localhost:8211/http/mcp rm -rf ~/.mcp-auth ``` The local HTTP request should return either `401` or an MCP response. ## FAQ ::: details Which Plane plans work? The server follows your Plane plan and role. A plan-gated action returns a message naming the unavailable feature. ::: ::: details Is the server free? The MIT-licensed server is free to use. The Plane features it can access follow your Plane plan. ::: ::: details Does it work with self-hosted Plane? Yes. Use stdio with `PLANE_BASE_URL`, or [deploy your own MCP server](/dev-tools/mcp-server-self-host). ::: ::: details Is there a read-only mode? There is no separate read-only endpoint. Use your client's tool allow-list; read-only tools are annotated with `readOnlyHint`. ::: ::: details Can I limit which tools are available? Yes. Use the client's tool allow-list or deny-list. ::: ::: details How do epics work? An epic is a work item whose type is "Epic". Follow the [epics recipe](/dev-tools/mcp-server-tools#epics). ::: ::: details Does it use Plane AI credits? No. The MCP server calls Plane's API directly; the AI model belongs to your MCP client. ::: ::: details Where does my data go? The hosted server proxies requests to `api.plane.so`. Self-host the MCP server if you need full infrastructure control. ::: ## See also * [Tool reference](/dev-tools/mcp-server-tools) * [Self-host the MCP server](/dev-tools/mcp-server-self-host) * [Short setup guide](https://docs.plane.so/ai/mcp-server) * [Plane MCP server on GitHub](https://github.com/makeplane/plane-mcp-server) * [Plane Query Language](https://docs.plane.so/core-concepts/issues/plane-query-language) --- --- url: 'https://developers.plane.so/dev-tools/mcp-server-self-host.html' description: >- Deploy the Plane MCP server with Docker Compose or Helm, register OAuth callbacks, configure storage and security, and connect AI clients. --- # Self-host the MCP server This guide is for teams that want to run their own instance of the Plane MCP server — either because they use a self-hosted Plane installation that needs OAuth against their own domain, or because they want full control over the MCP infrastructure. If you're a Plane Cloud user connecting to `mcp.plane.so`, you don't need this. Use the [MCP server setup guide](/dev-tools/mcp-server) instead. ## Prerequisites * A running **Plane instance** (self-hosted or Cloud) with workspace admin access. OAuth application registration is available on Plane Cloud and Plane Commercial Edition; Plane Community Edition does not include it, so the OAuth transport cannot be used against a Community Edition instance. Community Edition users should run the server in [local (stdio) mode](/dev-tools/mcp-server#local-stdio) with a personal access token instead. * **Docker** and Docker Compose v2+, *or* **Kubernetes** v1.21+ with Helm v3+ * A **public URL** for the MCP server (e.g. `https://mcp.yourdomain.com`) — OAuth callbacks must reach it over HTTPS *** ## Register an OAuth app in Plane The MCP server authenticates users through Plane's OAuth 2.0 system. You need to register an app to get a Client ID and Client Secret. 1. Go to **Workspace settings → Integrations**: ```text https:////settings/integrations/ ``` 2. Click **Build your own**. 3. Fill in the application details: | Field | Value | | ---------------- | ---------------------------------------------------------------- | | **App Name** | Anything descriptive (e.g. `Plane MCP Server`) | | **Setup URL** | Your MCP server's public URL (e.g. `https://mcp.yourdomain.com`) | | **Redirect URI** | Both URIs listed below, space-separated | | **Webhook URL** | Leave empty unless you need webhook events | ::: tip Add both redirect URIs FastMCP exposes one callback under the HTTP mount and one under the SSE mount: | Transport | Redirect URI | | ---------------- | ------------------------------------- | | Streamable HTTP | `/http/auth/callback` | | SSE (deprecated) | `/auth/callback` | For `https://mcp.yourdomain.com`, paste this into the Redirect URI field: ```text https://mcp.yourdomain.com/http/auth/callback https://mcp.yourdomain.com/auth/callback ``` A previously registered `https://mcp.yourdomain.com/callback` URI is harmless but unnecessary. ::: 4. Under **Scopes & permissions**, select both **read** and **write** scopes. 5. Save. Copy the generated **Client ID** and **Client Secret** - you'll need them in the next step. ::: warning Never expose the Client Secret in client-side code or commit it to version control. ::: For more detail on OAuth app creation, see [Create an OAuth Application](/dev-tools/build-plane-app/create-oauth-application). *** ## Deploy ### Option A: Docker Compose **1. Create a `docker-compose.yaml`:** ```yaml name: plane-mcp services: mcp: image: makeplane/plane-mcp-server:${APP_RELEASE_VERSION:-latest} restart: always ports: - "8211:8211" env_file: - variables.env environment: REDIS_HOST: valkey REDIS_PORT: "6379" depends_on: valkey: condition: service_healthy valkey: image: valkey/valkey:8-alpine restart: always volumes: - valkey-data:/data healthcheck: test: ["CMD", "valkey-cli", "ping"] interval: 5s timeout: 3s retries: 5 volumes: valkey-data: ``` **2. Create a `variables.env` with your OAuth credentials from Step 1:** ```env # Image tag - pin to a specific version in production APP_RELEASE_VERSION=latest # Plane API URL - use your self-hosted instance URL or https://api.plane.so for Cloud PLANE_BASE_URL=https://api.plane.so # Optional: internal URL for server-to-server calls (same-network setups) # PLANE_INTERNAL_BASE_URL= # OAuth credentials from Step 1 PLANE_OAUTH_PROVIDER_CLIENT_ID=your-client-id PLANE_OAUTH_PROVIDER_CLIENT_SECRET=your-client-secret # Public URL where MCP clients reach this server (must match what you registered in Step 1) PLANE_OAUTH_PROVIDER_BASE_URL=https://mcp.yourdomain.com ``` **3. Start:** ```bash docker compose up -d ``` **4. Verify:** ```bash docker compose logs -f mcp # follow startup logs curl http://localhost:8211/http/mcp # expect: 401 or MCP protocol response ``` ::: warning Terminate TLS in front of this container The container listens on plain HTTP at `:8211`. Put it behind a reverse proxy (nginx, Caddy, Traefik, Cloudflare) that handles TLS. OAuth callbacks will fail without HTTPS, and `PLANE_OAUTH_PROVIDER_BASE_URL` must be the `https://` URL that proxy exposes. ::: #### Environment variable reference | Variable | Required | Description | | ------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | | `APP_RELEASE_VERSION` | No | Image tag to deploy. Defaults to `latest`. Pin in production. | | `PLANE_BASE_URL` | No | Public Plane API URL. Defaults to `https://api.plane.so`. | | `PLANE_INTERNAL_BASE_URL` | No | Internal Plane URL for server-to-server calls. Falls back to `PLANE_BASE_URL`. | | `PLANE_OAUTH_PROVIDER_CLIENT_ID` | Yes | OAuth Client ID from Step 1. | | `PLANE_OAUTH_PROVIDER_CLIENT_SECRET` | Yes | OAuth Client Secret from Step 1. | | `PLANE_OAUTH_PROVIDER_BASE_URL` | Yes | Public URL of **this MCP server**, not your Plane instance. | | `PLANE_OAUTH_PROVIDER_ENABLE_CIMD` | No | Enables client ID metadata documents. Defaults to `false`. | | `PLANE_OAUTH_ALLOWED_REDIRECT_URIS` | No | Comma-separated extra client redirect patterns. `*` can match a port, path segment, or subdomain; keep hosts pinned. | | `MCP_PATH_PREFIX` | No | Prefix for every route. For example, `/plane` serves MCP at `/plane/http/mcp`. | | `REDIS_HOST` | No | Redis or Valkey host for persistent OAuth token storage. Without it, tokens use in-memory storage. | | `REDIS_PORT` | No | Redis or Valkey port. | | `REDIS_PASSWORD` | No | Static Redis or Valkey password. | | `REDIS_SSL` | No | Enables TLS for Redis or Valkey when set to `true`. | | `ELASTICACHE_SECRET_ARN` | No | AWS Secrets Manager ARN containing a rotating ElastiCache authentication token. | | `AWS_REGION` | No | AWS region for `ELASTICACHE_SECRET_ARN`. | | `REDIS_AUTH_TOKEN_KEY` | No | JSON key that contains the rotating token in the AWS secret. | | `LOG_USER_INFO` | No | Logs the user's display name when `true`. Defaults to `false`; the display name is PII. | #### Onboard a new MCP client The built-in redirect allowlist contains: * `http://localhost:*`, `http://localhost:*/*`, `http://127.0.0.1:*`, and `http://127.0.0.1:*/*` * `cursor://anysphere.cursor-mcp/oauth/*` and `https://www.cursor.com/*` * `https://vscode.dev/redirect` and `https://insiders.vscode.dev/redirect` * `https://antigravity.google/oauth-callback` * `https://claude.ai/*` * `https://chatgpt.com/connector/oauth/*` and `https://chatgpt.com/connector_platform_oauth_redirect` Append new client callbacks without releasing a new server version: ```env PLANE_OAUTH_ALLOWED_REDIRECT_URIS=https://newclient.com/cb,https://other.app/oauth/* ``` The `*` wildcard can match any port, path segment, or subdomain. Keep the host pinned to a domain you trust. #### Upgrading ```bash docker compose pull docker compose up -d ``` *** ### Option B: Helm **1. Add the Plane Helm repo:** ```bash helm repo add plane https://helm.plane.so helm repo update ``` **2. Create a `values.yaml`:** ```yaml ingress: enabled: true host: mcp.yourdomain.com ingressClass: nginx ssl: enabled: true issuer: cloudflare # cloudflare | digitalocean | http email: you@yourdomain.com services: api: plane_base_url: "https://api.plane.so" plane_oauth: enabled: true client_id: "" client_secret: "" provider_base_url: "https://mcp.yourdomain.com" ``` **3. Install:** ```bash helm install plane-mcp plane/plane-mcp-server \ --namespace plane-mcp \ --create-namespace \ -f values.yaml ``` #### Helm values reference | Value | Default | Description | | -------------------------------------------- | ----------------- | --------------------------------------------------- | | `dockerRegistry.default_tag` | `latest` | Image tag to deploy | | `ingress.enabled` | `true` | Enable ingress | | `ingress.host` | `mcp.example.com` | Public hostname | | `ingress.ingressClass` | `nginx` | Ingress class name | | `ingress.ssl.enabled` | `false` | Enable TLS via cert-manager | | `ingress.ssl.issuer` | `cloudflare` | ACME issuer (`cloudflare`, `digitalocean`, `http`) | | `services.api.replicas` | `1` | Number of MCP server replicas | | `services.api.plane_base_url` | `""` | Plane API URL | | `services.api.plane_oauth.enabled` | `false` | Enable OAuth endpoints | | `services.api.plane_oauth.client_id` | `""` | OAuth Client ID | | `services.api.plane_oauth.client_secret` | `""` | OAuth Client Secret | | `services.api.plane_oauth.provider_base_url` | `""` | Public URL this server is reachable on | | `services.redis.local_setup` | `true` | Deploy Valkey in-cluster | | `services.redis.external_redis_url` | `""` | External Valkey/Redis URL (if not using in-cluster) | Environment variables that have no Helm value — for example `PLANE_OAUTH_ALLOWED_REDIRECT_URIS` or `LOG_USER_INFO` — must be set as environment variables on the MCP server deployment. #### Upgrading ```bash helm upgrade plane-mcp plane/plane-mcp-server \ --namespace plane-mcp \ -f values.yaml ``` #### Uninstalling ```bash helm uninstall plane-mcp --namespace plane-mcp ``` *** ## Logging and observability The server emits structured JSON logs with the tool name, duration, status, opaque user ID, and workspace slug. `LOG_USER_INFO` defaults to `false`. Setting it to `true` also logs the user's display name, which is personally identifiable information. Even with `LOG_USER_INFO=false`, log entries contain the opaque user ID and the workspace slug, which can identify a person or organisation when combined with other data. Treat log storage as sensitive: restrict who can read it, set a retention period, and redact those fields before sharing logs outside your team. ## Connect AI clients Once the server is running, your available endpoints are: | Endpoint | Auth | Description | | --------------------------------------------- | --------------------------------------------------------- | -------------------------------- | | `https://mcp.yourdomain.com/http/mcp` | OAuth | Recommended for most clients | | `https://mcp.yourdomain.com/http/api-key/mcp` | `Authorization: Bearer `, `x-workspace-slug: ` | CI, scripts, and headless setups | | `https://mcp.yourdomain.com/sse` | OAuth | Deprecated HTTP+SSE transport | Client configuration is identical to the [MCP server setup guide](/dev-tools/mcp-server). Swap `https://mcp.plane.so` for your server's host in each configuration. *** ## Troubleshooting **Server not starting:** ```bash docker compose logs mcp ``` **Valkey not reachable:** ```bash docker compose exec valkey valkey-cli ping # Expect: PONG ``` If Valkey is unhealthy, tokens are stored in-memory and lost on restart. Verify `REDIS_HOST` and `REDIS_PORT` are set correctly in your environment. **OAuth errors:** * Confirm both redirect URIs are registered in your Plane OAuth app: `/http/auth/callback` and `/auth/callback`. An existing `/callback` registration is harmless but unnecessary. * Check that `PLANE_OAUTH_PROVIDER_CLIENT_ID` and `PLANE_OAUTH_PROVIDER_CLIENT_SECRET` match what Plane generated. * Check that `PLANE_OAUTH_PROVIDER_BASE_URL` is the publicly reachable `https://` URL of this MCP server - not your Plane instance URL. * If the client reports `redirect_uri is not allowed`, add its exact callback or a host-pinned pattern to `PLANE_OAUTH_ALLOWED_REDIRECT_URIS`, then restart the deployment. * Clear any cached auth tokens on the client side: ```bash rm -rf ~/.mcp-auth ``` **Reset Docker Compose** (deletes Valkey data): ```bash docker compose down -v docker compose up -d ``` **Still stuck:** 1. Double-check OAuth credentials and redirect URIs in Plane workspace settings. 2. Check the [plane-mcp-server](https://github.com/makeplane/plane-mcp-server) repo for known issues. 3. Contact support@plane.so. *** → For client configuration details, see the [MCP server setup guide](/dev-tools/mcp-server). → For the full list of available tools, see the [tool reference](/dev-tools/mcp-server-tools). --- --- url: 'https://developers.plane.so/dev-tools/mcp-server-tools.html' description: >- Reference for the 28 tools and 183 actions exposed by the Plane MCP server — work items, cycles, modules, releases, customers, pages, and more. --- # Tool reference The Plane MCP server exposes 28 tools, one per resource. Resource tools take an `action` parameter that selects one of 183 operations; `get_pql_reference` is the only exception. Every transport—hosted OAuth, hosted access token, local stdio, and deprecated SSE—exposes the same surface. The description your MCP client receives is generated from each tool's action declarations. It is the authoritative reference at call time, including required and optional parameters. **Totals:** 28 tools, 183 actions, and 169 retired-name aliases. For connection and authentication instructions, see [MCP server](/dev-tools/mcp-server). ## How to read this page Each tool table has these columns: * **Action** is the value to pass as `action`. * **Required** parameters must be present for that action. * **Optional** parameters are accepted only for that action. * **Notes** include action-specific behavior and the **Read-only** or **Destructive** flags. Actions with `cursor` and `per_page` return a `next_cursor` when another page is available. Follow it until it is empty. Update actions change only the fields you pass. Parameter values are plain strings, numbers, booleans, and lists. The server also coerces string-encoded lists, integers, and booleans. If you pass an argument that belongs to another action, validation rejects it and names the permitted parameters. ## Conventions ### Identifiers Most actions take UUIDs for projects, work items, states, labels, members, and other resources. List or resolve the relevant resource first when you have only its name or short identifier. `workitem retrieve_by_identifier` is the exception: its `workitem_identifier` accepts a readable identifier such as `ENG-42`. Other `workitem` actions use the work item's UUID in `workitem_id`. ### Project vs workspace scope Pages, work item types, and work item properties can belong to a project or the workspace. Supply `project_id` for the project's own set; omit it for the workspace's set. Some workspaces centrally govern the type and property vocabulary. In that mode, project-scoped writes are refused. Use `workitem_type resolve` to find or create a usable type without duplicates, or `workitem_type import_to_project` to link existing workspace types to a project. ### PQL `workitem list`, `workitem list_archived`, `workitem count`, `cycle list_workitems`, and `module list_workitems` accept a `pql` filter. Call `get_pql_reference` with `detail="brief"` or `detail="full"` before composing a query, and see the [Plane Query Language guide](https://docs.plane.so/core-concepts/issues/plane-query-language). UUID-backed PQL fields—such as project, assignee, state, label, cycle, module, type, milestone, and creator—need UUIDs. Resolve names before inserting them into a query. ### Epics Plane represents an epic as a work item whose type is named **Epic**. There is no separate epic tool. 1. Call `workitem_type resolve` with `project_id` and `name="Epic"`, then keep the returned type `id`. 2. Call `workitem create` with the project, name, and that `type_id`. 3. List epics with `workitem list` and `pql='type = ""'`. 4. Read, edit, or delete an epic with `workitem retrieve`, `workitem update`, or `workitem delete`. To nest a work item under it, pass the epic's work item UUID as `parent` to `workitem update` or `workitem create`. 5. List an epic's children with `workitem list` and `pql='childOf("PROJ-12")'`, using the epic's readable identifier. ### Plan availability The server declares feature gates for `work_log` (**Time tracking**), `workitem_type` (**Work item types**), `workitem_property` (**Work item properties**), and some `project` features. Customers, initiatives, releases, and pages can also be gated by the Plane API and your plan. When a feature is unavailable, the error names it. ## Tools by resource group Tools are grouped by the Plane resource they manage. Each group opens with an example prompt. ## Work items *Example prompt: “Summarize what changed on ENG-42 this week, including comments, state changes, and assignees.”* ### `workitem` — Work items Work items -- issues, tasks and epics. | Action | Required | Optional | Notes | | ------------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `list` | — | `project_id`, `pql`, `order_by`, `per_page`, `cursor`, `expand`, `fields`, `external_id`, `external_source` | omit project\_id to search the whole workspace; Read-only | | `list_archived` | `project_id` | `pql`, `order_by`, `per_page`, `cursor`, `expand`, `fields`, `external_id`, `external_source` | Read-only | | `retrieve` | `project_id`, `workitem_id` | `expand`, `fields`, `external_id`, `external_source`, `order_by` | Read-only | | `retrieve_by_identifier` | `workitem_identifier` | `expand`, `fields`, `external_id`, `external_source`, `order_by` | identifier is PROJECT-N, e.g. ENG-42; Read-only | | `search` | `query` | `expand`, `fields`, `external_id`, `external_source`, `order_by` | Read-only | | `count` | — | `project_id`, `pql`, `group_by`, `sub_group_by` | counts the whole workspace unless project\_id narrows it; Read-only | | `create` | `project_id`, `name` | `assignees`, `labels`, `type_id`, `point`, `description_html`, `description_stripped`, `priority`, `start_date`, `target_date`, `sort_order`, `is_draft`, `parent`, `state`, `estimate_point`, `external_source`, `external_id` | — | | `update` | `project_id`, `workitem_id` | `name`, `assignees`, `labels`, `type_id`, `point`, `description_html`, `description_stripped`, `priority`, `start_date`, `target_date`, `sort_order`, `is_draft`, `parent`, `state`, `estimate_point`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `project_id`, `workitem_id` | — | Destructive | | `archive` | `project_id`, `workitem_id` | `archive` | archive defaults to true; pass archive=false to unarchive. Only completed or cancelled items can be archived | | `manage_assignee` | `project_id`, `workitem_id` | `add_user_id`, `remove_user_id` | each takes one id or several; the list is merged, not replaced, and removals apply first | | `manage_label` | `project_id`, `workitem_id` | `add_label_id`, `remove_label_id` | each takes one id or several; the list is merged, not replaced, and removals apply first | **Notes:** priority: urgent, high, medium, low, none. UUID fields (assignees, labels, state, parent, type\_id) need UUIDs -- list the relevant resource first if you only have a name. description\_stripped is plain text and is wrapped into HTML on save; description\_html wins if both are given. fields is a sparse fieldset: use `project`, not project\_id, and `description_html`, not description. count group\_by and sub\_group\_by accept: state\_id, state\_\_group, priority, project\_id, type\_id, labels\_\_id, assignees\_\_id, issue\_module\_\_module\_id, release\_work\_items\_\_release\_id, cycle\_id, milestone\_id, created\_by, target\_date, start\_date. These are grouping keys only -- they are not PQL filter fields, and filtering on state\_\_group is rejected. ### `workitem_comment` — Work item comments Comments on a work item. | Action | Required | Optional | Notes | | ---------- | ------------------------------------------- | ---------------------------------------------------------- | ----------- | | `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | | `retrieve` | `project_id`, `workitem_id`, `comment_id` | — | Read-only | | `create` | `project_id`, `workitem_id`, `comment_html` | `access`, `external_source`, `external_id` | — | | `update` | `project_id`, `workitem_id`, `comment_id` | `comment_html`, `access`, `external_source`, `external_id` | — | | `delete` | `project_id`, `workitem_id`, `comment_id` | — | Destructive | **Notes:** comment\_html is HTML, e.g. '\

Looks good.\

'. access is INTERNAL or EXTERNAL. ### `workitem_activity` — Work item activity Change history for a work item. | Action | Required | Optional | Notes | | ---------- | ------------------------------------------ | -------------------- | --------- | | `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | | `retrieve` | `project_id`, `workitem_id`, `activity_id` | — | Read-only | ### `workitem_attachment` — Work item attachments Files attached to a work item. | Action | Required | Optional | Notes | | ----------------- | -------------------------------------------- | -------- | ----------------------------------------------------------------------------- | | `list` | `project_id`, `workitem_id` | — | Read-only | | `read` | `project_id`, `workitem_id`, `attachment_id` | — | returns images and text inline; use download\_url for anything else; Read-only | | `download_url` | `project_id`, `workitem_id`, `attachment_id` | — | Read-only | | `upload_from_url` | `project_id`, `workitem_id`, `url` | `name` | — | | `delete` | `project_id`, `workitem_id`, `attachment_id` | — | Destructive | **Notes:** read supports PNG/JPEG/GIF/WEBP up to 5 MB and TXT/MD/CSV/HTML/XML/YAML/JSON up to 1 MB. Get attachment\_id from the list action. upload\_from\_url fetches the file server-side, so the URL must be reachable without authentication and must not resolve to a private address. ### `workitem_link` — Work item links External links attached to a work item. | Action | Required | Optional | Notes | | ---------- | --------------------------------------------- | -------------------- | ----------- | | `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | | `retrieve` | `project_id`, `workitem_id`, `link_id` | — | Read-only | | `create` | `project_id`, `workitem_id`, `url` | — | — | | `update` | `project_id`, `workitem_id`, `link_id`, `url` | — | — | | `delete` | `project_id`, `workitem_id`, `link_id` | — | Destructive | ### `workitem_relation` — Work item relations Relations between work items, and the definitions that type them. | Action | Required | Optional | Notes | | ------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `list` | `project_id`, `workitem_id` | — | Read-only | | `create` | `project_id`, `workitem_id`, `workitem_ids` | `relation_type`, `relation_definition_id`, `relation_definition_label` | pass relation\_type for a dependency, or definition id + label for a custom relation | | `delete` | `project_id`, `workitem_id`, `related_workitem_id` | `is_dependency` | removes one relation; dependencies and custom relations are independent, so is\_dependency must match the kind that was created (default false); Destructive | | `list_definitions` | — | `is_default`, `is_active` | Read-only | | `create_definition` | `name` | `outward`, `inward`, `is_active`, `color` | — | | `update_definition` | `definition_id` | `name`, `outward`, `inward`, `is_active`, `color` | — | | `delete_definition` | `definition_id` | — | Destructive | **Notes:** Call list\_definitions first and match the user's wording to an entry. A built\_in\_dependencies value (blocking, blocked\_by, start\_before, start\_after, finish\_before, finish\_after) goes in relation\_type; a custom definition needs its id in relation\_definition\_id and the matched outward or inward label in relation\_definition\_label, which sets direction. ### `work_log` — Work logs Time logged against a work item. | Action | Required | Optional | Notes | | -------- | ------------------------------------------ | ------------------------- | ----------- | | `list` | `project_id`, `workitem_id` | `cursor`, `per_page` | Read-only | | `create` | `project_id`, `workitem_id`, `duration` | `description` | — | | `update` | `project_id`, `workitem_id`, `work_log_id` | `duration`, `description` | — | | `delete` | `project_id`, `workitem_id`, `work_log_id` | — | Destructive | **Notes:** duration is in minutes. ## Types, properties and estimates *Example prompt: “Create an Epic type for ENG, add a Customer impact property, and set up point estimates.”* ### `workitem_type` — Work item types Work item types, at project or workspace scope. | Action | Required | Optional | Notes | | ------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `list` | — | `project_id`, `cursor`, `per_page` | workspace scope when project\_id is omitted; Read-only | | `retrieve` | `workitem_type_id` | `project_id` | Read-only | | `resolve` | `project_id`, `name` | — | finds or creates a named type usable in the project; never duplicates | | `create` | `name` | `project_id`, `description`, `project_ids`, `is_active`, `external_source`, `external_id` | — | | `update` | `workitem_type_id` | `project_id`, `name`, `description`, `project_ids`, `is_active`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `workitem_type_id` | `project_id` | Destructive | | `import_to_project` | `project_id`, `workitem_type_ids` | — | links workspace types to a project | **Notes:** Omit project\_id to work at workspace scope. A type's id is the type\_id for `workitem create` and the workitem\_type\_id for `workitem_property list`. Prefer resolve over create when you just need a usable type such as Epic or Initiative: it handles both modes, matches exactly (case-sensitive, whitespace-stripped) and never duplicates. Where the workspace owns the vocabulary, creating a type on a project is rejected and importing is the only valid path -- resolve does that for you. ### `workitem_property` — Work item properties Custom work item properties and their options. | Action | Required | Optional | Notes | | ------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `list` | — | `project_id`, `workitem_type_id`, `cursor`, `per_page` | no ids lists every workspace property in one call -- the fast path for PQL; Read-only | | `retrieve` | `workitem_property_id` | `project_id`, `workitem_type_id` | Read-only | | `create` | `display_name`, `property_type` | `project_id`, `workitem_type_id`, `description`, `relation_type`, `is_required`, `is_multi`, `is_active`, `default_value`, `options`, `display_format`, `external_source`, `external_id` | — | | `update` | `workitem_property_id` | `project_id`, `workitem_type_id`, `display_name`, `property_type`, `description`, `relation_type`, `is_required`, `is_multi`, `is_active`, `default_value`, `display_format`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `workitem_property_id` | `project_id`, `workitem_type_id` | Destructive | | `manage_type_properties` | `workitem_type_id` | `project_id`, `attach_ids`, `detach_ids` | omit project\_id where the workspace owns types; detach removes the association only, it does not delete the property | | `list_options` | `property_id` | `project_id` | Read-only | | `retrieve_option` | `property_id`, `option_id` | `project_id` | Read-only | | `create_option` | `property_id`, `name` | `project_id`, `description`, `color`, `is_default`, `external_source`, `external_id` | — | | `update_option` | `property_id`, `option_id` | `project_id`, `name`, `description`, `color`, `is_default`, `external_source`, `external_id` | — | | `delete_option` | `property_id`, `option_id` | `project_id` | Destructive | | `get_value` | `project_id`, `workitem_id`, `property_id` | — | Read-only | | `set_value` | `project_id`, `workitem_id`, `property_id`, `value` | `external_source`, `external_id` | upsert; for a multi-value property this replaces every existing value | | `delete_value` | `project_id`, `workitem_id`, `property_id` | — | Destructive | **Notes:** property\_type is one of: TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE, FORMULA. relation\_type (for RELATION properties) is one of: ISSUE, USER, RELEASE, RICH\_TEXT. A property id is what goes in a PQL cf\["\"] filter; for OPTION properties the value is an option id. options takes a JSON array of {"name", "color", "is\_default"} objects. display\_format is required by TEXT (single-line, multi-line, readonly) and DATETIME (MMM dd, yyyy, dd/MM/yyyy, MM/dd/yyyy, yyyy/MM/dd) properties. A property lives with its type: where the workspace owns types, pass workitem\_type\_id without project\_id and it is created in the workspace catalogue and associated for you. list resolves scope in this order: project\_id + workitem\_type\_id is type-scoped (falling back to project-flat then workspace when empty), project\_id alone is every property in the project, and neither is every workspace property. To filter by property name in PQL, call list with no ids -- one workspace-wide fetch beats iterating types -- then match display\_name in memory to get the id for a cf\[] condition. The \*\_value actions read and write a property on one work item: pass value in the type the property expects -- TEXT/URL/EMAIL/FILE as a string; DATETIME as a YYYY-MM-DD or YYYY-MM-DD HH:MM:SS string; DECIMAL as a number; BOOLEAN as true or false; OPTION and RELATION as an option or record id string, or an array of them when the property is multi-value. Send the value's own type, not a stringified form: "007" stays the text 007, whereas 7 is the number. ### `project_estimate` — Project estimates A project's estimate system and its points. | Action | Required | Optional | Notes | | --------------- | ------------------------------------------------ | -------------------------------------------------------------------- | --------------------------------------------- | | `retrieve` | `project_id` | — | a project has at most one estimate; Read-only | | `create` | `project_id`, `name` | `type`, `description`, `last_used`, `external_source`, `external_id` | — | | `update` | `project_id` | `name`, `description`, `external_source`, `external_id` | — | | `delete` | `project_id` | — | Destructive | | `link` | `project_id`, `estimate_id` | — | makes that estimate the project's active one | | `list_points` | `project_id`, `estimate_id` | — | Read-only | | `create_points` | `project_id`, `estimate_id`, `points` | — | — | | `update_point` | `project_id`, `estimate_id`, `estimate_point_id` | `value`, `key`, `description`, `external_source`, `external_id` | — | | `delete_point` | `project_id`, `estimate_id`, `estimate_point_id` | — | Destructive | **Notes:** type is one of: categories, points, time. A point's `value` is its display label ("5", "XL") and its `key` is the sort order. points takes a JSON array such as \[{"value": "1", "key": 0}]. To set a work item's estimate: retrieve to get the estimate\_id, list\_points to see the available values, then pass the chosen point's id to `workitem update` as estimate\_point. ## Planning *Example prompt: “Move unfinished work from Sprint 14 to Sprint 15, then count it by priority.”* ### `cycle` — Cycles Cycles (time-boxed iterations) in a project. | Action | Required | Optional | Notes | | -------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `list` | `project_id` | `archived`, `status`, `cursor`, `per_page`, `order_by` | Read-only | | `retrieve` | `project_id`, `cycle_id` | — | Read-only | | `create` | `project_id`, `name`, `owned_by` | `description`, `start_date`, `end_date`, `timezone`, `external_source`, `external_id` | — | | `update` | `project_id`, `cycle_id` | `name`, `description`, `start_date`, `end_date`, `owned_by`, `timezone`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `project_id`, `cycle_id` | — | Destructive | | `list_workitems` | `project_id`, `cycle_id` | `pql`, `order_by`, `cursor`, `per_page`, `expand`, `fields` | Read-only | | `manage_workitems` | `project_id`, `cycle_id` | `add_ids`, `remove_ids` | pass at least one of add\_ids or remove\_ids; returns nothing, read back with list\_workitems | | `transfer_workitems` | `project_id`, `cycle_id`, `new_cycle_id` | — | moves everything to new\_cycle\_id | | `complete` | `project_id`, `cycle_id` | — | sets end\_date to today | | `archive` | `project_id`, `cycle_id` | — | ends the cycle first if it is still running | | `unarchive` | `project_id`, `cycle_id` | — | — | **Notes:** status filters active cycles: current, upcoming, completed, draft, incomplete; it is ignored when archived is true. Dates are ISO 8601 (YYYY-MM-DD). owned\_by is a member id. Optional Plane Query Language (PQL) filter. Examples: `priority = "urgent" AND assignee = currentUser()`, `stateGroup IN openStates() AND isOverdue()`. UUID fields (project, assignee, state, label, cycle, module, type, milestone, createdBy) need UUIDs — resolve a name to its UUID first if you only have a name or short identifier (e.g. `LSS` → `project list` and match `identifier` to get `id`). Call `get_pql_reference` for full PQL syntax before composing complex queries. ### `module` — Modules Modules (feature groupings) in a project. | Action | Required | Optional | Notes | | ------------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `list` | `project_id` | `archived`, `cursor`, `per_page`, `order_by` | Read-only | | `retrieve` | `project_id`, `module_id` | — | Read-only | | `create` | `project_id`, `name` | `description`, `start_date`, `target_date`, `status`, `lead`, `members`, `external_source`, `external_id` | — | | `update` | `project_id`, `module_id` | `name`, `description`, `start_date`, `target_date`, `status`, `lead`, `members`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `project_id`, `module_id` | — | Destructive | | `list_workitems` | `project_id`, `module_id` | `pql`, `order_by`, `cursor`, `per_page`, `expand`, `fields` | Read-only | | `manage_workitems` | `project_id`, `module_id` | `add_ids`, `remove_ids` | pass at least one of add\_ids or remove\_ids; returns nothing, read back with list\_workitems | | `archive` | `project_id`, `module_id` | — | — | | `unarchive` | `project_id`, `module_id` | — | — | **Notes:** status is one of: backlog, planned, in-progress, paused, completed, cancelled. Dates are ISO 8601 (YYYY-MM-DD). lead and members are member ids. Optional Plane Query Language (PQL) filter. Examples: `priority = "urgent" AND assignee = currentUser()`, `stateGroup IN openStates() AND isOverdue()`. UUID fields (project, assignee, state, label, cycle, module, type, milestone, createdBy) need UUIDs — resolve a name to its UUID first if you only have a name or short identifier (e.g. `LSS` → `project list` and match `identifier` to get `id`). Call `get_pql_reference` for full PQL syntax before composing complex queries. ### `milestone` — Milestones Milestones within a project. | Action | Required | Optional | Notes | | ------------------ | ---------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `list` | `project_id` | `cursor`, `per_page` | Read-only | | `retrieve` | `project_id`, `milestone_id` | — | Read-only | | `create` | `project_id`, `title` | `target_date`, `external_source`, `external_id` | — | | `update` | `project_id`, `milestone_id` | `title`, `target_date`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `project_id`, `milestone_id` | — | Destructive | | `list_workitems` | `project_id`, `milestone_id` | `cursor`, `per_page` | Read-only | | `manage_workitems` | `project_id`, `milestone_id` | `add_ids`, `remove_ids` | pass at least one of add\_ids or remove\_ids; returns nothing, read back with list\_workitems | **Notes:** target\_date is ISO 8601 (YYYY-MM-DD). add\_ids and remove\_ids take work item UUIDs. ### `initiative` — Initiatives Workspace initiatives. | Action | Required | Optional | Notes | | ----------------- | ------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------- | | `list` | — | — | returns every initiative; this endpoint does not paginate; Read-only | | `retrieve` | `initiative_id` | — | Read-only | | `create` | `name` | `description_html`, `start_date`, `end_date`, `state`, `lead` | — | | `update` | `initiative_id` | `name`, `description_html`, `start_date`, `end_date`, `state`, `lead` | only the fields you pass are changed | | `delete` | `initiative_id` | — | Destructive | | `list_projects` | `initiative_id` | `cursor`, `per_page` | Read-only | | `add_projects` | `initiative_id`, `project_ids` | — | returns nothing, read back with list\_projects | | `remove_projects` | `initiative_id`, `project_ids` | — | returns nothing, read back with list\_projects; Destructive | **Notes:** state is one of: DRAFT, PLANNED, ACTIVE, COMPLETED, CLOSED. Dates are ISO 8601 (YYYY-MM-DD). lead is a member id. project\_ids takes project UUIDs. ## Releases *Example prompt: “Create release v1.8.0, add ENG-40 through ENG-45, and draft its changelog.”* ### `release` — Releases Releases in the workspace. | Action | Required | Optional | Notes | | ------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `list` | — | `cursor`, `per_page` | Read-only | | `retrieve` | `release_id` | — | Read-only | | `create` | `name` | `description_html`, `status`, `release_date`, `target_date`, `tag_id`, `lead_id`, `is_prerelease`, `external_source`, `external_id` | — | | `update` | `release_id` | `name`, `description_html`, `status`, `release_date`, `target_date`, `tag_id`, `lead_id`, `is_prerelease` | only the fields you pass are changed | | `delete` | `release_id` | — | Destructive | | `get_changelog` | `release_id` | — | Read-only | | `update_changelog` | `release_id` | `description_html`, `description_stripped` | — | | `list_workitems` | `release_id` | `cursor`, `per_page` | Read-only | | `manage_workitems` | `release_id` | `add_ids`, `remove_ids` | pass at least one of add\_ids or remove\_ids; returns nothing, read back with list\_workitems | **Notes:** status is one of: unreleased, released, cancelled, defaulting to unreleased. release\_date is what the Plane UI labels "Target date" (YYYY-MM-DD); target\_date is a separate stored date that the UI does not show. tag\_id comes from `release_tag list`, lead\_id from `member list_workspace`. For the changelog pass description\_html, or description\_stripped for plain text. A changelog is created empty with the release, so get\_changelog always returns one. ### `release_tag` — Release tags Release tags (version markers). | Action | Required | Optional | Notes | | ---------- | --------- | -------------------------------------------------- | ------------------------------------ | | `list` | — | `cursor`, `per_page` | Read-only | | `retrieve` | `tag_id` | — | Read-only | | `create` | `version` | `description`, `commit_hash`, `git_tag` | — | | `update` | `tag_id` | `version`, `description`, `commit_hash`, `git_tag` | only the fields you pass are changed | | `delete` | `tag_id` | — | Destructive | **Notes:** version is a version string such as "v1.2.0". A tag id is what release takes as tag\_id. ### `release_label` — Release labels Release labels, workspace palette and per release. | Action | Required | Optional | Notes | | -------- | ------------------------- | ---------------------------------- | ----------------------------------------------------------- | | `list` | — | `release_id`, `cursor`, `per_page` | the workspace palette unless release\_id is given; Read-only | | `create` | `name` | `color`, `sort_order` | adds to the workspace palette | | `update` | `label_id` | `name`, `color`, `sort_order` | — | | `delete` | `label_id` | — | removes it from the palette entirely; Destructive | | `attach` | `release_id`, `label_ids` | — | returns nothing, read back with list | | `detach` | `release_id`, `label_ids` | — | returns nothing, read back with list; Destructive | **Notes:** color is a hex code such as #4E5355. label\_ids takes palette label ids. Detaching a label leaves it in the palette; delete removes it for everyone. ## Projects and workspace *Example prompt: “List my active projects and show the work currently assigned to me.”* ### `project` — Projects Projects in a workspace. | Action | Required | Optional | Notes | | ----------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `list` | — | `cursor`, `per_page`, `order_by` | trimmed fields; use retrieve for full detail; Read-only | | `retrieve` | `project_id` | — | Read-only | | `create` | `name`, `identifier` | `description`, `project_lead`, `default_assignee`, `emoji`, `cover_image`, `timezone`, `archive_in`, `close_in`, `external_source`, `external_id` | — | | `update` | `project_id` | `name`, `description`, `identifier`, `project_lead`, `default_assignee`, `emoji`, `cover_image`, `network`, `timezone`, `archive_in`, `close_in`, `default_state`, `estimate`, `is_time_tracking_enabled`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `project_id` | — | Destructive | | `archive` | `project_id` | — | — | | `unarchive` | `project_id` | — | — | | `worklog_summary` | `project_id` | — | Read-only | | `get_features` | `project_id` | — | Read-only | | `update_features` | `project_id` | `modules`, `cycles`, `views`, `pages`, `intakes`, `workitem_types`, `epics`, `parallel_cycles`, `project_updates`, `workflows` | toggles project features on or off | **Notes:** identifier is the short work item prefix, such as ENG. network is 0 for secret or 2 for public. project\_lead and default\_assignee are member ids -- get them from `member list_workspace`. Feature toggles are booleans; omitted ones are left as they are. ### `state` — Workflow states Workflow states within a project. | Action | Required | Optional | Notes | | ---------- | ----------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------ | | `list` | `project_id` | `cursor`, `per_page` | Read-only | | `retrieve` | `project_id`, `state_id` | — | Read-only | | `create` | `project_id`, `name`, `color` | `description`, `sequence`, `group`, `is_triage`, `default`, `external_source`, `external_id` | — | | `update` | `project_id`, `state_id` | `name`, `color`, `description`, `sequence`, `group`, `is_triage`, `default` | only the fields you pass are changed | | `delete` | `project_id`, `state_id` | — | Destructive | **Notes:** group is one of: backlog, unstarted, started, completed, cancelled, triage. color is a hex code such as #EF4444. ### `label` — Labels Labels within a project. | Action | Required | Optional | Notes | | ---------- | ------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------ | | `list` | `project_id` | `cursor`, `per_page` | Read-only | | `retrieve` | `project_id`, `label_id` | — | Read-only | | `create` | `project_id`, `name` | `color`, `description`, `parent`, `sort_order`, `external_source`, `external_id` | — | | `update` | `project_id`, `label_id` | `name`, `color`, `description`, `parent`, `sort_order`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `project_id`, `label_id` | — | Destructive | **Notes:** color is a hex code such as #EF4444. parent is the UUID of another label, for nesting. ### `member` — Members and roles Workspace and project members, and role definitions. | Action | Required | Optional | Notes | | ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | | `me` | — | — | the authenticated user; Read-only | | `list_workspace` | — | `first_name`, `last_name`, `email`, `display_name`, `role_slug`, `is_active`, `is_bot`, `cursor`, `per_page`, `order_by` | name filters match case-insensitively and combine with AND; Read-only | | `list_project` | `project_id` | — | Read-only | | `list_roles` | — | `namespace`, `cursor`, `per_page` | Read-only | | `retrieve_role` | `role_id` | — | Read-only | **Notes:** namespace is 'workspace' (Owner/Admin/Member/Guest) or 'project' (Admin/Contributor/Commenter/Guest); omit for both. A role slug is stable but not globally unique -- key on (namespace, slug). ### `workspace` — Workspace settings Workspace-level feature flags. | Action | Required | Optional | Notes | | ----------------- | -------- | --------------------------------------------------------------------- | -------------------------------------------------- | | `get_features` | — | — | feature flags for the current workspace; Read-only | | `update_features` | — | `project_grouping`, `initiatives`, `teams`, `customers`, `wiki`, `pi` | only the flags you pass are changed | **Notes:** For a project's feature flags use `project get_features` and `project update_features`. ### `intake` — Intake queue The intake (triage) queue for a project. | Action | Required | Optional | Notes | | ---------- | --------------------------- | ------------------------------------------------------------------ | ------------------------------------- | | `list` | `project_id` | `cursor`, `per_page` | Read-only | | `retrieve` | `project_id`, `workitem_id` | — | Read-only | | `create` | `project_id`, `name` | `description_html`, `priority` | — | | `update` | `project_id`, `workitem_id` | `status`, `snoozed_till`, `duplicate_to`, `source`, `source_email` | pass status to make a triage decision | | `delete` | `project_id`, `workitem_id` | — | Destructive | **Notes:** workitem\_id is the `issue` field of an intake record, not the record's own id. status: -2 pending, -1 declined, 0 snoozed (needs snoozed\_till), 1 accepted, 2 duplicate (needs duplicate\_to). priority is one of: urgent, high, medium, low, none. ### `page` — Pages Pages at workspace or project scope. | Action | Required | Optional | Notes | | ---------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `list` | — | `project_id`, `cursor`, `per_page` | workspace pages unless project\_id is given; Read-only | | `retrieve` | `page_id` | `project_id` | Read-only | | `create` | `name`, `description_html` | `project_id`, `access`, `color`, `is_locked`, `external_source`, `external_id` | — | | `list_workitem_pages` | `project_id`, `workitem_id` | — | Read-only | | `attach_to_workitem` | `project_id`, `workitem_id`, `page_id` | — | — | | `detach_from_workitem` | `project_id`, `workitem_id`, `workitem_page_id` | — | workitem\_page\_id is the link id from list\_workitem\_pages, not the page id; Destructive | **Notes:** description\_html is the page body as HTML. access is the page access level. Omit project\_id to work with workspace-level pages. ## Customers *Example prompt: “Create Acme as a customer and link its checkout request to the relevant work items.”* ### `customer` — Customers Customers in the workspace. | Action | Required | Optional | Notes | | ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `list` | — | `query`, `cursor`, `per_page` | Read-only | | `retrieve` | `customer_id` | — | Read-only | | `create` | `name` | `description_html`, `email`, `website_url`, `domain`, `employees`, `stage`, `contract_status`, `revenue`, `external_source`, `external_id` | upsert: matches on external\_source + external\_id, else on name, so it never duplicates | | `update` | `customer_id` | `name`, `description_html`, `email`, `website_url`, `domain`, `employees`, `stage`, `contract_status`, `revenue`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | — | `customer_id`, `external_source`, `external_id` | address by customer\_id, or by external\_source plus external\_id; Destructive | | `list_workitems` | `customer_id` | `customer_request_id`, `search` | Read-only | | `manage_workitems` | `customer_id` | `link_ids`, `unlink_ids`, `customer_request_id` | pass at least one of link\_ids or unlink\_ids; returns nothing, read back with list\_workitems | **Notes:** domain is the customer's industry, shown as "Industry" in Plane -- the website goes in website\_url. stage renders as one of: lead, sales\_qualified\_lead, contract\_negotiation, closed\_won, closed\_lost. contract\_status renders as one of: active, pre\_contract, signed, inactive. Both are stored free-form; anything else is kept but not displayed. revenue is annual revenue as a string. ### `customer_request` — Customer requests Requests raised by a customer. | Action | Required | Optional | Notes | | ---------- | --------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------- | | `list` | `customer_id` | `query`, `cursor`, `per_page` | Read-only | | `retrieve` | `customer_id`, `request_id` | — | Read-only | | `create` | `customer_id`, `name` | `description_html`, `link`, `workitem_ids` | workitem\_ids can only be set here; change links afterwards with customer manage\_workitems | | `update` | `customer_id`, `request_id` | `name`, `description_html`, `link` | only the fields you pass are changed | | `delete` | `customer_id`, `request_id` | — | Destructive | **Notes:** link is a URL associated with the request. workitem\_ids is never echoed back -- read the links with `customer list_workitems`. ### `customer_property` — Customer properties Custom properties on customers. | Action | Required | Optional | Notes | | ------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | | `list` | — | `cursor`, `per_page` | Read-only | | `retrieve` | `property_id` | — | Read-only | | `create` | `display_name`, `property_type` | `relation_type`, `description`, `is_required`, `is_multi`, `is_active`, `default_value`, `options`, `display_format`, `external_source`, `external_id` | — | | `update` | `property_id` | `display_name`, `relation_type`, `description`, `is_required`, `is_multi`, `is_active`, `default_value`, `options`, `external_source`, `external_id` | only the fields you pass are changed | | `delete` | `property_id` | — | Destructive | | `get_values` | `customer_id` | `property_id` | omit property\_id to read them all; Read-only | | `set_values` | `customer_id`, `values` | — | replaces the values of the properties named; others keep theirs | **Notes:** display\_name is the user-facing label and must be unique in the workspace -- the stored name is derived from it. property\_type is one of: TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE, FORMULA. relation\_type (required for RELATION) is one of: ISSUE, USER, RELEASE, RICH\_TEXT. display\_format is required by TEXT (single-line, multi-line, readonly) and DATETIME (MMM dd, yyyy, dd/MM/yyyy, MM/dd/yyyy, yyyy/MM/dd). options takes a JSON array of {"name", "description", "is\_default"} objects. values takes a JSON object of property id to a list of strings, e.g. {"\": \["Enterprise"]} -- every value is a string whatever the property type, and a single-item list unless is\_multi. ## Query *Example prompt: “Show the PQL syntax for overdue, in-progress work assigned to the current user.”* ### `get_pql_reference` — PQL reference Plane Query Language (PQL) syntax reference. Call this before composing a `pql` filter for the workitem list, list\_archived or count actions. Takes `detail`: `full` (default) or `brief`. This tool has no `action` parameter. **Notes:** detail 'full' gives operators, functions, common mistakes and worked examples; 'brief' gives the compact field and operator quick reference. ## Retired tool names Plane MCP server 0.3.0 consolidated 177 per-operation tools into 28 resource tools. Of those 177 names, the 169 aliases below still resolve, stay hidden from tool listings, and accept their original parameter names; `get_pql_reference` is unchanged; and seven cannot be mapped to one action and instead return a message naming the replacement. The server logs each alias resolution. ### Names without an alias | Retired name | Use instead | | ---------------------------- | ----------------------------------------------------------- | | `manage_customer_work_items` | `customer manage_workitems` with `link_ids` or `unlink_ids` | | `manage_cycle_archive` | `cycle archive` or `cycle unarchive` | | `manage_initiative_projects` | `initiative add_projects` or `initiative remove_projects` | | `manage_module_archive` | `module archive` or `module unarchive` | | `manage_project_archive` | `project archive` or `project unarchive` | | `manage_release_labels` | `release_label attach` or `release_label detach` | | `manage_release_work_items` | `release manage_workitems` with `add_ids` or `remove_ids` | ### Alias table ::: details Show all 169 aliases #### `customer` | Retired name | Now | | -------------------------- | ------------------------- | | `list_customers` | `customer list` | | `retrieve_customer` | `customer retrieve` | | `create_customer` | `customer create` | | `update_customer` | `customer update` | | `delete_customer` | `customer delete` | | `list_customer_work_items` | `customer list_workitems` | #### `customer_property` | Retired name | Now | | ------------------------------ | ------------------------------ | | `list_customer_properties` | `customer_property list` | | `retrieve_customer_property` | `customer_property retrieve` | | `create_customer_property` | `customer_property create` | | `update_customer_property` | `customer_property update` | | `delete_customer_property` | `customer_property delete` | | `get_customer_property_values` | `customer_property get_values` | | `set_customer_property_values` | `customer_property set_values` | #### `customer_request` | Retired name | Now | | --------------------------- | --------------------------- | | `list_customer_requests` | `customer_request list` | | `retrieve_customer_request` | `customer_request retrieve` | | `create_customer_request` | `customer_request create` | | `update_customer_request` | `customer_request update` | | `delete_customer_request` | `customer_request delete` | #### `cycle` | Retired name | Now | | --------------------------- | -------------------------- | | `list_cycles` | `cycle list` | | `retrieve_cycle` | `cycle retrieve` | | `create_cycle` | `cycle create` | | `update_cycle` | `cycle update` | | `delete_cycle` | `cycle delete` | | `list_cycle_work_items` | `cycle list_workitems` | | `manage_cycle_work_items` | `cycle manage_workitems` | | `transfer_cycle_work_items` | `cycle transfer_workitems` | | `complete_cycle` | `cycle complete` | #### `initiative` | Retired name | Now | | -------------------------- | -------------------------- | | `list_initiatives` | `initiative list` | | `retrieve_initiative` | `initiative retrieve` | | `create_initiative` | `initiative create` | | `update_initiative` | `initiative update` | | `delete_initiative` | `initiative delete` | | `list_initiative_projects` | `initiative list_projects` | #### `intake` | Retired name | Now | | --------------------------- | ----------------- | | `list_intake_work_items` | `intake list` | | `retrieve_intake_work_item` | `intake retrieve` | | `create_intake_work_item` | `intake create` | | `update_intake_work_item` | `intake update` | | `delete_intake_work_item` | `intake delete` | #### `label` | Retired name | Now | | ---------------- | ---------------- | | `list_labels` | `label list` | | `retrieve_label` | `label retrieve` | | `create_label` | `label create` | | `update_label` | `label update` | | `delete_label` | `label delete` | #### `member` | Retired name | Now | | ----------------------- | ----------------------- | | `get_me` | `member me` | | `get_workspace_members` | `member list_workspace` | | `get_project_members` | `member list_project` | | `list_roles` | `member list_roles` | | `retrieve_role` | `member retrieve_role` | #### `milestone` | Retired name | Now | | ----------------------------- | ---------------------------- | | `list_milestones` | `milestone list` | | `retrieve_milestone` | `milestone retrieve` | | `create_milestone` | `milestone create` | | `update_milestone` | `milestone update` | | `delete_milestone` | `milestone delete` | | `list_milestone_work_items` | `milestone list_workitems` | | `manage_milestone_work_items` | `milestone manage_workitems` | #### `module` | Retired name | Now | | -------------------------- | ------------------------- | | `list_modules` | `module list` | | `retrieve_module` | `module retrieve` | | `create_module` | `module create` | | `update_module` | `module update` | | `delete_module` | `module delete` | | `list_module_work_items` | `module list_workitems` | | `manage_module_work_items` | `module manage_workitems` | #### `page` | Retired name | Now | | ---------------------------- | --------------------------- | | `list_pages` | `page list` | | `retrieve_page` | `page retrieve` | | `create_page` | `page create` | | `list_work_item_pages` | `page list_workitem_pages` | | `attach_page_to_work_item` | `page attach_to_workitem` | | `detach_page_from_work_item` | `page detach_from_workitem` | #### `project` | Retired name | Now | | ----------------------------- | ------------------------- | | `list_projects` | `project list` | | `retrieve_project` | `project retrieve` | | `create_project` | `project create` | | `update_project` | `project update` | | `delete_project` | `project delete` | | `get_project_worklog_summary` | `project worklog_summary` | | `update_project_features` | `project update_features` | #### `project_estimate` | Retired name | Now | | -------------------------------- | -------------------------------- | | `get_project_estimate` | `project_estimate retrieve` | | `create_project_estimate` | `project_estimate create` | | `update_project_estimate` | `project_estimate update` | | `delete_project_estimate` | `project_estimate delete` | | `link_estimate_to_project` | `project_estimate link` | | `list_project_estimate_points` | `project_estimate list_points` | | `create_project_estimate_points` | `project_estimate create_points` | | `update_project_estimate_point` | `project_estimate update_point` | | `delete_project_estimate_point` | `project_estimate delete_point` | #### `release` | Retired name | Now | | -------------------------- | -------------------------- | | `list_releases` | `release list` | | `retrieve_release` | `release retrieve` | | `create_release` | `release create` | | `update_release` | `release update` | | `delete_release` | `release delete` | | `get_release_changelog` | `release get_changelog` | | `update_release_changelog` | `release update_changelog` | | `list_release_work_items` | `release list_workitems` | #### `release_label` | Retired name | Now | | ---------------------- | ---------------------- | | `list_release_labels` | `release_label list` | | `create_release_label` | `release_label create` | | `update_release_label` | `release_label update` | | `delete_release_label` | `release_label delete` | #### `release_tag` | Retired name | Now | | ---------------------- | ---------------------- | | `list_release_tags` | `release_tag list` | | `retrieve_release_tag` | `release_tag retrieve` | | `create_release_tag` | `release_tag create` | | `update_release_tag` | `release_tag update` | | `delete_release_tag` | `release_tag delete` | #### `state` | Retired name | Now | | ---------------- | ---------------- | | `list_states` | `state list` | | `retrieve_state` | `state retrieve` | | `create_state` | `state create` | | `update_state` | `state update` | | `delete_state` | `state delete` | #### `work_log` | Retired name | Now | | ----------------- | ----------------- | | `list_work_logs` | `work_log list` | | `create_work_log` | `work_log create` | | `update_work_log` | `work_log update` | | `delete_work_log` | `work_log delete` | #### `workitem` | Retired name | Now | | ---------------------------------- | --------------------------------- | | `list_work_items` | `workitem list` | | `list_archived_work_items` | `workitem list_archived` | | `retrieve_work_item` | `workitem retrieve` | | `retrieve_work_item_by_identifier` | `workitem retrieve_by_identifier` | | `search_work_items` | `workitem search` | | `count_work_items` | `workitem count` | | `create_work_item` | `workitem create` | | `update_work_item` | `workitem update` | | `delete_work_item` | `workitem delete` | | `manage_work_item_archive` | `workitem archive` | | `manage_work_item_assignee` | `workitem manage_assignee` | | `manage_work_item_label` | `workitem manage_label` | #### `workitem_activity` | Retired name | Now | | ----------------------------- | ---------------------------- | | `list_work_item_activities` | `workitem_activity list` | | `retrieve_work_item_activity` | `workitem_activity retrieve` | #### `workitem_attachment` | Retired name | Now | | --------------------------------------- | ------------------------------------- | | `list_work_item_attachments` | `workitem_attachment list` | | `read_work_item_attachment` | `workitem_attachment read` | | `get_work_item_attachment_download_url` | `workitem_attachment download_url` | | `upload_work_item_attachment_from_url` | `workitem_attachment upload_from_url` | | `delete_work_item_attachment` | `workitem_attachment delete` | #### `workitem_comment` | Retired name | Now | | ---------------------------- | --------------------------- | | `list_work_item_comments` | `workitem_comment list` | | `retrieve_work_item_comment` | `workitem_comment retrieve` | | `create_work_item_comment` | `workitem_comment create` | | `update_work_item_comment` | `workitem_comment update` | | `delete_work_item_comment` | `workitem_comment delete` | #### `workitem_link` | Retired name | Now | | ------------------------- | ------------------------ | | `list_work_item_links` | `workitem_link list` | | `retrieve_work_item_link` | `workitem_link retrieve` | | `create_work_item_link` | `workitem_link create` | | `update_work_item_link` | `workitem_link update` | | `delete_work_item_link` | `workitem_link delete` | #### `workitem_property` | Retired name | Now | | ------------------------------------ | ------------------------------------------ | | `list_work_item_properties` | `workitem_property list` | | `retrieve_work_item_property` | `workitem_property retrieve` | | `create_work_item_property` | `workitem_property create` | | `update_work_item_property` | `workitem_property update` | | `delete_work_item_property` | `workitem_property delete` | | `manage_work_item_type_properties` | `workitem_property manage_type_properties` | | `list_work_item_property_options` | `workitem_property list_options` | | `retrieve_work_item_property_option` | `workitem_property retrieve_option` | | `create_work_item_property_option` | `workitem_property create_option` | | `update_work_item_property_option` | `workitem_property update_option` | | `delete_work_item_property_option` | `workitem_property delete_option` | | `get_work_item_property_value` | `workitem_property get_value` | | `set_work_item_property_value` | `workitem_property set_value` | | `delete_work_item_property_value` | `workitem_property delete_value` | #### `workitem_relation` | Retired name | Now | | -------------------------------------- | ------------------------------------- | | `list_work_item_relations` | `workitem_relation list` | | `create_work_item_relation` | `workitem_relation create` | | `remove_work_item_relation` | `workitem_relation delete` | | `list_work_item_relation_definitions` | `workitem_relation list_definitions` | | `create_work_item_relation_definition` | `workitem_relation create_definition` | | `update_work_item_relation_definition` | `workitem_relation update_definition` | | `delete_work_item_relation_definition` | `workitem_relation delete_definition` | #### `workitem_type` | Retired name | Now | | ----------------------------------- | --------------------------------- | | `list_work_item_types` | `workitem_type list` | | `retrieve_work_item_type` | `workitem_type retrieve` | | `resolve_work_item_type` | `workitem_type resolve` | | `create_work_item_type` | `workitem_type create` | | `update_work_item_type` | `workitem_type update` | | `delete_work_item_type` | `workitem_type delete` | | `import_work_item_types_to_project` | `workitem_type import_to_project` | #### `workspace` | Retired name | Now | | --------------------------- | --------------------------- | | `get_features` | `workspace get_features` | | `update_workspace_features` | `workspace update_features` | ::: ## See also * [Set up the MCP server](/dev-tools/mcp-server) * [Self-host the MCP server](/dev-tools/mcp-server-self-host) * [Tool architecture and extension guide](https://github.com/makeplane/plane-mcp-server/blob/v0.3.0/plane_mcp/tools/README.md) --- --- url: 'https://developers.plane.so/dev-tools/plane-compose.html' description: >- Define Plane projects, workflows, and work items in YAML files. Version control your project structure and sync bidirectionally with Plane using the command line. --- # 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. ## Prerequisites * Python 3.10 or later. Verify with `python3 --version`. * pipx. Plane Compose is distributed as a Python package and pipx is the recommended installer. If you do not have pipx: ```bash # macOS brew install pipx pipx ensurepath # Linux / Windows (via pip) pip install --user pipx pipx ensurepath ``` * A Plane account with access to at least one workspace. * An API token. You will need it during authentication. ## Install Plane Compose ```bash pipx install plane-compose ``` The package is published at . 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//settings/account/api-tokens/` * **Workspace** - your workspace slug (the URL segment after `app.plane.so/`) * **Connection name** - a label for this connection (e.g. `personal`, `work`, `staging`), leave blank for default; used to identify it in `plane auth list-connections` Verify it worked: ```bash plane auth list-connections ``` To add credentials for a second workspace or a different Plane instance, run `plane auth login` again. Each login creates a separate connection. Link a workspace to a specific connection: ```bash plane auth connect-workspace --connection ``` Remove a connection: ```bash plane auth logout ``` ## Project operations ### Start a new project ```bash plane init --workspace ``` This creates the project directory with the given key and generates the full file structure inside it. To start from a template instead of the default schema: ```bash plane init --workspace --template ./templates/standard # or a Git URL: plane init --workspace \ --template https://github.com///templates/ ``` Before pushing, edit your schema files to define your project's structure. See the **Define your project schema** guide below. Then push the schema to create the project in Plane: ```bash cd plane schema push ``` `plane.yaml` is updated with the project UUID after this runs. ### Define your project schema After `plane init`, the `schema/` directory contains default files. Edit them to match your project's actual structure before pushing. **Edit the files in this order** - each file can only reference names defined in the files before it. 1. **Define your states** Open `schema/states.yaml`. Every state belongs to one of five groups: `backlog`, `unstarted`, `started`, `completed`, `cancelled`. Exactly one state should have `is_default: true`. ```yaml states: Backlog: group: backlog color: "#858585" is_default: true allow_issue_creation: true In Progress: group: started color: "#f59e0b" Done: group: completed color: "#22c55e" Cancelled: group: cancelled color: "#ef4444" ``` See the `schema/states.yaml` reference for all fields. 2. **Define your labels** Open `schema/labels.yaml`. Labels are flat - no nesting. ```yaml labels: - name: backend color: "#3b82f6" - name: frontend color: "#8b5cf6" ``` 3. **Define your workflows** Open `schema/workflows.yaml`. A workflow ties a set of states together and optionally restricts which transitions are allowed. Use only state names defined in `schema/states.yaml`. ```yaml workflows: default: is_active: true work_item_types: - Story - Bug states: - Backlog - In Progress - Done - Cancelled ``` Without a `transitions` block, any state change is permitted. See the `schema/workflows.yaml` reference for transition and approval syntax. 4. **Define your work item types** Open `schema/types.yaml`. Each type references a workflow by name. Use only workflow names defined in `schema/workflows.yaml`. ```yaml work_item_types: Story: description: A unit of user-facing work workflow: default is_epic: false Bug: description: A defect requiring correction workflow: default ``` To add custom properties to a type, see the `schema/types.yaml` reference. 5. **Set your default type in `plane.yaml`** Open `plane.yaml` and set `defaults.type` to the type name used when a work item does not specify one: ```yaml defaults: type: Story workflow: default ``` 6. **Toggle features** Open `schema/features.yaml` and disable features your project does not need. Disabling a feature hides it from the Plane UI and causes Plane Compose to skip its corresponding work file on push and pull. ```yaml features: cycles: true modules: true pages: false ``` 7. **Validate** Check for errors without making any API calls: ```bash plane schema validate ``` Fix any reported errors before proceeding. 8. **Push the schema** 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. This creates the project in Plane if it does not exist yet and pushes your types, states, labels, and workflows. `plane.yaml` is updated with the project UUID after this runs. ### Clone an existing project Use this when the project already exists in Plane and you want to manage it locally. ```bash plane clone --workspace ``` :::info Use `--connection ` if the workspace slug is common across multiple connections. ::: This downloads `plane.yaml`, all schema files, and all work files into a new local directory. The schema files will reflect what is currently in Plane, you can edit them and push changes back. ### Push changes to Plane `plane push` pushes everything - schema and work files - in the correct order. `plane schema push` pushes schema only and is equivalent to `plane push --schema-only`. Use `plane schema push` when you are setting up a project for the first time and have no work items yet, or when you want to push schema changes without touching work items. Use `plane push` for all other cases. 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///templates/ --dry-run ``` Apply the upgrade: ```bash plane upgrade --template https://github.com///templates/ ``` 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 ``` 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. ## Workspace operations ### Manage workspace configuration Clone workspace-level configuration to a local directory: ```bash plane ws clone -c ``` 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 ``` To filter by project name pattern: ```bash plane push --all --filter "" ``` ## 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:** ```bash plane state remove types. plane state remove work_items. ``` **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] ``` | Option | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PROJECT` | Name of the directory to create and the project key written into `plane.yaml` under `project.key`. | | `--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. | *** #### `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 [--connection CONN] [--server-url URL] [--auth-type TYPE] [--token TOKEN] [--workspace WS] ``` | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--connection CONN` | Name or ID to assign to this connection. Used to identify it in `plane auth list-connections` and to reference it with `--connection` in other commands. If omitted, a name is prompted interactively. | | `--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. | *** #### `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] ``` | 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`. ``` 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 ``` | Option | Description | | ---------------------- | --------------------------------------------- | | `WORKSPACE_SLUG` | The Plane workspace slug to link. | | `--connection CONN_ID` | ID 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] ``` | 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. | *** #### `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] ``` | Option | Description | | ------------- | ------------------------------------------------------------ | | `PROJECT` | Project key or directory. Defaults to the current directory. | | `--path PATH` | Explicit 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] ``` | 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. | *** #### `plane schema import` Reads the current schema from the Plane remote and reconciles it with local files. 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] ``` | 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 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] ``` | Option | Description | | ------------- | ------------------------------------------------------------ | | `PROJECT` | Project key or directory. Defaults to the current directory. | | `--path PATH` | Explicit 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] ``` | 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. | *** #### `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] ``` | 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 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] ``` | 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. | *** #### `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] ``` | 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. | **Output categories:** | 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. | *** #### `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] ``` | 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. | *** #### `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] ``` | 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. | *** #### `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] ``` | 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. | *** #### `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] ``` | 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. | *** #### `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] ``` | 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. | *** #### `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] ``` | 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. | *** #### `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, 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 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; 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. | 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 ``` / ├── plane.yaml ├── schema/ │ ├── types.yaml │ ├── states.yaml │ ├── workflows.yaml │ ├── labels.yaml │ ├── features.yaml │ ├── members.yaml # populated on pull; read-only │ ├── workitem_templates.yaml # populated on pull │ └── page_templates.yaml # populated on pull ├── work/ │ ├── workitems.yaml │ ├── cycles.yaml │ ├── modules.yaml │ └── milestones.yaml ├── .plane/ │ ├── state.json # sync state; do not edit manually │ └── .state.lock # held during active sync operations └── .gitignore ``` Workspace directory (created by `plane ws clone`): ``` / ├── workspace.yaml ├── schema/ │ └── workitem_types.yaml # workspace-level WIT 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`. Do not set manually. | | `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 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 ``` *** #### `workspace.yaml` The configuration file for a workspace directory created by `plane ws clone`. Identifies the workspace and connection, and contains workspace-level member data. | Field | Type | Description | | ------------------------ | ------ | --------------------------------------------------------------------------------------------------------------- | | `workspace` | string | Slug of the Plane workspace. Used to resolve the correct connection from `~/.config/plane-compose/config.json`. | | `connection` | string | Optional. ID of a specific connection to pin to this workspace directory. | | `workspace_features` | map | Optional. Workspace-level feature flag overrides. Keys are feature names; values are `true` or `false`. | | `members` | list | Workspace members. Populated automatically on `plane ws pull`. Read-only - do not edit manually. | | `members[].id` | string | Remote UUID of the member. | | `members[].email` | string | Email address of the member. | | `members[].display_name` | string | Display name of the member as shown in Plane. | ```yaml workspace: myteam connection: conn-1 workspace_features: epics: true members: - id: abc-123 email: dev@example.com display_name: Dev User ``` *** #### `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 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`. Requires `epics: true` in `schema/features.yaml`. | | `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. | | `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. | Alias: `enum`. Requires `options` list. Add `is_multi: true` for multi-select. | | `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. | ```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. | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `` | map | Top-level key is the state name, referenced in `work/workitems.yaml` and `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 - no nesting. 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" ``` *** #### `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. | 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. 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. The `status` field is computed by Plane based on dates relative to the current time and is read-only - do not set it manually. 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. 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 ``` ### Troubleshooting #### Authentication failed (401) The stored token is invalid, expired, or has been revoked. Remove the connection and re-authenticate: ```bash plane auth logout 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: ```bash 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 ```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 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. ### 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 connection bundles three things: a server URL, an auth type, and a token. Each connection gets a name. 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. 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. Work files define *what is happening*: the actual items, sprints, modules, and milestones. Work changes constantly - every day, by everyone on the team. 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 another source, or let the team manage through the Plane UI. Some teams commit both to Git. Others commit only schema and treat work items as data managed through Plane directly. Both are valid. 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. --- --- url: 'https://developers.plane.so/dev-tools/openapi-specification.html' description: >- Generate and access the OpenAPI 3.0 specification for the Plane public REST API using drf-spectacular. --- # OpenAPI Specification Plane uses [drf-spectacular](https://drf-spectacular.readthedocs.io/) to generate an OpenAPI 3.0 specification for the public REST API (`/api/v1/`). The feature is **disabled by default** and must be explicitly enabled. ## Enable the OpenAPI spec Add the following to your `.env` file (at the project root or `apps/api/.env`): ```ini ENABLE_DRF_SPECTACULAR=1 ``` Then restart the API server so it picks up the new variable. | Variable | Required value | Default | Description | | ------------------------ | -------------- | ------- | ------------------------------------------------------------ | | `ENABLE_DRF_SPECTACULAR` | `1` | `0` | Activates drf-spectacular and registers the schema endpoints | No other environment variables are needed — everything else (schema path prefix, tags, auth schemes, servers) is pre-configured in `apps/api/plane/settings/openapi.py`. ## Access the OpenAPI spec > Replace `{domain_name}` below with your self-hosted Plane domain (e.g. `plane.example.com`). Once the API server is running with the variable enabled, three endpoints are available: | Endpoint | URL | Description | | ----------------------------- | ---------------------------------------------- | -------------------------- | | `GET /api/schema/` | `https://{domain_name}/api/schema/` | Raw OpenAPI schema (YAML) | | `GET /api/schema/swagger-ui/` | `https://{domain_name}/api/schema/swagger-ui/` | Interactive Swagger UI | | `GET /api/schema/redoc/` | `https://{domain_name}/api/schema/redoc/` | ReDoc documentation viewer | ## Download the OpenAPI spec ### Browser Open `https://{domain_name}/api/schema/` and save the page. The default format is YAML. For JSON, append the `format` query parameter: ```text https://{domain_name}/api/schema/?format=openapi-json ``` ### curl ```bash # YAML curl -o openapi.yaml https://{domain_name}/api/schema/ # JSON curl -o openapi.json https://{domain_name}/api/schema/?format=openapi-json ``` ### Management command (offline, no running server required) ```bash # From apps/api/ ENABLE_DRF_SPECTACULAR=1 python manage.py spectacular --file openapi.yaml ENABLE_DRF_SPECTACULAR=1 python manage.py spectacular --file openapi.json --format openapi-json ``` --- --- url: 'https://developers.plane.so/dev-tools/intro-webhooks.html' description: >- Configure webhooks for Plane. Setup real-time event notifications and automate workflows with webhook integrations. --- # Webhooks Webhooks give you a way to push Plane events into other systems the moment they happen. Instead of polling the Plane API periodically to check for changes, your service registers a URL and Plane calls it, with a structured payload, whenever something occurs. The value of webhooks over polling is immediacy and simplicity. Your endpoint receives exactly what happened without having to query anything. A work item created at 14:00:01 triggers a request at 14:00:01. Webhooks in Plane work at the workspace level. A single webhook can subscribe to events from every project in the workspace. There is no per-project webhook configuration. Only Workspace Owners and Admins can create or manage them. The current webhook system is v2. V2 payloads use dot-notation event names (e.g., `workitem.created`) and include structured fields for deduplication, diffing, and filtering that were not available in the original version. If you have webhooks created before v2, they appear in the list with a **(deprecated)** tag. They still deliver but do not receive any v2 fields. Recreate them as new webhooks to get the full v2 feature set. :::warning Migrate your v1 webhooks to v2 V1 webhooks are deprecated. They continue to deliver but will not receive v2 payload features — including `delivery_id` and `event_id` for deduplication, `previous_attributes` for diffs on updated events, and dot-notation event names. There is no in-place upgrade. To move to v2, recreate each v1 webhook as a new webhook and update your server to handle the v2 payload structure. **To migrate a v1 webhook:** 1. Open the v1 webhook (marked **deprecated**) and note its URL and event subscriptions. 2. Create a new webhook with the same URL and event subscriptions. See [How to create a webhook](#how-to-create-a-webhook). 3. Save the new secret key from the CSV download — your server will need it to verify v2 requests. 4. Update your server to expect the v2 payload structure. See [Payload structure](#payload-structure) for the full field reference. 5. Test that deliveries are arriving and your server is handling them correctly. 6. Delete the original v1 webhook once you're confident the new one is working. ::: ## Creating a webhook ![Plane architecture](/images/webhooks/create-webhook.webp#hero) ### How to create a webhook 1. Go to **Workspace Settings → Webhooks**. 2. Click **Add webhook**. 3. Enter a **Webhook title** and **Payload URL**. 4. Check the events you want this webhook to fire for. 5. Optionally, expand **Advanced configurations** to add a work item filter. 6. Click **Create webhook**. Plane downloads the secret key as a CSV file to your computer and returns you to the webhook list. The webhook is active immediately. If you lose the CSV, you can re-generate the secret key from the edit form - but the old key stops working the moment you do. ### What you're configuring When you create a webhook, you're telling Plane two things: where to send events, and which events to send. * **Webhook title** is a label for your own reference - it appears in the webhook list and helps you tell multiple webhooks apart. * **Payload URL** is the endpoint that Plane will POST to. It must be a publicly reachable `http://` or `https://` address. Local addresses (localhost, private IPs) are not accepted. * **Events** control what triggers this webhook. The form groups events by type - Projects, Cycles, Modules, Work items, and so on. Check the specific actions you care about. You can subscribe to as many or as few as you need. * **Advanced configurations** lets you add a filter so the webhook only fires for work items that match specific conditions - for example, high-priority bugs in a particular project. See [Filtering work item events](#filtering-work-item-events) below. * **Secret key** is generated automatically when you save the webhook. Plane downloads it as a CSV file the moment you click **Create webhook** and then returns you to the webhook list. It is not displayed on screen - the download is the only time you receive it automatically. Save the file. You need the key to verify incoming requests. ## Filtering work item events ### How filtering works By default, a webhook fires for every work item event you subscribe to, across all projects in the workspace. Filters let you narrow that, for example, to fire only when a high-priority work item is created in a specific project. Filters apply only to work item events. Events for projects, cycles, pages, milestones, and other types are always delivered without filtering. Plane evaluates filters at delivery time. If the filter fails to evaluate for any reason, the delivery is skipped rather than defaulting to "deliver everything." If you're not receiving expected deliveries, check that your filter expression is valid. ### Basic mode versus PQL mode The filter builder in **Advanced configurations** offers two modes you can switch between freely: * **Basic** - a visual picker. Select values from dropdowns and Plane converts your selections into a filter expression behind the scenes. * **PQL** - direct text input. Type a PQL (Plane Query Language) expression. The expression shown is exactly what is stored and evaluated at delivery time. Switching between modes is lossless - your filter is not lost when you switch. ### How to add a work item filter 1. Create or edit a webhook. 2. Check at least one **Work items** event. 3. Scroll down to the **Work item v2 filters** section. 4. Use the filter builder to define your conditions in Basic mode, or switch to PQL mode to type an expression directly. 5. Save the webhook. ### PQL syntax and supported fields | Filter field | PQL field name | Accepted values | | -------------- | -------------- | --------------------------------------------------------------- | | Work item type | `type_id` | Work item type UUID | | State group | `state_group` | `backlog` · `unstarted` · `started` · `completed` · `cancelled` | | Assignees | `assignee_id` | User UUID | | Labels | `label_id` | Label UUID | | Projects | `project_id` | Project UUID | | Priority | `priority` | `none` · `low` · `medium` · `high` · `urgent` | | Start date | `start_date` | ISO date | | Due date | `target_date` | ISO date | **Expression syntax** ``` priority = "urgent" Single value priority in ["urgent", "high"] Multiple values state_group = "started" State group match assignee_id = "" Specific assignee project_id = "" Specific project ``` ## Managing webhooks ### Disabling versus deleting Disabling a webhook pauses delivery without removing any configuration. The webhook stays in the list, its event subscriptions are preserved, and you can re-enable it at any time. Events are not queued while the webhook is disabled - any event that fires during the disabled period is not delivered. Use this when your endpoint is temporarily down or you need to make changes to your receiving system. Deleting a webhook removes it permanently - configuration and delivery history are gone. There is no undo. ### Edit a webhook 1. Go to **Workspace Settings → Webhooks**. 2. Click **···** on the webhook row and select **Edit**. 3. Update the title, URL, event subscriptions, or filter. 4. Click **Update webhook**. ### Disable or enable a webhook 1. Go to **Workspace Settings → Webhooks**. 2. Click **···** on the webhook row. 3. Select **Disable webhook** to stop delivery, or **Enable webhook** to resume it. ### Delete a webhook 1. Go to **Workspace Settings → Webhooks**. 2. Click **···** on the webhook row and select **Delete webhook**. ### View and copy the secret key The secret key is not displayed during creation - Plane downloads it as a CSV instead. To access it later: 1. Go to **Workspace Settings → Webhooks**. 2. Click **···** on the webhook row and select **Edit**. 3. In the **Secret key** section, click the eye icon to reveal the key. 4. Click the copy icon to copy it. ### Re-generate the secret key Re-generate if your secret key is compromised. The old key is invalidated the moment you re-generate - update your server before completing this step or your signature verification will break. 1. Go to **Workspace Settings → Webhooks**. 2. Click **···** on the webhook row and select **Edit**. 3. In the **Secret key** section, click **Re-generate key**. Plane downloads the new key as a CSV. ## Securing requests ### Why Plane signs every request Any server on the internet can send a POST request to your endpoint. Without a way to verify the source, someone could send fake webhook payloads to your system and trigger whatever logic you've built around them. Plane solves this by signing every request with HMAC-SHA256 using your secret key. The signature is attached as an `X-Plane-Signature` header. Because only Plane and you know the secret, a valid signature proves the request came from Plane and was not modified in transit. Skipping verification means your endpoint will process any request that arrives - forged or not. ### How to verify a webhook payload On your server, compute the expected signature from the **raw request body bytes** and compare it to the value in `X-Plane-Signature`. Use a constant-time comparison to prevent timing attacks. ```python import hashlib import hmac def verify_webhook(request_body_bytes: bytes, secret: str, signature_header: str) -> bool: expected = hmac.new( secret.encode("utf-8"), request_body_bytes, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature_header) ``` Use the raw bytes from the incoming request - not a parsed or re-serialized version. JSON re-serialization can change key ordering, spacing, or escaping, which will produce a different signature and cause verification to fail. Reject any request where the signature does not match before running any other logic. ### Signature header reference | Header | Value | | ------------------- | ------------------------------------------------------------------------------ | | `X-Plane-Signature` | HMAC-SHA256 hex digest of the raw request body, keyed with your webhook secret | The secret key is formatted as `plane_wh_` followed by a random string. Plane masks it in the UI. To view the full key, open the edit form for the webhook and use the show/hide toggle in the **Secret key** section. ## Delivery and monitoring ### How Plane delivers events Plane sends webhook requests asynchronously. When an event occurs, Plane queues the delivery and sends a POST request to your endpoint. Any 2xx response is treated as a success. If your endpoint is unavailable or returns a server error, Plane retries using exponential backoff with a ~10-minute base and jitter. After **5 failed attempts**, Plane automatically disables the webhook and emails the webhook creator. Re-enable it from the webhook list once your endpoint is fixed. 4xx responses are not retried. Plane treats them as a deliberate rejection from your server. Retry behavior is automatic. There is no way to trigger a manual retry for a failed delivery. ### How to read delivery logs 1. Go to **Workspace Settings → Webhooks**. 2. Click on a webhook to open its detail view. The top of the view shows four summary stats: | Stat | What it shows | | ---------------- | ------------------------------------------------------ | | Total deliveries | Total number of delivery attempts | | Successful | Deliveries that received a 2xx response | | Failed | Deliveries that returned an error or exhausted retries | | Success rate | Successful deliveries as a percentage of total | Below the summary, the delivery log lists individual attempts: | Column | What it shows | | ------------- | ------------------------------------------------------- | | Events | The event type that triggered the delivery | | Status | Successful or Failed | | Response time | How long your endpoint took to respond, in milliseconds | | Event time | When the delivery was sent | ## Events and payload ### Event reference | Group | Event key | Fires when | | -------------------------- | ----------------------------- | -------------------------------------- | | **Projects** | `project.created` | A project is created | | | `project.updated` | A project is updated | | | `project.archived` | A project is archived | | | `project.deleted` | A project is deleted | | **Cycles** | `cycle.created` | A cycle is created | | | `cycle.updated` | A cycle is updated | | | `cycle.archived` | A cycle is archived | | | `cycle.deleted` | A cycle is deleted | | **Modules** | `module.created` | A module is created | | | `module.updated` | A module is updated | | | `module.archived` | A module is archived | | | `module.deleted` | A module is deleted | | **Milestones** | `milestone.created` | A milestone is created | | | `milestone.updated` | A milestone is updated | | | `milestone.deleted` | A milestone is deleted | | **Pages** | `page.created` | A page is created | | | `page.updated` | A page is updated | | | `page.archived` | A page is archived | | | `page.deleted` | A page is deleted | | **Page comments** | `page.comment.created` | A comment is added to a page | | | `page.comment.updated` | A page comment is edited | | | `page.comment.deleted` | A page comment is deleted | | **Work items** | `workitem.created` | A work item is created | | | `workitem.updated` | A work item is updated | | | `workitem.archived` | A work item is archived | | | `workitem.deleted` | A work item is deleted | | **Work item comments** | `workitem.comment.created` | A comment is added to a work item | | | `workitem.comment.updated` | A work item comment is edited | | | `workitem.comment.deleted` | A work item comment is deleted | | **Work item links** | `workitem.link.created` | A link is added to a work item | | | `workitem.link.updated` | A work item link is updated | | | `workitem.link.deleted` | A work item link is removed | | **Work item votes** | `workitem.vote.created` | A vote is cast on a work item | | | `workitem.vote.deleted` | A vote is removed | | **Work item attachments** | `workitem.attachment.created` | A file is attached to a work item | | | `workitem.attachment.updated` | A work item attachment is updated | | | `workitem.attachment.deleted` | A work item attachment is removed | | **Work item relations** | `workitem.relation.created` | A relation is added between work items | | | `workitem.relation.deleted` | A relation is removed | | **Work item dependencies** | `workitem.dependency.created` | A dependency is added | | | `workitem.dependency.deleted` | A dependency is removed | | **Work item page links** | `workitem.page_link.created` | A page link is added to a work item | | | `workitem.page_link.deleted` | A page link is removed | ### Request headers Every webhook request includes these headers: | Header | Value | | ------------------- | ----------------------------------------------------------------------------- | | `Content-Type` | `application/json` | | `User-Agent` | `Autopilot` | | `X-Plane-Delivery` | Unique UUID per delivery attempt. Matches `delivery_id` in the payload body. | | `X-Plane-Event` | The event type, e.g. `workitem.created`. Matches `event` in the payload body. | | `X-Plane-Signature` | HMAC-SHA256 signature of the request body | These headers are reserved and cannot be overridden with custom values: `host`, `content-length`, `content-type`, `user-agent`, `x-plane-delivery`, `x-plane-event`, `x-plane-signature`. ### Payload structure All v2 payloads share this top-level structure: ```json { "version": "v2", "delivery_id": "", "event_id": "", "entity_id": "", "entity_type": "", "event": "", "webhook_id": "", "workspace_id": "", "data": {}, "previous_attributes": {} } ``` | Field | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | Always `"v2"` | | `delivery_id` | Unique ID for this delivery attempt. Matches the `X-Plane-Delivery` header. A new UUID is generated for each retry. | | `event_id` | Unique ID for the triggering event. Stable across retries - use this for deduplication. | | `entity_id` | UUID of the primary entity affected by the event. | | `entity_type` | Type of the entity, e.g. `issue`, `cycle`, `issue_comment`, `issue_link`. | | `event` | Full dot-notation event name, e.g. `workitem.comment.updated`. | | `webhook_id` | ID of the webhook configuration that triggered this delivery. | | `workspace_id` | UUID of the workspace in which the event occurred. | | `data` | Full entity object for create and update events. Empty object `{}` for delete events. | | `previous_attributes` | Present on all events. For `updated` events, contains the previous values of changed fields. For `deleted` events, contains the full record before deletion. Empty object `{}` for all other events. | ### Payload examples **workitem.updated** ```json { "version": "v2", "delivery_id": "2a0d0510-9052-446e-a1c7-a704bbd68cba", "event_id": "9d508cd9-36c2-44a5-928d-7ee2f2a3b8a8", "entity_id": "775c5716-5302-4617-bb9f-2cd843911268", "entity_type": "issue", "event": "WebhookScope.ScopeChoices.WORK_ITEM_UPDATED", "webhook_id": "8944ed18-1331-4eae-b9bb-7c40864b8abd", "workspace_id": "b54ecb0d-e3eb-4986-b238-f83fd8665e65", "data": { "id": "775c5716-5302-4617-bb9f-2cd843911268", "name": "webhook test 3", "point": "None", "type_id": "None", "is_draft": false, "priority": "none", "state_id": "067b88e5-304b-4221-ba09-94340dcc36e5", "label_ids": [], "parent_id": "None", "created_at": "2026-03-31T11:44:41.249292+00:00", "deleted_at": "None", "project_id": "59e3be42-87ec-4950-99a3-ae639cf2b089", "sort_order": 75535, "start_date": "None", "updated_at": "2026-03-31T11:44:41.249304+00:00", "archived_at": "None", "external_id": "None", "sequence_id": 3, "target_date": "None", "assignee_ids": [], "completed_at": "None", "workspace_id": "b54ecb0d-e3eb-4986-b238-f83fd8665e65", "created_by_id": "754009ab-3fb5-424e-909a-b46e9c9d0c4f", "updated_by_id": "None", "external_source": "None", "description_json": {}, "last_activity_at": "2026-03-31T11:44:41.346305+00:00", "estimate_point_id": "None" }, "previous_attributes": { "last_activity_at": "2026-03-31 11:44:41.242868+00" } } ``` **workitem.comment.created** ```json { "version": "v2", "delivery_id": "01ab9316-f978-4449-bad6-dce958be8454", "event_id": "0afa042d-92a9-4326-bdca-5ff5490dbf09", "entity_id": "088a83b9-a53f-4dda-b2bc-c860cf455997", "entity_type": "issue", "event": "workitem.comment.created", "webhook_id": "285f087b-e1e0-4f90-b9f4-0b720acfac04", "workspace_id": "d250cd44-fa71-42c2-b2b5-3c73227288fc", "data": { "id": "088a83b9-a53f-4dda-b2bc-c860cf455997", "name": "Webhook Test Work Item 2", "comment": { "id": "4797f841-c731-4e55-971f-d9cfe1938dfb", "access": "INTERNAL", "actor_id": "88fc36c8-73b0-4547-81c7-96b70f61835e", "issue_id": "088a83b9-a53f-4dda-b2bc-c860cf455997", "edited_at": null, "comment_stripped": "Webhook Test Comment" } }, "previous_attributes": {} } ``` **workitem.link.created** ```json { "version": "v2", "delivery_id": "616d98fe-35a7-4431-a233-db40936c8339", "event_id": "7b3c1e2a-8f94-4b12-a781-2c5e9d4f6a03", "entity_id": "8661bdfa-098f-434d-8e44-b1f32de62406", "entity_type": "issue_link", "event": "workitem.link.created", "webhook_id": "285f087b-e1e0-4f90-b9f4-0b720acfac04", "workspace_id": "d250cd44-fa71-42c2-b2b5-3c73227288fc", "data": { "id": "a6f8e562-49d2-4c19-bc4b-2bcb9d917da1", "url": "http://google.com", "title": "", "issue_id": "8661bdfa-098f-434d-8e44-b1f32de62406", "created_at": "2026-05-20T09:51:27.373582+00:00", "project_id": "45b87d89-0ce0-4d6f-8903-4070f1c67f1b", "workspace_id": "d250cd44-fa71-42c2-b2b5-3c73227288fc", "created_by_id": "88fc36c8-73b0-4547-81c7-96b70f61835e" }, "previous_attributes": {} } ``` **workitem.link.updated** ```json { "version": "v2", "delivery_id": "2a0d0510-9052-446e-a1c7-a704bbd68cba", "event_id": "9d508cd9-36c2-44a5-928d-7ee2f2a3b8a8", "entity_id": "775c5716-5302-4617-bb9f-2cd843911268", "entity_type": "issue", "event": "WebhookScope.ScopeChoices.WORK_ITEM_UPDATED", "webhook_id": "8944ed18-1331-4eae-b9bb-7c40864b8abd", "workspace_id": "b54ecb0d-e3eb-4986-b238-f83fd8665e65", "data": { "id": "775c5716-5302-4617-bb9f-2cd843911268", "name": "webhook test 3", "point": "None", "type_id": "None", "is_draft": false, "priority": "none", "state_id": "067b88e5-304b-4221-ba09-94340dcc36e5", "label_ids": [], "parent_id": "None", "created_at": "2026-03-31T11:44:41.249292+00:00", "deleted_at": "None", "project_id": "59e3be42-87ec-4950-99a3-ae639cf2b089", "sort_order": 75535, "start_date": "None", "updated_at": "2026-03-31T11:44:41.249304+00:00", "archived_at": "None", "external_id": "None", "sequence_id": 3, "target_date": "None", "assignee_ids": [], "completed_at": "None", "workspace_id": "b54ecb0d-e3eb-4986-b238-f83fd8665e65", "created_by_id": "754009ab-3fb5-424e-909a-b46e9c9d0c4f", "updated_by_id": "None", "external_source": "None", "description_json": {}, "last_activity_at": "2026-03-31T11:44:41.346305+00:00", "estimate_point_id": "None" }, "previous_attributes": { "last_activity_at": "2026-03-31 11:44:41.242868+00" } } ``` --- --- url: 'https://developers.plane.so/api-reference/inbox-issue/add-inbox-issue.html' description: >- Create intake issue via Plane API. HTTP POST request format, required fields, and example responses. --- # Add intake issue Adds an intake issue in a project ### Path Parameters ### Body Parameters An object containing the issue details, including a `name` field (required). ### Scopes `projects.intakes:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issue": "example-issue" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/", headers={"X-API-Key": "your-api-key"}, json={ 'issue': 'example-issue' } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ issue: "example-issue", }), }, ); const data = await response.json(); ``` ```json { "id": "project-uuid", "name": "Project Name", "identifier": "PROJ", "description": "Project description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/work-item-relations/create-work-item-relation.html description: >- Create work item relation via Plane API. HTTP request format, parameters, scopes, and example responses for create work item relation. --- # Create work item relation Create relationships between work items. Supports various relation types including blocking, blocked\_by, duplicate, relates\_to, start\_before, start\_after, finish\_before, and finish\_after. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters Type of relationship between work items * `blocking` - Blocking * `blocked_by` - Blocked By * `duplicate` - Duplicate * `relates_to` - Relates To * `start_before` - Start Before * `start_after` - Start After * `finish_before` - Finish Before * `finish_after` - Finish After Array of work item IDs to create relations with ### Scopes `projects.work_items:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "relation_type": "blocking", "issues": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000" ] }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/", headers={"X-API-Key": "your-api-key"}, json={ "relation_type": "blocking", "issues": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000" ] } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ relation_type: "blocking", issues: ["550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000"], }), }, ); const data = await response.json(); ``` ```json [ [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Example Name", "sequence_id": 42, "project_id": "550e8400-e29b-41d4-a716-446655440000", "relation_type": "blocked_by", "state_id": "550e8400-e29b-41d4-a716-446655440000", "priority": "high", "type_id": "550e8400-e29b-41d4-a716-446655440000", "is_epic": false, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z", "created_by": "550e8400-e29b-41d4-a716-446655440000", "updated_by": "550e8400-e29b-41d4-a716-446655440000" } ] ] ``` --- --- url: 'https://developers.plane.so/api-reference/inbox-issue/delete-inbox-issue.html' description: >- Delete an intake issue from the inbox via Plane API. Permanently removes the triaged submission. Returns 204 on success. --- # Delete intake issue Deletes an intake issue ### Path Parameters ### Scopes `projects.intakes:write` ```bash curl -X DELETE \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.delete( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid", { method: "DELETE", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json // 204 No Content ``` --- --- url: >- https://developers.plane.so/api-reference/inbox-issue/get-inbox-issue-detail.html description: >- Get intake issue detail details via Plane API. Retrieve complete information for a specific resource. --- # Get intake issue detail Gets the details of an intake issue ### Path Parameters ### Scopes `projects.intakes:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "project-uuid", "name": "Project Name", "identifier": "PROJ", "description": "Project description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: 'https://developers.plane.so/api-reference/inbox-issue/list-inbox-issues.html' description: >- List intake issues via Plane API. HTTP GET request with pagination, filtering, and query parameters. --- # List intake issues Gets all the intake issue of a project ### Path Parameters ### Scopes `projects.intakes:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "id": "project-uuid", "name": "Project Name", "identifier": "PROJ", "description": "Project description", "created_at": "2024-01-01T00:00:00Z" } ``` --- --- url: >- https://developers.plane.so/api-reference/work-item-relations/list-work-item-relations.html description: >- List work item relations via Plane API. HTTP request format, parameters, scopes, and example responses for list work item relations. --- # List work item relations Retrieve all relationships for a work item including blocking, blocked\_by, duplicate, relates\_to, start\_before, start\_after, finish\_before, and finish\_after relations. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Query Parameters Pagination cursor for getting next set of results Comma-separated list of related fields to expand in response Comma-separated list of fields to include in response Field to order results by. Prefix with '-' for descending order Number of results per page (default: 20, max: 100) ### Scopes `projects.work_items:read` ```bash curl -X GET \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/?cursor=20:1:0&expand=assignees" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" ``` ```python import requests response = requests.get( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/?cursor=20:1:0&expand=assignees", headers={"X-API-Key": "your-api-key"} ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/?cursor=20:1:0&expand=assignees", { method: "GET", headers: { "X-API-Key": "your-api-key", }, }, ); const data = await response.json(); ``` ```json { "blocking": ["550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000"], "blocked_by": ["550e8400-e29b-41d4-a716-446655440000"], "duplicate": [], "relates_to": ["550e8400-e29b-41d4-a716-446655440000"], "start_after": [], "start_before": ["550e8400-e29b-41d4-a716-446655440000"], "finish_after": [], "finish_before": [] } ``` --- --- url: 'https://developers.plane.so/self-hosting/methods/one-click.html' description: >- Deploy Plane instantly with one-click installers on popular cloud providers. Quick setup on AWS, DigitalOcean, Render, and Railway. --- # One-click deploy :::info This feature is included in our paid plans, but for a limited time, our community users can access it for free. ::: ### Requirements * Operating systems: Debian, Ubuntu, CentOS * Supported CPU architectures: AMD64, ARM64, x86\_64, AArch64 ### Download the latest stable release Run ↓ on any CLI. ``` curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - ``` ### Download the Preview release `Preview` builds do not support ARM64, AArch64 CPU architectures Run ↓ on any CLI. ``` export BRANCH=preview curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh - ``` ### Successful installation You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance. ![Install Output](/images/one-click-deploy/one-click-install.png) ### Manage your Plane instance Use `plane-app` \[OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`. ![Plane Help](/images/one-click-deploy/one-click-help.png) 1. Basic operators 1. `plane-app start` starts the Plane server. 2. `plane-app restart` restarts the Plane server. 3. `plane-app stop` stops the Plane server. 2. Advanced operators `plane-app --configure` will show advanced configurators. ![Advanced operators](/images/one-click-deploy/one-click-advanced.png) * Change your proxy or listening port * Change your domain name * File upload size 3. Version operators 1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images 2. `plane-app --update-installer` updates the installer and the `plane-app` utility. 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in Postgres, Redis, and Minio alone. 4. `plane-app --install` installs the Plane app again. --- --- url: 'https://developers.plane.so/api-reference/inbox-issue/overview.html' description: >- Plane Inbox-Issue API overview. Learn about endpoints, request/response format, and how to work with inbox-issue via REST API. --- # Overview ::: warning **Deprecation notice** We are deprecating all `/api/v1/.../inbox-issues/` endpoints in favor of `/api/v1/.../intake-issues/`. **End of support**\ 31st March 2025 **What you need to do**\ To ensure uninterrupted service, replace all `/inbox-issues/` references with `/intake-issues/` in your codebase before the support end date. ::: To enable the Intake feature, the user can hit a PATCH request on the project api with the body as ```http GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/inbox-issues/ POST /api/v1/workspaces/{workspace_slug}/projects/{project_id}/inbox-issues/ GET /api/v1/workspaces/{workspace_slug}/projects/{project_id}/inbox-issues/{work_item_id}/ PATCH /api/v1/workspaces/{workspace_slug}/projects/{project_id}/inbox-issues/{work_item_id}/ DELETE /api/v1/workspaces/{workspace_slug}/projects/{project_id}/inbox-issues/{work_item_id}/ ``` ``` { inbox_view:true, } ``` To create an Intake issue, the payload should be sent in the below format ```json { "issue": { "name": "Snoozed Issue 2", "priority": "high" } } ``` ### Intake issue object **Attribute** * `created_at` *timestamp* The timestamp of the time when the project was created * `updated_at` *timestamp* The timestamp of the time when the project was last updated * `status` the status of the issue can be in above mentioned status * -2 - Pending * -1 - Rejected * 0 - Snoozed * 1 - Accepted * 2 - Duplicate * `snoozed_till` The time untill the issue is snoozed. * `source` The source describes the type intake issue from * `created_by` , `updated_by` *uuid* These values are auto saved and represent the id of the user that created or updated the module * `Project` uuid It contains projects uuid which is automatically saved. * `Workspace` uuid It contains workspace uuid which is automatically saved. * `inbox` intake id of the issue * `issue` issue id of the issue * `duplicate_to` Id of the issue of which the current issue is duplicate of. ```json { "id": "0de4d6d1-fdc7-4849-8080-dc379ab210e3", "pending_issue_count": 0, "created_at": "2023-11-21T07:32:26.072634Z", "updated_at": "2023-11-21T07:32:26.072648Z", "name": "a dummy project with Intake", "description": "", "is_default": true, "view_props": {}, "created_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "updated_by": "0649cb9d-05c8-4ef4-8e8b-d108ccddd42c", "project": "6436c4ae-fba7-45dc-ad4a-5440e17cb1b2", "workspace": "c467e125-59e3-44ec-b5ee-f9c1e138c611" } ``` --- --- url: 'https://developers.plane.so/api-reference/work-item-relations/overview.html' description: >- Plane Work Item Relations API overview. Learn how to list, create, and remove work item relationships using the Plane API. --- # Overview Work item relations let you model dependencies and planning constraints between work items, including blocking, duplicate, relates-to, and scheduling relationships. [Learn more about Work Items](https://docs.plane.so/core-concepts/issues) ## The Work Item Relations Response ### Attributes * `blocking` *array* List of issue IDs that are blocking this issue * `blocked_by` *array* List of issue IDs that this issue is blocked by * `duplicate` *array* List of issue IDs that are duplicates of this issue * `relates_to` *array* List of issue IDs that relate to this issue * `start_after` *array* List of issue IDs that start after this issue * `start_before` *array* List of issue IDs that start before this issue * `finish_after` *array* List of issue IDs that finish after this issue * `finish_before` *array* List of issue IDs that finish before this issue ```json { "blocking": ["550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440000"], "blocked_by": ["550e8400-e29b-41d4-a716-446655440000"], "duplicate": [], "relates_to": ["550e8400-e29b-41d4-a716-446655440000"], "start_after": [], "start_before": ["550e8400-e29b-41d4-a716-446655440000"], "finish_after": [], "finish_before": [] } ``` --- --- url: >- https://developers.plane.so/api-reference/work-item-relations/remove-work-item-relation.html description: >- Remove work item relation via Plane API. HTTP request format, parameters, scopes, and example responses for remove work item relation. --- # Remove work item relation Remove a relationship between work items by specifying the related work item ID. ### Path Parameters The unique identifier of the work item. The unique identifier of the project. The workspace\_slug represents the unique workspace identifier for a workspace in Plane. It can be found in the URL. For example, in the URL `https://app.plane.so/my-team/projects/`, the workspace slug is `my-team`. ### Body Parameters ID of the related work item to remove relation with ### Scopes `projects.work_items:write` ```bash curl -X POST \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/remove/" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "related_issue": "550e8400-e29b-41d4-a716-446655440000" }' ``` ```python import requests response = requests.post( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/remove/", headers={"X-API-Key": "your-api-key"}, json={ "related_issue": "550e8400-e29b-41d4-a716-446655440000" } ) print(response.status_code) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/work-items/work-item-uuid/relations/remove/", { method: "POST", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ related_issue: "550e8400-e29b-41d4-a716-446655440000", }), }, ); console.log(response.status); ``` No response body. --- --- url: >- https://developers.plane.so/api-reference/inbox-issue/update-inbox-issue-detail.html description: >- Update intake issue detail via Plane API. HTTP PATCH request format, editable fields, and example responses. --- # Update intake issue detail Updates the details of an intake issue ### Path Parameters ### Body Parameters An object containing the issue details to update, including an optional `name` field. ### Scopes `projects.intakes:write` ```bash curl -X PATCH \ "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid" \ -H "X-API-Key: $PLANE_API_KEY" \ # Or use -H "Authorization: Bearer $PLANE_OAUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "issue": "example-issue" }' ``` ```python import requests response = requests.patch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid", headers={"X-API-Key": "your-api-key"}, json={ 'issue': 'example-issue' } ) print(response.json()) ``` ```javascript const response = await fetch( "https://api.plane.so/api/v1/workspaces/my-workspace/projects/project-uuid/inbox-issues/issue-uuid", { method: "PATCH", headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json", }, body: JSON.stringify({ issue: "example-issue", }), }, ); const data = await response.json(); ``` ```json { "id": "project-uuid", "name": "Project Name", "identifier": "PROJ", "description": "Project description", "created_at": "2024-01-01T00:00:00Z" } ```