List cycles
Return the cycles in a project as a paginated page. Use it to build a sprint picker, to find the cycle covering a date range, or to reconcile cycles you imported from another tracker.
Filters combine with AND — ?owned_by_id=…&search=sprint returns only cycles that match both.
Path Parameters
slug:requiredstringThe workspace slug. It appears in your Plane URLs — in https://app.plane.so/my-team/projects/, the slug is my-team.
project_id:requiredstring (uuid)The project whose cycles you want.
Query Parameters
Filters
owned_by_id:optionalstring (uuid)Return only cycles owned by this user. This is how you build a "my cycles" view — pass the authenticated user's id. Ownership is assigned by Plane, so this is a read-side filter only.
external_id:optionalstringReturn only cycles whose external_id matches exactly. Pair it with external_source when the same identifier could come from more than one system.
external_source:optionalstringReturn only cycles imported from this source, for example jira.
Search
search:optionalstringFree-text search term matched against the cycle.
Ordering
order_by:optionalstringField to sort by. Prefix with - for descending.
sort_order/-sort_order— the project's manual cycle orderingcreated_at/-created_at— newest or oldest firstid/-id
Pagination
per_page:optionalintegerPage size. Defaults to 50, maximum 200.
offset:optionalintegerNumber of rows to skip from the start of the result set. Maximum 10000 — past that, switch to cursor pagination.
paginate:optionalstringSet to cursor to opt into the COUNT-free keyset envelope instead of the default offset envelope. The response then carries next_cursor and has_more rather than next, previous, and total_count; pass next_cursor back as cursor to walk to the following page. Use it for deep traversal, where offset paging gets expensive.
count:optionalbooleanDefaults to true. Set to false to skip the COUNT(*) behind total_count — the field is then omitted from the envelope. Worth doing when you only need the rows.
Bad enum values fail silently
order_by and paginate are not validated against their allowed values. An unrecognized order_by falls back to the cycle default ordering, and anything other than paginate=cursor uses offset pagination — so check your spelling, because a typo shows up as an unexpected sort order or envelope rather than an error.
Scopes
projects.cycles:read
Errors
| Status | Code | Cause |
|---|---|---|
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Your role or token scope can't read cycles in this project. |
404 | resource_not_found | No such workspace or project, or it's outside your tenant. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
curl -X GET \
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/cycles/?per_page=50&order_by=-created_at" \
-H "X-Api-Key: $PLANE_API_KEY"import requests
response = requests.get(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/cycles/",
headers={"X-Api-Key": "your-api-key"},
params={"per_page": 50, "order_by": "-created_at"},
)
print(response.json())const params = new URLSearchParams({ per_page: "50", order_by: "-created_at" });
const response = await fetch(
`https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/cycles/?${params}`,
{
headers: { "X-Api-Key": "your-api-key" },
}
);
const data = await response.json();{
"data": [
{
"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",
"timezone": "America/New_York",
"owned_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"sort_order": 65535,
"logo_props": {},
"external_id": null,
"external_source": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
},
{
"id": "2b8d5e07-9a41-4c6f-b3d2-71e8a4c05f96",
"name": "Sprint 23",
"description": "",
"start_date": "2025-12-22T00:00:00Z",
"end_date": "2026-01-05T00:00:00Z",
"timezone": "America/New_York",
"owned_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"sort_order": 55535,
"logo_props": {},
"external_id": "SPR-23",
"external_source": "jira",
"created_at": "2025-12-18T11:04:07.912004Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
}
],
"next": 50,
"previous": null,
"total_count": 27,
"pagination": {
"style": "offset"
}
}{
"data": [
{
"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",
"timezone": "America/New_York",
"owned_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"sort_order": 65535,
"logo_props": {},
"external_id": null,
"external_source": null,
"created_at": "2026-01-14T09:22:41.478363Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
}
],
"next_cursor": "b3A9MTcxOjA6MA==",
"has_more": true,
"pagination": {
"style": "cursor"
}
}
