Modules overview
A module groups work items into a single deliverable inside one project — a launch, a migration, a redesign. Modules belong to exactly one project and are never shared across projects.
Where a cycle answers "what are we doing this sprint", a module answers "what does this piece of work consist of". A work item can sit in a module and a cycle at the same time.
The module object
Attributes
idstring (uuid)Unique identifier for the module.
namestringDisplay name, unique within the project. Maximum 255 characters.
descriptionstringPlain-text summary of what the module covers.
description_htmlstring or nullThe module's rich-text description, as HTML, or
null. Read-only through the API, which writes onlydescription. Omitted from list rows unless?fields=names it.statusstringWhere the module sits in its lifecycle. One of
backlog,planned,in-progress,paused,completed, orcancelled.start_datestring (date)Date the module is scheduled to begin, as
YYYY-MM-DD, ornull.target_datestring (date)Date the module is expected to land, as
YYYY-MM-DD, ornull.lead_idstring (uuid)The project member accountable for the module, or
null.member_idsarray of stringProject members assigned to the module. Read-only in v2 — module membership is not yet writable through the v2 API.
sort_ordernumberOrdering weight used when modules are listed. Lower values sort first.
logo_propsanyFree-form JSON object holding the icon Plane renders for the module.
{}when no icon is set.external_id,external_sourcestringCorrelation fields for sync and import. Together they let you map a module to a record in another system and find it again later.
total_issuesintegerThe number of live work items in the module, across every state group. Omitted from list rows unless
?fields=names it. See Progress counts.backlog_issues,unstarted_issues,started_issues,completed_issues,cancelled_issuesintegerThe number of live work items in the module whose state belongs to that group. Omitted from list rows unless
?fields=names them.archived_atstring (date-time) or nullWhen the module was archived, or
nullfor an active module. Set by Archive a module and cleared by Unarchive a module.created_atstring (date-time)When the module was created.
updated_atstring (date-time)When the module was last updated.
created_by_idstring (uuid)The user who created the module.
updated_by_idstring (uuid) or nullThe user who last updated the module.
nulluntil someone updates it.
Expand the lead and members
lead_id and member_ids come back as ids. Add ?expand=lead,members to embed the user objects next to them, or resolve the ids against project members. See Expanding relations.
{
"id": "7c1f3d90-2a64-4e58-9b0d-3fa1c7e28b45",
"name": "Billing revamp",
"description": "Rework subscription billing end to end.",
"description_html": "<p>Rework subscription billing end to end.</p>",
"start_date": "2026-01-05",
"target_date": "2026-02-27",
"status": "in-progress",
"lead_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"member_ids": ["16c61a3a-512a-48ac-b0be-b6b46fe6f430", "9d3e1f27-8b4c-4a06-95f1-2c7ea45b0d18"],
"sort_order": 65535.0,
"external_id": null,
"external_source": null,
"logo_props": {},
"total_issues": 24,
"backlog_issues": 3,
"unstarted_issues": 5,
"started_issues": 6,
"completed_issues": 9,
"cancelled_issues": 1,
"archived_at": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"updated_at": "2026-01-14T09:22:41.478363Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"updated_by_id": null
}Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v2/workspaces/{slug}/projects/{project_id}/modules/ | List modules |
GET | /api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/ | Get a module |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/ | Create a module |
PATCH | /api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/ | Update a module |
DELETE | /api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/ | Delete a module |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/upsert/ | Upsert a module |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/bulk-create/ | Bulk create modules |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/bulk-update/ | Bulk update modules |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/bulk-delete/ | Bulk delete modules |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/work-items/ | Add or remove module work items |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/archive/ | Archive a module |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/modules/{pk}/unarchive/ | Unarchive a module |
Status
status is the module's lifecycle position and is validated on both writes and the status filter — a value outside the enum is a 400 invalid_request, never a silently ignored write or an empty list.
| Value | Meaning |
|---|---|
backlog | Captured, not yet committed to |
planned | Committed to but not started |
in-progress | Actively being worked on |
paused | Started, then put on hold |
completed | Delivered |
cancelled | Dropped without delivering |
A module created without a status starts as planned.
Lead and members
lead_id must be a member of the module's project. Passing a user who isn't a project member is rejected with 400 invalid_request naming lead_id — the link is never made silently.
member_ids is read-only. Reads return the module's current members, but there is no v2 write path for adding or removing them yet.
Dates
start_date and target_date are plain dates (YYYY-MM-DD), and either can be null. A target_date earlier than start_date is rejected with 400 invalid_request.
Names are unique per project
Two modules in the same project cannot share a name. A duplicate on create — or on a rename via PATCH — returns 409 conflict.
Progress counts
Module reads carry six counts of the work items in the module: total_issues, backlog_issues, unstarted_issues, started_issues, completed_issues, and cancelled_issues.
Only live work items count. A work item is left out when it has been removed from the module, archived, or deleted, or when it is still a draft. total_issues counts every live work item, and each group count covers one state group. A work item in a triage state counts toward total_issues and toward none of the five groups, so the five can add up to less than the total.
Single-object responses (get, create, update, and upsert) include the counts and description_html. List rows leave them out. Name them in ?fields=, or pass ?fields=all, to get them on list rows:
GET .../modules/?fields=id,name,status,total_issues,completed_issuesArchiving
Archive a module sets archived_at, and Unarchive a module clears it. Only a module whose status is completed or cancelled can be archived. Any other status returns 409 conflict.
Archived modules drop out of List modules by default. Pass ?is_archived=true to list only archived modules, or ?include_archived=true to list archived and active modules together. Get a module returns an archived module by id either way.
Module work items
Add work items to a module or remove them with Add or remove module work items, and list a module's work items by filtering List work items by module_id.
Changed from v1
leadis nowlead_id,membersis nowmember_ids,created_byis nowcreated_by_id, andupdated_byis nowupdated_by_id.member_idsis read-only.- Reads no longer return
project,workspace,view_props, ordescription_text.description_htmlis read-only. description_htmland the progress counts are omitted from list rows unless?fields=names them. v1 returns them on every row.- Archived modules are listed with
?is_archived=trueon the regular list instead of a separatearchived-modules/route. Unarchive isPOST …/modules/{pk}/unarchive/instead ofDELETE …/archived-modules/{pk}/unarchive/. - Archiving a module that is not
completedorcancelledreturns409 conflict. v1 returns400. PUTis gone — updates arePATCHand partial.
See Migrating from v1 for the full list.

