Cycles overview
A cycle is a time-boxed iteration inside a project — a sprint, a release window, or any fixed period a team commits work to. A cycle belongs to exactly one project and is never shared across projects.
Cycles are defined by a start_date and an end_date, interpreted in the cycle's own timezone. Both dates are optional, so a cycle can exist as an unscheduled container and be given dates later.
The cycle object
Attributes
idstring (uuid)Unique identifier for the cycle.
namestringDisplay name, unique within the project. Maximum 255 characters.
descriptionstringFree-form description of what the cycle covers.
start_datestring (date-time)When the cycle opens.
nullif the cycle has not been scheduled.end_datestring (date-time)When the cycle closes.
nullif the cycle has not been scheduled.timezonestringThe IANA time zone the cycle's dates are interpreted in, for example
America/New_YorkorUTC. This is what makes a cycle boundary land at local midnight rather than UTC midnight for a distributed team.owned_by_idstring (uuid)The user who owns the cycle. Read-only — assigned by Plane and not settable through the API. Use it to filter cycles on list.
sort_ordernumberOrdering weight for the cycle within the project. Lower values sort first when you order by
sort_order.logo_propsanyJSON blob holding the cycle's icon configuration, as written by Plane clients. Passed through unchanged.
external_id,external_sourcestringCorrelation fields for sync and import. Together they let you map a cycle to a record in another system and find it again later — both are exposed as list filters.
total_issuesintegerThe number of live work items in the cycle, 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 cycle whose state belongs to that group. Omitted from list rows unless
?fields=names them.progress_snapshotobjectThe cycle's progress as it stood when its unfinished work items were moved out with Transfer work items between cycles. It holds the six counts at that moment, plus
distribution(label and assignee breakdowns and a completion chart) andestimate_distribution.{}until a transfer runs. Read-only.archived_atstring (date-time) or nullWhen the cycle was archived, or
nullfor an active cycle. Set by Archive a cycle and cleared by Unarchive a cycle. Read-only.created_atstring (date-time)When the cycle was created.
updated_atstring (date-time)When the cycle was last updated.
created_by_idstring (uuid)The user who created the cycle.
updated_by_idstring (uuid) or nullThe user who last updated the cycle.
nulluntil someone updates it.
Dates are timestamps, not calendar days
start_date and end_date are full date-times. Send them as ISO 8601 timestamps and read timezone to interpret where the boundary falls.
{
"id": "7c1f9a4e-3b6d-4a52-8f0c-2d9e6b18c3a7",
"name": "Sprint 24",
"description": "Checkout rewrite and billing cleanup",
"start_date": "2026-01-05T00:00:00Z",
"end_date": "2026-01-19T00:00:00Z",
"owned_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"sort_order": 65535,
"timezone": "America/New_York",
"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,
"progress_snapshot": {},
"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}/cycles/ | List cycles |
GET | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/ | Get a cycle |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/ | Create a cycle |
PATCH | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/ | Update a cycle |
DELETE | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/ | Delete a cycle |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/upsert/ | Upsert a cycle |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/bulk-create/ | Bulk create cycles |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/bulk-update/ | Bulk update cycles |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/bulk-delete/ | Bulk delete cycles |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/work-items/ | Add or remove cycle work items |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/transfer/ | Transfer work items between cycles |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/archive/ | Archive a cycle |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/cycles/{pk}/unarchive/ | Unarchive a cycle |
Names are unique per project
Cycle names must be unique within a project. Creating a second cycle with a name already in use, or renaming a cycle onto an existing name, returns 409 conflict. Uniqueness is scoped to the project, so two different projects can both have a Sprint 24.
Ownership is read-only
owned_by_id is returned on every read but is not part of the write body — you cannot assign a cycle owner through the v2 API. You can still filter by owner on list with ?owned_by_id=, which is the common case for building "my cycles" views.
Expanding the owner
?expand=owned_by adds an owned_by user object next to owned_by_id. It is the only relation you can expand on a cycle. Resolve created_by_id and updated_by_id against the members endpoints when you need names. See Expanding relations.
Progress counts
Cycle reads carry six counts of the work items in the cycle: 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 cycle, 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. List rows leave them out, because they are computed for each request. Name them in ?fields=, or pass ?fields=all, to get them on list rows:
GET .../cycles/?fields=id,name,total_issues,completed_issuesArchiving
Archive a cycle sets archived_at, and Unarchive a cycle clears it. Only a cycle whose end_date has passed can be archived. A running or upcoming cycle, or one with no end_date, returns 409 conflict.
Archived cycles drop out of List cycles by default. Pass ?is_archived=true to list only archived cycles, or ?include_archived=true to list archived and active cycles together. Get a cycle returns an archived cycle by id either way.
Cycle work items
Add work items to a cycle or remove them with Add or remove cycle work items, and move them to another cycle with Transfer work items between cycles. To list a cycle's work items, filter List work items by cycle_id.
Changed from v1
owned_byis nowowned_by_id,created_byis nowcreated_by_id, andupdated_byis nowupdated_by_id.start_dateandend_dateare date-times, not plain calendar dates.- Reads no longer return
project,workspace, orview_props. - The progress counts are omitted from list rows unless
?fields=names them. v1 returns them on every row. - The list endpoint drops
cycle_viewand addssearch,name,owned_by_id,external_id,external_source,is_archived, andinclude_archivedfilters. - Archived cycles are listed with
?is_archived=trueon the regular list instead of a separatearchived-cycles/route. Unarchive isPOST …/cycles/{pk}/unarchive/instead ofDELETE …/archived-cycles/{cycle_id}/unarchive/. - Archiving a cycle that has not ended returns
409 conflict. v1 returns400. - Updates are
PATCHonly — v2 has noPUT.
See Migrating from v1 for the full list.

