Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,37 @@ state = client.states.update(
client.states.delete(workspace_slug, project_id, state_id)
```

#### Workspace States

Workspace-level work-item states. Reads are dual-mode: under workspace
governance they serve the workspace states catalog; in ungoverned workspaces
they aggregate the states of every project the caller can access. Writes
require the workspace to own states and workflows (check
`client.workspaces.get_features(workspace_slug).states_owned_by_workspace`).

```python
# List states at workspace scope (works in both modes)
states = client.workspace_states.list(workspace_slug)

# Create a workspace (catalog) state — governed workspaces only
from plane.models.states import CreateWorkspaceState

state = client.workspace_states.create(
workspace_slug,
data=CreateWorkspaceState(name="In Review", color="#3b82f6", group="started"),
)

# Retrieve / update / delete
state = client.workspace_states.retrieve(workspace_slug, state_id)

from plane.models.states import UpdateWorkspaceState

state = client.workspace_states.update(
workspace_slug, state_id, data=UpdateWorkspaceState(color="#22c55e")
)
client.workspace_states.delete(workspace_slug, state_id)
```

#### Labels

```python
Expand Down Expand Up @@ -737,6 +768,98 @@ wit = client.work_item_types.update(
client.work_item_types.delete(workspace_slug, project_id, type_id)
```

#### Workspace Workflows

The workspace workflow catalog (workspace governance). `list` is dual-mode;
all writes require the workspace to own states and workflows.

```python
# List workspace workflows
workflows = client.workspace_workflows.list(workspace_slug)

# Create a workflow draft, then configure its chain from catalog states
from plane.models.states import CreateWorkspaceState
from plane.models.workspace_workflows import (
AddWorkspaceWorkflowStates,
CreateWorkspaceWorkflow,
CreateWorkspaceWorkflowTransition,
)

state_a = client.workspace_states.create(
workspace_slug, data=CreateWorkspaceState(name="Todo", color="#94a3b8", group="unstarted")
)
state_b = client.workspace_states.create(
workspace_slug, data=CreateWorkspaceState(name="Doing", color="#3b82f6", group="started")
)
workflow = client.workspace_workflows.create(
workspace_slug, data=CreateWorkspaceWorkflow(name="Engineering")
)
client.workspace_workflows.states.add(
workspace_slug,
workflow.id,
data=AddWorkspaceWorkflowStates(state_ids=[state_a.id, state_b.id]),
)
client.workspace_workflows.states.mark_default(workspace_slug, workflow.id, state_a.id)

# Transitions
transition = client.workspace_workflows.transitions.create(
workspace_slug,
workflow.id,
data=CreateWorkspaceWorkflowTransition(state_id=state_a.id, transition_state_id=state_b.id),
)

# Full chain, usage report, and activity log
workflow = client.workspace_workflows.retrieve(workspace_slug, workflow.id)
usage = client.workspace_workflows.usage(workspace_slug, workflow.id)
activities = client.workspace_workflows.activities(workspace_slug, workflow.id)

# Transition hooks (validation/action hooks, webhook secrets, executions)
hooks = client.workspace_workflows.hooks.list(workspace_slug, workflow.id, transition.id)
```

#### Work Item Type Governance

Governs which workflows a workspace-level work item type may use
(`any` / `constrained` / `required` modes, allowlists, and per-project pins).
Workspace governance only.

```python
# type_id: UUID of a workspace work item type; workflow_id: UUID of a
# workspace workflow (e.g. workflow.id from the example above)

# Read and change a type's governance
governance = client.work_item_type_governance.retrieve(workspace_slug, type_id)

from plane.models.work_item_type_governance import UpdateTypeGovernance

governance = client.work_item_type_governance.update(
workspace_slug,
type_id,
data=UpdateTypeGovernance(mode="constrained", workflow_ids=[workflow_id]),
)

# Dry-run the impact first
from plane.models.work_item_type_governance import TypeGovernancePreviewRequest

preview = client.work_item_type_governance.preview(
workspace_slug,
type_id,
data=TypeGovernancePreviewRequest(mode="required", required_workflow_id=workflow_id),
)

# Per-project pins
pins = client.work_item_type_governance.pins.list(workspace_slug, type_id)

# Project-side view: each type's effective workflow, and the project's pick
entries = client.work_item_type_governance.project_workflows.list(workspace_slug, project_id)

from plane.models.work_item_type_governance import SetProjectWorkflowPick

client.work_item_type_governance.project_workflows.update_pick(
workspace_slug, project_id, type_id, data=SetProjectWorkflowPick(workflow_id=workflow_id)
)
```

#### Work Item Properties

```python
Expand Down
87 changes: 85 additions & 2 deletions plane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@
from .api.users import Users
from .api.work_item_properties import WorkItemProperties
from .api.work_item_relation_definitions import WorkItemRelationDefinitions
from .api.work_item_type_governance import WorkItemTypeGovernance
from .api.work_item_types import WorkItemTypes
from .api.work_items import WorkItems
from .api.workflows import Workflows, WorkflowStates, WorkflowTransitions
from .api.workflows import (
ProjectWorkflowTransitionHooks,
Workflows,
WorkflowStates,
WorkflowTransitions,
)
from .api.workspace_project_labels import WorkspaceProjectLabels
from .api.workspace_project_states import WorkspaceProjectStates
from .api.workspace_states import WorkspaceStates
from .api.workspace_templates import WorkspaceTemplates
from .api.workspace_work_item_properties import WorkspaceWorkItemProperties
from .api.workspace_work_item_types import WorkspaceWorkItemTypes
from .api.workspace_workflows import WorkspaceWorkflows
from .api.workspaces import Workspaces
from .client import (
OAuthAuthorizationParams,
Expand All @@ -44,16 +52,51 @@
WorkItemTemplate,
)
from .models.projects import ProjectFeature, ProjectMember
from .models.states import CreateWorkspaceState, UpdateWorkspaceState
from .models.work_item_type_governance import (
CreateWorkItemTypeWorkflowPins,
GovernancePreview,
ProjectTypeWorkflow,
ProjectWorkflowPickResult,
SetProjectWorkflowPick,
TypeGovernance,
TypeGovernancePreviewRequest,
UpdateTypeGovernance,
WorkflowFallbackPreviewRequest,
WorkItemTypeWorkflowPin,
)
from .models.workflows import (
AttachWorkflowStates,
CreateWorkflow,
CreateWorkflowTransition,
CreateWorkflowTransitionHook,
SubmitWorkItemApproval,
UpdateWorkflow,
UpdateWorkflowState,
UpdateWorkflowTransition,
UpdateWorkflowTransitionHook,
Workflow,
WorkflowActivity,
WorkflowTransition,
WorkflowTransitionHook,
WorkItemApprovalResult,
)
from .models.workspace_workflows import (
AddWorkspaceWorkflowStates,
CreateWorkspaceWorkflow,
CreateWorkspaceWorkflowTransition,
PaginatedWorkspaceWorkflowResponse,
RemoveWorkspaceWorkflowState,
UpdateWorkspaceWorkflow,
UpdateWorkspaceWorkflowState,
UpdateWorkspaceWorkflowTransition,
WorkspaceWorkflow,
WorkspaceWorkflowState,
WorkspaceWorkflowTransition,
WorkspaceWorkflowUsage,
WorkspaceWorkflowUsageProject,
)
Comment thread
sunder-ch marked this conversation as resolved.
from .models.workspaces import WorkspaceMember
from .models.workspaces import WorkspaceFeature, WorkspaceMember

__all__ = [
"PlaneClient",
Expand Down Expand Up @@ -83,12 +126,16 @@
"ProjectTemplates",
"ProjectWorkItemTemplates",
"ProjectPageTemplates",
"ProjectWorkflowTransitionHooks",
"Releases",
"WorkspaceTemplates",
"WorkspaceWorkItemTypes",
"WorkspaceWorkItemProperties",
"WorkspaceProjectLabels",
"WorkspaceProjectStates",
"WorkspaceStates",
"WorkspaceWorkflows",
"WorkItemTypeGovernance",
"PlaneError",
"ConfigurationError",
"HttpError",
Expand All @@ -102,11 +149,47 @@
"CreateWorkflow",
"UpdateWorkflow",
"AttachWorkflowStates",
"UpdateWorkflowState",
"WorkflowActivity",
"WorkflowTransition",
"CreateWorkflowTransition",
"UpdateWorkflowTransition",
"WorkflowTransitionHook",
"CreateWorkflowTransitionHook",
"UpdateWorkflowTransitionHook",
"SubmitWorkItemApproval",
"WorkItemApprovalResult",
# Workspace state models
"CreateWorkspaceState",
"UpdateWorkspaceState",
# Workspace workflow models
"WorkspaceWorkflow",
"WorkspaceWorkflowState",
"WorkspaceWorkflowTransition",
"PaginatedWorkspaceWorkflowResponse",
"CreateWorkspaceWorkflow",
"UpdateWorkspaceWorkflow",
"AddWorkspaceWorkflowStates",
"UpdateWorkspaceWorkflowState",
"RemoveWorkspaceWorkflowState",
"CreateWorkspaceWorkflowTransition",
"UpdateWorkspaceWorkflowTransition",
"WorkspaceWorkflowUsage",
"WorkspaceWorkflowUsageProject",
# Type governance models
"TypeGovernance",
"UpdateTypeGovernance",
"TypeGovernancePreviewRequest",
"GovernancePreview",
"WorkItemTypeWorkflowPin",
"CreateWorkItemTypeWorkflowPins",
"ProjectTypeWorkflow",
"SetProjectWorkflowPick",
"ProjectWorkflowPickResult",
"WorkflowFallbackPreviewRequest",
"ProjectFeature",
"ProjectMember",
"WorkspaceFeature",
"WorkspaceMember",
# Project template models
"WorkItemTemplate",
Expand Down
3 changes: 3 additions & 0 deletions plane/api/work_item_type_governance/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .base import WorkItemTypeGovernance

__all__ = ["WorkItemTypeGovernance"]
79 changes: 79 additions & 0 deletions plane/api/work_item_type_governance/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from typing import Any

from ...models.work_item_type_governance import (
GovernancePreview,
TypeGovernance,
TypeGovernancePreviewRequest,
UpdateTypeGovernance,
)
from ..base_resource import BaseResource
from .pins import WorkItemTypeWorkflowPins
from .project_workflows import ProjectTypeWorkflows


class WorkItemTypeGovernance(BaseResource):
"""API client for work item type governance (workspace governance only).

Governs which workflows a workspace-level work item type may use
(``any`` / ``constrained`` / ``required`` modes and allowlists). Per-project
pins live on ``.pins``; the project-side view of effective workflows and
picks lives on ``.project_workflows``. Every endpoint requires the workspace
to own states and workflows — otherwise the API responds 400 with code
``workspace_not_managed``.
"""

def __init__(self, config: Any) -> None:
super().__init__(config, "/workspaces/")

# Initialize sub-resources
self.pins = WorkItemTypeWorkflowPins(config)
self.project_workflows = ProjectTypeWorkflows(config)

def retrieve(self, workspace_slug: str, type_id: str) -> TypeGovernance:
"""Retrieve a type's governance settings (mode, required workflow, allowlist).

Args:
workspace_slug: The workspace slug identifier
type_id: UUID of the workspace work item type
"""
response = self._get(f"{workspace_slug}/work-item-types/{type_id}/governance/")
return TypeGovernance.model_validate(response)

def update(
self, workspace_slug: str, type_id: str, data: UpdateTypeGovernance
) -> TypeGovernance:
"""Update a type's governance mode / allowlist / required workflow.

Destructive changes (dropping in-use workflows, mandating one) require
``data.acknowledge`` and may need a ``data.state_mapping`` for orphaned
work items.

Args:
workspace_slug: The workspace slug identifier
type_id: UUID of the workspace work item type
data: The governance change
"""
response = self._patch(
f"{workspace_slug}/work-item-types/{type_id}/governance/",
data.model_dump(exclude_none=True),
)
return TypeGovernance.model_validate(response)

def preview(
self, workspace_slug: str, type_id: str, data: TypeGovernancePreviewRequest
) -> GovernancePreview:
"""Dry-run a governance change and report affected work items (no writes).

Args:
workspace_slug: The workspace slug identifier
type_id: UUID of the workspace work item type
data: The governance change to preview
"""
response = self._post(
f"{workspace_slug}/work-item-types/{type_id}/governance/preview/",
data.model_dump(exclude_none=True),
)
payload = response.get("preview", response) if isinstance(response, dict) else response
return GovernancePreview.model_validate(payload)
Loading