Skip to content

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.

Learn more about cycles

The cycle object ​

Attributes ​

  • id string (uuid)

    Unique identifier for the cycle.

  • name string

    Display name, unique within the project. Maximum 255 characters.

  • description string

    Free-form description of what the cycle covers.

  • start_date string (date-time)

    When the cycle opens. null if the cycle has not been scheduled.

  • end_date string (date-time)

    When the cycle closes. null if the cycle has not been scheduled.

  • timezone string

    The IANA time zone the cycle's dates are interpreted in, for example America/New_York or UTC. This is what makes a cycle boundary land at local midnight rather than UTC midnight for a distributed team.

  • owned_by_id string (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_order number

    Ordering weight for the cycle within the project. Lower values sort first when you order by sort_order.

  • logo_props any

    JSON blob holding the cycle's icon configuration, as written by Plane clients. Passed through unchanged.

  • external_id , external_source string

    Correlation 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_issues integer

    The 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_issues integer

    The number of live work items in the cycle whose state belongs to that group. Omitted from list rows unless ?fields= names them.

  • progress_snapshot object

    The 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) and estimate_distribution. {} until a transfer runs. Read-only.

  • archived_at string (date-time) or null

    When the cycle was archived, or null for an active cycle. Set by Archive a cycle and cleared by Unarchive a cycle. Read-only.

  • created_at string (date-time)

    When the cycle was created.

  • updated_at string (date-time)

    When the cycle was last updated.

  • created_by_id string (uuid)

    The user who created the cycle.

  • updated_by_id string (uuid) or null

    The user who last updated the cycle. null until 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.

Response200
json
{
  "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 ​

MethodPathDescription
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:

bash
GET .../cycles/?fields=id,name,total_issues,completed_issues

Archiving ​

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_by is now owned_by_id, created_by is now created_by_id, and updated_by is now updated_by_id.
  • start_date and end_date are date-times, not plain calendar dates.
  • Reads no longer return project, workspace, or view_props.
  • The progress counts are omitted from list rows unless ?fields= names them. v1 returns them on every row.
  • The list endpoint drops cycle_view and adds search, name, owned_by_id, external_id, external_source, is_archived, and include_archived filters.
  • Archived cycles are listed with ?is_archived=true on the regular list instead of a separate archived-cycles/ route. Unarchive is POST …/cycles/{pk}/unarchive/ instead of DELETE …/archived-cycles/{cycle_id}/unarchive/.
  • Archiving a cycle that has not ended returns 409 conflict. v1 returns 400.
  • Updates are PATCH only — v2 has no PUT.

See Migrating from v1 for the full list.