Filtering and ordering
v2 list endpoints take query parameters from four families: filters that narrow the set, search that matches free text, ordering that sorts what is left, and pagination that splits the result into pages. Every list endpoint supports ordering and pagination. Each one declares its own filters and search, and a few smaller collections (work item types, properties, property options, property contexts) offer only ordering and pagination. Where the families are available you can combine them, and every filter you add narrows the result further. Filters are combined with AND, never OR.
Filters run after authorization, so a filter can only narrow the set of rows you are already allowed to see. No filter widens access.
Filter shapes
Public parameter names never contain __. The suffixes below are variants of a base name, and each list endpoint declares exactly which variants it supports.
| Shape | Parameter form | Example | Matches |
|---|---|---|---|
| Exact | <name> | ?priority=urgent | Rows equal to the value |
| Membership | <name>__in | ?priority__in=urgent,high | Rows equal to any comma-separated value |
| Null | <name>__isnull | ?parent_id__isnull=true | Rows where the field is (or is not) set |
| Range | <name>__gte / <name>__lte | ?created_at__gte=2026-01-01T00:00:00Z | Rows on or after / on or before a bound |
A few things that are easy to get wrong:
__intakes a comma-separated list in a single parameter, as in?priority__in=urgent,high. Repeating the parameter does not do the same thing.__gteand__lteare inclusive on both ends. Pair them to express a window; send one alone for an open-ended bound.- Date-time ranges take an ISO 8601 timestamp (
2026-01-01T00:00:00Z), and date ranges take a plain date (2026-01-01). The field a parameter filters determines which one it takes. - Not every base name offers every variant.
state_idsupports__inbut not__isnull, because a work item always has a state. Check the endpoint's parameter list instead of assuming every variant exists.
Filtering by a relation
Filter a relation with its *_id parameter. There is no bare-name form: use state_id, not state.
| Resource | Relation filters |
|---|---|
| Work items | state_id, assignee_id, label_id, parent_id, cycle_id, module_id, type_id, initiative_id, release_id, customer_id, project_id (workspace list) |
| Cycles | owned_by_id |
| Modules | lead_id |
| Labels | parent_id |
| Members | member_id |
This matches how writes work: you set a relation with state_id, and you filter on it with state_id. See Migrating from v1 if you are used to v1's embedded relation objects.
# every cycle owned by one person
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/cycles/?owned_by_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430" \
-H "X-Api-Key: $PLANE_API_KEY"
# modules a person leads that are underway
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/modules/?lead_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430&status=in-progress" \
-H "X-Api-Key: $PLANE_API_KEY"Looking something up by name
In v2 you cannot use a mutable attribute, such as a name, a URL, or a version string, as a path segment. Filter the list instead:
# the state called "In Progress"
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/ENG/states/?name=In%20Progress" \
-H "X-Api-Key: $PLANE_API_KEY"Combined with ?fields=, this resolves a name to an id in one call of about fifteen tokens:
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/ENG/states/?name=In%20Progress&fields=id" \
-H "X-Api-Key: $PLANE_API_KEY"{
"data": [{ "id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f" }],
"next": null,
"previous": null,
"total_count": 1,
"pagination": { "style": "offset" }
}Matching is case-insensitive and exact, not a substring match. For substring matching, use ?search=. Project and workspace automations and automation nodes are the exception: their ?name= is a case-insensitive substring match.
| Filter | Available on |
|---|---|
?name= | states, workspace states, project states, labels, project labels, cycles, modules, milestones (matches title), projects, project and workspace views, initiatives, initiative labels, releases, release labels, customers, customer properties, estimates, teamspaces, webhooks, work item types (project and workspace), work item properties (project and workspace), property options, property contexts, page templates (project and workspace), project templates, template categories, workspace workflows, recurring work items (matches the work item name) |
?key= | projects (matches identifier), estimate points |
?url= | webhooks, work item links |
?title= | work item links |
?value= | estimate points |
?version= | release tags |
?external_id=, ?external_source= | 24 list endpoints, for sync and import correlation |
Duplicates are visible, not guessed
If two states share a name, the filtered list returns both rows and you decide which one you meant. There is no path-level "ambiguous identifier" error to handle, because there is no path-level attribute lookup.
Scheme-prefixed path segments are gone
A v2 preview briefly accepted <scheme>:<value> path segments, such as …/states/name:In Progress/, …/projects/key:ENG/, …/webhooks/url:https://…/, and external:…. Those were removed. A name:-style segment is now just an unknown id and returns 404.
Use the filters above instead. Path segments take stable identifiers only. See Addressing.
Addressing a resource in a path
Path segments take UUIDs, with exactly three human-readable exceptions. All three are stable, and none is scheme-prefixed:
| Key | Where it works |
|---|---|
| Workspace slug | the workspaces/{slug}/ prefix on every route |
| Project identifier | the project detail route (…/projects/ENG/) and as a parent segment (…/projects/ENG/work-items/) |
Work item PROJ-123 | the by-identifier route, project-scoped detail, and as a work_item_id parent segment |
# both of these address the same project
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/ENG/" -H "X-Api-Key: $PLANE_API_KEY"
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/" -H "X-Api-Key: $PLANE_API_KEY"A project identifier is unique per workspace and uppercased on save, so it can never be ambiguous. Everything else in a path is a UUID.
Enum-backed filters are validated
Parameters backed by an enum (work item priority and state_group, state group, module status, comment access) are checked against their allowed values before the query runs. A value outside the enum returns 400 invalid_request, naming the field.
In an API that ignores unknown filter values, ?state_group=in_progress (a plausible guess; the real value is started) returns 200 with an empty list, and you cannot tell whether the project is actually empty. In v2 the request fails immediately:
{
"type": "invalid_request",
"code": "invalid_request",
"detail": "One or more fields failed validation.",
"errors": [
{
"field": "state_group",
"code": "invalid_choice",
"message": "Select a valid choice. in_progress is not one of the available choices."
}
]
}The __in variants validate every element of the list, not just the first.
Search
?search=<text> is a case-insensitive partial match over the resource's searchable text. It is meant for simple lookups and is not a query language: it has no operators, field prefixes, or quoting.
| Resource | Searches |
|---|---|
| Work items | Name |
| States, labels, cycles, modules | Name |
| Work item comments | Comment text |
| Members | Display name, email, first name, last name |
| Audit logs | Event name, actor display name, actor email, target display name |
Search composes with filters, so ?search=login&priority=urgent finds urgent work items whose name contains "login".
Ordering
?order_by=<field> sorts the list. Prefix the field with - for descending order:
?order_by=created_at # oldest first
?order_by=-created_at # newest firstEach resource declares its own allowed fields. Unlike the enum-backed filters above, an unrecognized order_by does not fail. The list falls back to the resource's default ordering. If a sort looks wrong, check the spelling against the endpoint's allowed values before assuming the data is wrong. Every ordering uses a unique tiebreaker internally, so a page boundary never drops or duplicates a row when two rows share a sort key.
Semantic orderings
Two work item orderings sort in a semantic order instead of alphabetically:
order_by=prioritysortsurgent→high→medium→low→none. An alphabetical sort would givehigh, low, medium, none, urgent.order_by=state_groupsortsbacklog→unstarted→started→completed→cancelled→triage, following the workflow.
-priority and -state_group reverse those sequences.
Semantic orderings are offset-only
priority and state_group cannot be combined with ?paginate=cursor. A cursor needs a unique, monotonic sort key, and these fields have only a few distinct values. The pairing returns 400 ordering_not_cursor_eligible. sort_order is not cursor-eligible either, for the same reason. On work items the cursor-eligible orderings are created_at, updated_at, sequence_id, and id. See Pagination.
Worked example: the work items list
The work items list has more filters than any other v2 list. Its parameters, grouped by concept:
Relations
| Base name | Variants | Type |
|---|---|---|
state_id | __in | string (uuid) |
type_id | __in | string (uuid) |
assignee_id | __in, __isnull | string (uuid) |
label_id | __in, __isnull | string (uuid) |
parent_id | __in, __isnull | string (uuid) |
cycle_id | __in, __isnull | string (uuid) |
module_id | __in, __isnull | string (uuid) |
initiative_id | __in | string (uuid) |
release_id | __in | string (uuid) |
customer_id | __in | string (uuid) |
Enums and scalars
| Name | Variants | Type | Values |
|---|---|---|---|
priority | __in | string | urgent, high, medium, low, none |
state_group | __in | string | backlog, unstarted, started, completed, cancelled, triage |
is_draft | None | boolean | true, false. true matches nothing unless archived is also set |
sequence_id | None | integer | The number in a PROJ-123 identifier |
external_id | None | string | Your system's id, for sync correlation |
external_source | None | string | The system external_id came from |
archived | None | string | true (or 1) drops the default list's exclusions. only does the same, then keeps just the archived items. Other values are ignored |
Ranges
| Base name | Variants | Type |
|---|---|---|
created_at | __gte, __lte | string (date-time) |
updated_at | __gte, __lte | string (date-time) |
start_date | __gte, __lte | string (date) |
target_date | __gte, __lte | string (date) |
initiative_id, release_id, and customer_id match work items linked to that initiative, release, or customer. customer_id matches both direct links and links made through a customer request. To combine one of these with other filters, for example ?initiative_id=…&state_group=started, use the filter instead of the work item list under that initiative, release, or customer.
Without archived, the list leaves out archived items, drafts, triage (intake) items, and items in archived projects. true, 1, and only lift all of those exclusions, not only the archive one, so drafts, triage items, and items in archived projects come back too.
Date fields support only range filters. There is no exact ?created_at= filter. To match a single day, bracket it: ?created_at__gte=2026-01-14T00:00:00Z&created_at__lte=2026-01-14T23:59:59Z.
Search: search.
Ordering: order_by, one of created_at, updated_at, sequence_id, id, sort_order, priority, state_group, each also available with a - prefix.
Pagination: per_page, offset, count, paginate, cursor. See Pagination.
Combined filters
# urgent and high-priority items that are in flight or already done
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?priority__in=urgent,high&state_group__in=started,completed" \
-H "X-Api-Key: $PLANE_API_KEY"# one person's open work, most urgent first
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?assignee_id=16c61a3a-512a-48ac-b0be-b6b46fe6f430&state_group__in=backlog,unstarted,started&order_by=priority" \
-H "X-Api-Key: $PLANE_API_KEY"# an incremental sync: everything touched since the last run, oldest first
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?updated_at__gte=2026-01-14T09:22:41Z&order_by=updated_at&paginate=cursor&per_page=200" \
-H "X-Api-Key: $PLANE_API_KEY"# top-level items only — nothing that is a sub-item of something else
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?parent_id__isnull=true&is_draft=false" \
-H "X-Api-Key: $PLANE_API_KEY"# unassigned work due this quarter, with the state object attached
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?assignee_id__isnull=true&target_date__gte=2026-01-01&target_date__lte=2026-03-31&expand=state" \
-H "X-Api-Key: $PLANE_API_KEY"# find the item you imported from another system
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/?external_source=jira&external_id=ENG-4417" \
-H "X-Api-Key: $PLANE_API_KEY"Related
- Pagination: the envelopes, and which orderings a cursor can use
- Expanding relations: attaching related objects to filtered results
- Errors: the
invalid_requestbody and itserrors[]array

