Skip to content

Commit f087b61

Browse files
mguptahubPlane AI
andauthored
[INFRA-395] feat: add Workflows and ProjectTemplates API resources (#45)
* feat: add Workflows and ProjectTemplates API resources Adds project-level Workflows (with WorkflowStates and WorkflowTransitions sub-resources) and ProjectTemplates (WorkItem and Page templates) to the Node SDK, matching INFRA-395 parity with the Python SDK. - src/api/Workflows/ — list, create, update; states attach/detach; transitions list, create, update, del - src/api/ProjectTemplates/ — work item templates and page templates CRUD - src/models/Workflow.ts, ProjectTemplate.ts — TypeScript interfaces and DTOs - Registered on PlaneClient and exported from index - Unit tests for both resources - README API resources section updated Co-authored-by: Plane AI <noreply@plane.so> * fix: address CodeRabbit review on Workflows and ProjectTemplates - Transitions.ts: harden error.response parsing in create() — defensive type check instead of JSON.stringify to avoid throw on non-serializable payloads - Workflow.ts: derive CreateWorkflow/UpdateWorkflow and CreateWorkflowTransition/UpdateWorkflowTransition from model interfaces via Pick/Partial/Required instead of duplicating fields - ProjectTemplate.ts: same — derive Create/Update DTOs from WorkItemTemplate and PageTemplate via Pick/Partial - Tests: replace resolves.not.toThrow() with resolves.toBeUndefined() for Promise<void> assertions (workflow + project-templates) Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so>
1 parent 18cbaae commit f087b61

14 files changed

Lines changed: 674 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ const project = await client.projects.create("workspace-slug", {
7474
- **Intake**: Intake form and request management
7575
- **Stickies**: Stickies management
7676
- **Teamspaces**: Teamspace management
77+
- **Milestones**: Milestone tracking and management
7778
- **Initiatives**: Initiative management
79+
- **AgentRuns**: AI agent run orchestration and activity tracking
80+
- **Workflows**: Project workflow management with state attachments and transitions
81+
- **ProjectTemplates**: Work item and page template management per project
7882
- **Features**: Workspace and project features management
7983

8084
## Development

src/api/ProjectTemplates/Pages.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BaseResource } from "../BaseResource";
2+
import { Configuration } from "../../Configuration";
3+
import { CreatePageTemplate, PageTemplate, UpdatePageTemplate } from "../../models/ProjectTemplate";
4+
5+
/**
6+
* ProjectPageTemplates sub-resource
7+
* Manages page templates within a project
8+
*/
9+
export class Pages extends BaseResource {
10+
constructor(config: Configuration) {
11+
super(config);
12+
}
13+
14+
/**
15+
* List all page templates for a project
16+
*/
17+
async list(workspaceSlug: string, projectId: string): Promise<PageTemplate[]> {
18+
const data = await this.get<PageTemplate[] | { results: PageTemplate[] }>(
19+
`/workspaces/${workspaceSlug}/projects/${projectId}/pages/templates/`
20+
);
21+
return Array.isArray(data) ? data : data.results;
22+
}
23+
24+
/**
25+
* Create a new page template for a project
26+
*/
27+
async create(workspaceSlug: string, projectId: string, data: CreatePageTemplate): Promise<PageTemplate> {
28+
return this.post<PageTemplate>(`/workspaces/${workspaceSlug}/projects/${projectId}/pages/templates/`, data);
29+
}
30+
31+
/**
32+
* Update a page template by ID
33+
*/
34+
async update(
35+
workspaceSlug: string,
36+
projectId: string,
37+
templateId: string,
38+
data: UpdatePageTemplate
39+
): Promise<PageTemplate> {
40+
return this.patch<PageTemplate>(
41+
`/workspaces/${workspaceSlug}/projects/${projectId}/pages/templates/${templateId}/`,
42+
data
43+
);
44+
}
45+
46+
/**
47+
* Delete a page template by ID
48+
*/
49+
async del(workspaceSlug: string, projectId: string, templateId: string): Promise<void> {
50+
return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/pages/templates/${templateId}/`);
51+
}
52+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BaseResource } from "../BaseResource";
2+
import { Configuration } from "../../Configuration";
3+
import { CreateWorkItemTemplate, UpdateWorkItemTemplate, WorkItemTemplate } from "../../models/ProjectTemplate";
4+
5+
/**
6+
* ProjectWorkItemTemplates sub-resource
7+
* Manages work item templates within a project
8+
*/
9+
export class WorkItems extends BaseResource {
10+
constructor(config: Configuration) {
11+
super(config);
12+
}
13+
14+
/**
15+
* List all work item templates for a project
16+
*/
17+
async list(workspaceSlug: string, projectId: string): Promise<WorkItemTemplate[]> {
18+
const data = await this.get<WorkItemTemplate[] | { results: WorkItemTemplate[] }>(
19+
`/workspaces/${workspaceSlug}/projects/${projectId}/workitems/templates/`
20+
);
21+
return Array.isArray(data) ? data : data.results;
22+
}
23+
24+
/**
25+
* Create a new work item template for a project
26+
*/
27+
async create(workspaceSlug: string, projectId: string, data: CreateWorkItemTemplate): Promise<WorkItemTemplate> {
28+
return this.post<WorkItemTemplate>(`/workspaces/${workspaceSlug}/projects/${projectId}/workitems/templates/`, data);
29+
}
30+
31+
/**
32+
* Update a work item template by ID
33+
*/
34+
async update(
35+
workspaceSlug: string,
36+
projectId: string,
37+
templateId: string,
38+
data: UpdateWorkItemTemplate
39+
): Promise<WorkItemTemplate> {
40+
return this.patch<WorkItemTemplate>(
41+
`/workspaces/${workspaceSlug}/projects/${projectId}/workitems/templates/${templateId}/`,
42+
data
43+
);
44+
}
45+
46+
/**
47+
* Delete a work item template by ID
48+
*/
49+
async del(workspaceSlug: string, projectId: string, templateId: string): Promise<void> {
50+
return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/workitems/templates/${templateId}/`);
51+
}
52+
}

src/api/ProjectTemplates/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { BaseResource } from "../BaseResource";
2+
import { Configuration } from "../../Configuration";
3+
import { WorkItems } from "./WorkItems";
4+
import { Pages } from "./Pages";
5+
6+
/**
7+
* ProjectTemplates API resource
8+
* Container for work item and page template sub-resources
9+
*/
10+
export class ProjectTemplates extends BaseResource {
11+
public workItems: WorkItems;
12+
public pages: Pages;
13+
14+
constructor(config: Configuration) {
15+
super(config);
16+
this.workItems = new WorkItems(config);
17+
this.pages = new Pages(config);
18+
}
19+
}

src/api/Workflows/States.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { BaseResource } from "../BaseResource";
2+
import { Configuration } from "../../Configuration";
3+
import { AttachWorkflowStates } from "../../models/Workflow";
4+
5+
/**
6+
* WorkflowStates sub-resource
7+
* Manages state attachments on a workflow
8+
*/
9+
export class States extends BaseResource {
10+
constructor(config: Configuration) {
11+
super(config);
12+
}
13+
14+
/**
15+
* Attach states to a workflow
16+
*/
17+
async attach(
18+
workspaceSlug: string,
19+
projectId: string,
20+
workflowId: string,
21+
data: AttachWorkflowStates
22+
): Promise<void> {
23+
return this.post<void>(`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/states/`, data);
24+
}
25+
26+
/**
27+
* Detach a state from a workflow
28+
*/
29+
async detach(workspaceSlug: string, projectId: string, workflowId: string, stateId: string): Promise<void> {
30+
return this.httpDelete(
31+
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/states/${stateId}/`
32+
);
33+
}
34+
}

src/api/Workflows/Transitions.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { BaseResource } from "../BaseResource";
2+
import { Configuration } from "../../Configuration";
3+
import { HttpError } from "../../errors";
4+
import { CreateWorkflowTransition, UpdateWorkflowTransition, WorkflowTransition } from "../../models/Workflow";
5+
6+
/**
7+
* WorkflowTransitions sub-resource
8+
* Manages state transitions within a workflow
9+
*/
10+
export class Transitions extends BaseResource {
11+
constructor(config: Configuration) {
12+
super(config);
13+
}
14+
15+
/**
16+
* List all state transitions for a workflow
17+
*/
18+
async list(workspaceSlug: string, projectId: string, workflowId: string): Promise<WorkflowTransition[]> {
19+
const data = await this.get<WorkflowTransition[] | { results: WorkflowTransition[] }>(
20+
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/state-transitions/`
21+
);
22+
return Array.isArray(data) ? data : data.results;
23+
}
24+
25+
/**
26+
* Create a state transition for a workflow.
27+
* Returns null if the transition already exists (HTTP 400 "already exists").
28+
*/
29+
async create(
30+
workspaceSlug: string,
31+
projectId: string,
32+
workflowId: string,
33+
data: CreateWorkflowTransition
34+
): Promise<WorkflowTransition | null> {
35+
try {
36+
return await this.post<WorkflowTransition>(
37+
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/state-transitions/`,
38+
data
39+
);
40+
} catch (error) {
41+
if (error instanceof HttpError && error.statusCode === 400) {
42+
const response = error.response as unknown;
43+
const body =
44+
typeof response === "string"
45+
? response.toLowerCase()
46+
: typeof response === "object" &&
47+
response !== null &&
48+
"detail" in response &&
49+
typeof (response as { detail: unknown }).detail === "string"
50+
? (response as { detail: string }).detail.toLowerCase()
51+
: "";
52+
if (body.includes("already exists")) {
53+
return null;
54+
}
55+
}
56+
throw error;
57+
}
58+
}
59+
60+
/**
61+
* Update a workflow state transition
62+
*/
63+
async update(
64+
workspaceSlug: string,
65+
projectId: string,
66+
workflowId: string,
67+
transitionId: string,
68+
data: UpdateWorkflowTransition
69+
): Promise<WorkflowTransition> {
70+
return this.patch<WorkflowTransition>(
71+
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/state-transitions/${transitionId}/`,
72+
data
73+
);
74+
}
75+
76+
/**
77+
* Delete a workflow state transition
78+
*/
79+
async del(workspaceSlug: string, projectId: string, workflowId: string, transitionId: string): Promise<void> {
80+
return this.httpDelete(
81+
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/state-transitions/${transitionId}/`
82+
);
83+
}
84+
}

src/api/Workflows/index.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { BaseResource } from "../BaseResource";
2+
import { Configuration } from "../../Configuration";
3+
import { CreateWorkflow, UpdateWorkflow, Workflow } from "../../models/Workflow";
4+
import { States } from "./States";
5+
import { Transitions } from "./Transitions";
6+
7+
/**
8+
* Workflows API resource
9+
* Handles project workflow operations and exposes states/transitions sub-resources
10+
*/
11+
export class Workflows extends BaseResource {
12+
public states: States;
13+
public transitions: Transitions;
14+
15+
constructor(config: Configuration) {
16+
super(config);
17+
this.states = new States(config);
18+
this.transitions = new Transitions(config);
19+
}
20+
21+
/**
22+
* List all workflows for a project
23+
*/
24+
async list(workspaceSlug: string, projectId: string): Promise<Workflow[]> {
25+
const data = await this.get<Workflow[] | { results: Workflow[] }>(
26+
`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/`
27+
);
28+
return Array.isArray(data) ? data : data.results;
29+
}
30+
31+
/**
32+
* Create a new workflow for a project
33+
*/
34+
async create(workspaceSlug: string, projectId: string, data: CreateWorkflow): Promise<Workflow> {
35+
return this.post<Workflow>(`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/`, data);
36+
}
37+
38+
/**
39+
* Update a workflow by ID
40+
*/
41+
async update(workspaceSlug: string, projectId: string, workflowId: string, data: UpdateWorkflow): Promise<Workflow> {
42+
return this.patch<Workflow>(`/workspaces/${workspaceSlug}/projects/${projectId}/workflows/${workflowId}/`, data);
43+
}
44+
}

src/client/plane-client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { Teamspaces } from "../api/Teamspaces";
2020
import { Initiatives } from "../api/Initiatives";
2121
import { Milestones } from "../api/Milestones";
2222
import { AgentRuns } from "../api/AgentRuns";
23+
import { Workflows } from "../api/Workflows";
24+
import { ProjectTemplates } from "../api/ProjectTemplates";
2325

2426
/**
2527
* Main Plane Client class
@@ -48,6 +50,8 @@ export class PlaneClient {
4850
public milestones: Milestones;
4951
public initiatives: Initiatives;
5052
public agentRuns: AgentRuns;
53+
public workflows: Workflows;
54+
public projectTemplates: ProjectTemplates;
5155

5256
constructor(config: { baseUrl?: string; apiKey?: string; accessToken?: string; enableLogging?: boolean }) {
5357
this.config = new Configuration({
@@ -82,5 +86,7 @@ export class PlaneClient {
8286
this.milestones = new Milestones(this.config);
8387
this.initiatives = new Initiatives(this.config);
8488
this.agentRuns = new AgentRuns(this.config);
89+
this.workflows = new Workflows(this.config);
90+
this.projectTemplates = new ProjectTemplates(this.config);
8591
}
8692
}

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export { Teamspaces } from "./api/Teamspaces";
3232
export { Milestones } from "./api/Milestones";
3333
export { Initiatives } from "./api/Initiatives";
3434
export { AgentRuns } from "./api/AgentRuns";
35+
export { Workflows } from "./api/Workflows";
36+
export { ProjectTemplates } from "./api/ProjectTemplates";
3537

3638
// Sub-resources
3739
export { Relations as WorkItemRelations } from "./api/WorkItems/Relations";
@@ -49,6 +51,10 @@ export { Labels as InitiativeLabels } from "./api/Initiatives/Labels";
4951
export { Projects as InitiativeProjects } from "./api/Initiatives/Projects";
5052
export { Epics as InitiativeEpics } from "./api/Initiatives/Epics";
5153
export { Activities as AgentRunActivities } from "./api/AgentRuns/Activities";
54+
export { States as WorkflowStates } from "./api/Workflows/States";
55+
export { Transitions as WorkflowTransitions } from "./api/Workflows/Transitions";
56+
export { WorkItems as ProjectWorkItemTemplates } from "./api/ProjectTemplates/WorkItems";
57+
export { Pages as ProjectPageTemplates } from "./api/ProjectTemplates/Pages";
5258

5359
// Models
5460
export * from "./models";

src/models/ProjectTemplate.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { BaseModel } from "./common";
2+
3+
/**
4+
* Project template model interfaces
5+
*/
6+
export interface WorkItemTemplate extends BaseModel {
7+
name: string;
8+
short_description?: string;
9+
template_data?: Record<string, unknown>;
10+
template_type?: string;
11+
project: string;
12+
workspace: string;
13+
}
14+
15+
export type CreateWorkItemTemplate = Pick<WorkItemTemplate, "name" | "short_description" | "template_data">;
16+
17+
export type UpdateWorkItemTemplate = Partial<CreateWorkItemTemplate>;
18+
19+
export interface PageTemplate extends BaseModel {
20+
name: string;
21+
short_description?: string;
22+
template_data?: Record<string, unknown>;
23+
template_type?: string;
24+
project: string;
25+
workspace: string;
26+
}
27+
28+
export type CreatePageTemplate = Pick<PageTemplate, "name" | "short_description" | "template_data">;
29+
30+
export type UpdatePageTemplate = Partial<CreatePageTemplate>;

0 commit comments

Comments
 (0)