Enabling "Work item Types" via REST API silently and permanently prevents the project from ever having the native Epic type

# Enabling “Work item Types” via REST API silently and permanently prevents the project from ever having the native Epic type

**Product:** Plane Cloud (`app.plane.so` / `api.plane.so`, API `v1`)

**Workspace:** `cortex-intelligence` (plan: Pro)

**Observed:** 2026-08-04

**Auth used:** Personal Access Token (`X-API-Key`)

**Severity (our view):** high — the resulting project state is unrecoverable and the API gives no error

-–

## Summary

Enabling the **Work item Types** feature produces a *different result* depending on

whether it is done through the web UI or through the REST API:

| Enabled via | Types created | `POST /epics/` afterwards |

|—|—|—|

| Web UI (`Project settings > Work item Types > Enable`) | `Task` (level 0) **and `Epic` (level 1)** | `400` — epic feature is on |

| REST API (`PATCH /projects/{id}/ {“is_issue_type_enabled”: true}`) | **only `Task` (level 0)** | `404 {“error”:“Epic is not enabled for this project”}` |

Three properties combine to make this unrecoverable:

1. The API path never creates the level-1 `Epic` type.

2. The UI itself states **“Once enabled, Work item Types can’t be disabled.”** — so

the operator cannot turn it off and re-enable it correctly through the UI.

3. There is no API way to create a level-1 / `is_epic` type afterwards (details in

*What we tried*, below). `POST /work-item-types/` accepts `level: 1` and

`is_epic: true`, returns `201`, and stores `level=0, is_epic=false`.

Net effect: **a single API call permanently removes the ability to use native Epics

in that project**, and it does so without any error, warning, or difference in the

project resource. The only recovery we found is to delete the project and recreate it.

## Why this matters to us

We provision Plane projects as code (idempotent plan/apply script over API v1) so

that every team’s project is born with the same structure: work item types, custom

properties, labels, states, modules, cycles and saved views. In our model the native

**Epic (level 1) is the monitored unit** — it carries the 7 custom properties our

rules engine reads (owner, sponsor, health, original due date, blocked reason, etc.).

Because our script enabled Work item Types over the API — the natural thing to do in

an automated provisioning flow — every project it created was silently left without

Epics, and could not be fixed after the fact. We only found out by diffing a

UI-created project against an API-created one.

## Steps to reproduce

Controlled experiment, two brand-new empty projects in the same workspace. Both start

identical: `is_issue_type_enabled: false`, zero work item types,

`POST /epics/` → `404`.

**Group A — enable through the UI**

1. Create project `teste2` (UI or API).

2. Open `Project settings > Work item Types` and click **Enable** (confirm the

“can’t be disabled” dialog).

3. `GET /api/v1/workspaces/{slug}/projects/{project_id}/work-item-types/`

Result: **2 types** — `Task` (level 0) and `Epic` (level 1, `is_epic: true`).

**Group B — enable through the API**

1. Create project `teste3`.

2. `PATCH /api/v1/workspaces/{slug}/projects/{project_id}/` with

`{“is_issue_type_enabled”: true}` → `200`.

3. `GET …/work-item-types/`

Result: **1 type** — `Task` (level 0) only. Re-read after 4s and after 16s to rule

out asynchronous creation: still only `Task`.

## Evidence

Project ids replaced with `<project_id:NAME>`; everything else is verbatim.

### E1 — Types after enabling: UI vs API

```

GET /api/v1/workspaces/{slug}/projects/<project_id:TESTE2>/work-item-types/ [enabled via UI]

→ 200 2 type(s): {name:“Task”, level:0, is_epic:false}, {name:“Epic”, level:1, is_epic:true}

GET /api/v1/workspaces/{slug}/projects/<project_id:TESTE3>/work-item-types/ [enabled via API]

→ 200 1 type(s): {name:“Task”, level:0, is_epic:false}

```

### E2 — The project resource does not record the difference

