Skip to content

Update a work item type workflow ​

PATCH/api/v2/workspaces/{slug}/projects/{project_id}/work-item-types/{type_id}/workflow/

Set the project's pick for one work item type. The pick is the catalog workflow that the project's work items of that type follow. The response is the type's updated work item type workflow entry.

Work items of the type whose state the new workflow doesn't contain have to move. If any exist, send a state_mapping that covers their states, or the request returns 409 workflow_state_orphans and changes nothing. Add ?dry_run=true first to see which states need mapping.

The workspace can take this decision away from the project. Under required mode, the request returns 403 workflow_mandated. When the workspace has pinned the type's workflow for this project, it returns 403 workflow_assigned_by_workspace. See Work item type governance.

PATCH, not PUT

v1 sets the pick with PUT on the same path. v2 accepts the same body on PATCH.

Path Parameters ​

slug:requiredstring

The workspace slug. It appears in your Plane URLs. In https://app.plane.so/my-team/projects/, the slug is my-team.

project_id:requiredstring

The project. Accepts the project UUID or its bare identifier, for example ENG.

type_id:requiredstring (uuid)

The work item type whose workflow you are setting.

Query Parameters ​

dry_run:optionalboolean

Set to true to preview the change instead of applying it. The request runs every check up to the move, including the 403 and 400 refusals, then returns a preview and writes nothing. Any other value applies the change.

The preview has three fields:

  • total: how many work items of this type would move.
  • type_total: how many work items of this type the project has.
  • per_state: one entry per state the moving work items sit in now, with state_id, state_name, state_group, count, and target_state_id. target_state_id is the new workflow's default state, because the preview ignores state_mapping.

Body Parameters ​

workflow_id:requiredstring (uuid)

The catalog workflow to use. It must be active. Under constrained mode, it must also be on the type's allowlist, or the request returns 400 workflow_outside_allowlist.

state_mapping:optionalobject

Maps a current state id to a state id in the new workflow, for work items whose state the new workflow doesn't contain. Values must be UUIDs. Plane ignores a key that isn't a valid UUID. Every stranded state needs an entry, and every target must be in the new workflow. See The orphan gate.

Scopes ​

projects.workflows:write

Errors ​

StatusCodeCause
400invalid_requestworkflow_id missing or not a UUID, an unknown or inactive workflow, or a non-UUID state_mapping value.
400workflow_outside_allowlistThe type is in constrained mode and the workflow isn't on its allowlist.
401unauthorizedMissing or invalid credentials.
402payment_requiredMultiple workflows aren't on your plan.
403forbiddenYour role or token scope can't edit work item types in this project.
403workflow_mandatedThe type is in required mode, so the workspace decides its workflow.
403workflow_assigned_by_workspaceThe workspace pinned the type's workflow for this project.
404not_foundNo such project, or no such work item type in this workspace.
406not_acceptableThe Accept header asks for a representation the API can't produce.
409states_managed_at_projectThe workspace doesn't have governance turned on, so the project owns its workflows.
409workflow_state_orphansWork items would be stranded and state_mapping doesn't cover their states. The body carries the orphans report.
413payload_too_largeThe request body is over the size limit.
415unsupported_media_typeThe Content-Type isn't one this endpoint accepts.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Update a work item type workflow
bash
curl -X PATCH \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-item-types/d1e7a4c9-2b58-4f36-9a0e-7c3b5d81f240/workflow/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "workflow_id": "e6b2d9f1-48a7-4c3e-9f50-1d7a8c2b6e93",
  "state_mapping": {
    "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f"
  }
}'
Response200
json
{
  "type_id": "d1e7a4c9-2b58-4f36-9a0e-7c3b5d81f240",
  "governance": "constrained",
  "allowlist_count": 2,
  "allowlist_total": 3,
  "effective_workflow_id": "e6b2d9f1-48a7-4c3e-9f50-1d7a8c2b6e93",
  "effective_workflow_name": null,
  "source": "pick",
  "options": [
    {
      "workflow_id": "a1f4c7e2-3b98-4d61-8e25-6c0b9d7f3a14",
      "name": "Standard delivery"
    },
    {
      "workflow_id": "e6b2d9f1-48a7-4c3e-9f50-1d7a8c2b6e93",
      "name": "Regulated release"
    }
  ]
}
Response200
json
{
  "total": 7,
  "type_total": 42,
  "per_state": [
    {
      "state_id": "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53",
      "state_name": "In Review",
      "state_group": "started",
      "count": 7,
      "target_state_id": "2c4d16f8-9b3e-4a52-8d71-1f0e6c9a5b48"
    }
  ]
}
Response403
json
{
  "type": "forbidden",
  "code": "workflow_mandated",
  "detail": "This type's workflow is mandated by the workspace."
}
Response403
json
{
  "type": "forbidden",
  "code": "workflow_assigned_by_workspace",
  "detail": "This type's workflow is pinned by the workspace."
}

What a successful change does ​

  1. Plane writes the project's pick for the type to workflow_id.
  2. Work items of the type in states outside the new workflow move to the states you mapped. Plane records each move on the work item. Archived work items move too. Drafts and work items in triage group states don't.
  3. The response is the type's entry, now with source: "pick" and the new effective_workflow_id.

When the type isn't active in the project, Plane still writes the pick, and the response is only {"workflow_id": "<the workflow you sent>"}.

Map from the dry run

Run the request with ?dry_run=true, map every state_id in per_state to a state in the new workflow, then send the real request with that state_mapping. The dry run reports a target_state_id for each state, but the real request doesn't fall back to it. It needs the mapping.