```

TESTE2 (UI) : is_issue_type_enabled=true … is_member=true issue_views_view=true page_view=true intake_view=false is_voting_enabled=true

TESTE3 (API): is_issue_type_enabled=true … is_member=true issue_views_view=true page_view=true intake_view=false is_voting_enabled=true

```

`is_issue_type_enabled` is `true` on both, and no other boolean on the project

resource distinguishes them. (`cycle_view`/`module_view` differ only because our

script later enabled those on TESTE2; they are unrelated to this issue.) So an

automated flow has **no way to detect** that it landed in the broken state, other

than noticing that no level-1 type exists.

### E3 — `POST /epics/` as a probe for the epic state

```

POST …/projects/<project_id:TESTE2>/epics/ {} [enabled via UI]

→ 400 {“name”:[“This field is required.”]} ← feature is ON (validation error)

POST …/projects/<project_id:TESTE3>/epics/ {} [enabled via API]

→ 404 {“error”:“Epic is not enabled for this project”}

```

This confirms the server has a notion of “epic enabled for this project”, and that

the API-enabled project is not in it.

### E4 — A level-1 (epic) type cannot be created via API: `201` but stored as level 0

```

POST …/projects/<project_id:TESTE3>/work-item-types/ {“name”:“EvidenceEpic1”,“level”:1,“is_epic”:true}

→ 201 stored as: level=0 is_epic=false (requested level=1 is_epic=true)

PATCH …/work-item-types/{id}/ {“level”:1,“is_epic”:true}

→ 200 stored as: level=0 is_epic=false (silently ignored)

POST …/projects/<project_id:TESTE3>/work-item-types/ {“name”:“EvidenceEpic2”,“level”:1,“is_epic”:true,“is_active”:true,“rollout”:“none”,“governance_mode”:“any”}

→ 201 stored as: level=0 is_epic=false (requested level=1 is_epic=true)

```

The second payload copies every field of a real `Epic` type read from a working

project (`is_active`, `rollout`, `governance_mode`, `logo_props`). Both `level` and

`is_epic` are accepted and dropped, with a success status.

### E5 — No feature field accepts an epic toggle

```

PATCH …/projects/{id}/ {“is_epic_enabled”:true} → 200 field present in GET afterwards: false

PATCH …/projects/{id}/ {“epic_view”:true} → 200 field present in GET afterwards: false

PATCH …/projects/{id}/ {“is_epics_enabled”:true} → 200 field present in GET afterwards: false

PATCH …/projects/{id}/ {“epics_view”:true} → 200 field present in GET afterwards: false

PATCH …/projects/{id}/ {“is_epic_view_enabled”:true} → 200 field present in GET afterwards: false

```

Every unknown field returns `200 OK` and changes nothing — see *Secondary issue* below.

### E6 — `import-work-item-types` is plan-gated

```

OPTIONS …/projects/{id}/import-work-item-types/ → 405 allow=POST

POST …/projects/{id}/import-work-item-types/ {} → 402 {“error”:“Payment required”,“error_code”:1999}

POST …/projects/{id}/import-work-item-types/ {“work_item_type_ids”:[…]} → 402 {“error”:“Payment required”,“error_code”:1999}

```

The route exists and only accepts POST, but returns `402` for every payload shape,

including an empty one — so it is gated before validation and cannot be used as a

recovery path on our plan.

### E7 — The internal route the web app uses rejects a PAT

```

GET /api/workspaces/{slug}/projects/{id}/work-item-types/ (no /v1 segment)

→ 401 {“detail”:“Authentication credentials were not provided.”}

```

So we cannot replicate whatever call the UI’s **Enable** button performs.

### E8 — UI copy that closes the loop

- `Project settings > Work item Types` (before enabling), confirmation dialog:

**“Once enabled, Work item Types can’t be disabled.”**

- `Project settings > Features > Epics` (current builds): the page has **no toggle**,

only the notice *"Epics now live under Work Item Types. Configure epic properties,

layouts, and defaults from the Work Item Types page. Your existing epics aren’t

affected."* This makes the state look enabled when it is not, and gives an operator

no way to fix an affected project.

## What we tried (all failed)

| Attempt | Result |

|—|—|

| `PATCH` project with 5 candidate epic feature fields (E5) | `200`, silently ignored |

| `POST /work-item-types/` with `level:1, is_epic:true` (E4) | `201`, stored as level 0 |

| Same, with the full field set copied from a working Epic type | `201`, stored as level 0 |

| `PATCH` an existing type to `level:1, is_epic:true` | `200`, ignored |

| Invented shapes (`{“type”:“EPIC”}`, `{“kind”:“epic”}`) | `201`, plain level-0 type |

| `POST /projects/{id}/import-work-item-types/` (E6) | `402 Payment required` (`error_code 1999`) |

| Internal (non-`/v1`) route with PAT (E7) | `401` |

| Waiting for async creation (4s, 16s) | no change |

| `GET /projects/{id}/epics/` as a detection probe | returns `200` + empty list whether the feature is on or off; only `POST` distinguishes |

## Expected behavior

Any one of these would resolve it, in our order of preference:

1. **`PATCH is_issue_type_enabled: true` should create the same types the UI creates**

(`Task` **and** `Epic`). Same feature, same outcome, regardless of client.

2. Expose a documented way to enable epics for a project via API v1 — either a real

project field (e.g. `is_epic_enabled`) that is readable *and* writable, or an

endpoint such as `POST /projects/{id}/epics/enable/`.

3. Allow `POST /work-item-types/` to honour `level: 1` / `is_epic: true` — or reject

it with `400` instead of returning `201` and silently downgrading to level 0.

4. At minimum: make the project resource expose the epic state so automation can

*detect* the broken state, and make it recoverable without deleting the project.

## Secondary issue: unknown fields are silently accepted

`PATCH /projects/{id}/` returns `200 OK` for fields that do not exist on the

serializer (E5), changing nothing. The same applies to `level` / `is_epic` on work

item types (E4). This turns every typo and every version drift into a silent no-op:

we only discovered the original problem because we re-read the resource after every

write. Returning `400` for unknown or non-writable fields would have surfaced all of

this immediately.

## Workaround we adopted

Our provisioning script no longer writes `is_issue_type_enabled` at all. It only

**reads** it, and stops with an instruction when the feature is off:

> Work item Types is disabled. Do **not** enable it via API: the PATCH creates only

> the `Task` type, while enabling through the UI creates `Task` + `Epic` — and since

> the feature cannot be disabled afterwards, enabling it here leaves the project

> permanently without the native Epic. Enable it in

> `Project settings > Work item Types > Enable`, then re-run.

So the required order for a new project is: create the project → **enable Work item

Types in the UI (human step)** → run the automation, which does everything else. We

would very much like to remove that human step.

## Questions

1. Is the UI’s **Enable** action expected to create the `Epic` type as a side effect?

If so, can the API path be made to do the same?

2. Is there any supported way, on the Pro plan, to add native Epics to a project that

was enabled through the API — without deleting and recreating the project?

3. Is `import-work-item-types` intended to be plan-gated (`402`), and which plan

includes it?

4. Does `Features > Epics` still have a toggle in any current build, or has it been

fully replaced by the Work Item Types page?

Someone can help me with this???
I am running tests to adopt the tool at our company, but this is a blocker for proceeding with the purchase.

I need to be able to enable this via the API, just as I can through the UI.

Additionally, I noticed that I cannot manipulate or update project pages via the API; that is also a major issue.

Thanks for the detailed writeup, @cleybson

Yes, enabling Work Item Types through the API should create the Epic type as well. We’ve identified the issue and our team is working on a fix.

As for adding native Epic to projects where Work Item Types was enabled via API, we’re working out how to approach this holistically and will get back to you on this.

Import Work Item Types is an Enterprise Grid plan feature. Epics are now found under Work Item Types, and the toggle can be accessed via Work Item Types > Epic.