Sparse fields
Every v2 endpoint whose response is a resource body accepts ?fields=, a comma-separated list of the keys you want.
curl "https://api.plane.so/api/v2/workspaces/my-team/projects/ENG/work-items/?fields=id,name,state_id" \
-H "X-Api-Key: $PLANE_API_KEY"{
"data": [
{
"id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
"name": "Fix login redirect loop",
"state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f"
},
{
"id": "3b7d9e40-1c62-4a85-b0f3-9d5c2e6a8471",
"name": "Rate limit the invite endpoint",
"state_id": "f960d3c2-8524-4a41-b8eb-055ce4be2a7f"
}
],
"next": 50,
"previous": null,
"total_count": 327,
"pagination": { "style": "offset" }
}A default work item row has about twenty keys. If you only need three, ?fields= lets you pay for three instead of twenty, on up to 200 rows a page, on every call.
Omit, not null
Unrequested keys are absent from the JSON. They are not returned as null.
Omitting keys lets you tell "I didn't ask for this" apart from "this value is null". Filling unrequested keys with nulls would not.
| Response | Means |
|---|---|
| key absent | you didn't request it |
"target_date": null | you requested it and it is unset |
Filling with nulls would also save almost nothing. On a sparse row the key names are most of the payload, so a twenty-field row of nulls still sends twenty key names. Omitting the keys is what reduces the response size.
Unrequested fields are never computed
Omitted keys are dropped before serialization, so if a field costs a query or a computation to produce, that work never runs. ?fields= reduces latency as well as payload size.
id always comes back
You never have to ask for id, and you cannot remove it:
?fields=name → { "id": "…", "name": "…" }It is the only key that is always present, so a client can always match a row to its resource. (A few parent-addressed singletons, such as project features, have no id of their own. Those return exactly what you asked for.)
all
all is a reserved token meaning "every requestable field for this response shape".
?fields=all → the full body
?fields=all,name → the full body; `all` wins and `name` is redundantUse it when you want everything, including fields that a list would normally leave out. See deferred fields below.
What a list leaves out by default
Two Meta-level rules shape the default list row, so that a call with no parameters is not the expensive one.
Deferred fields
Fields of unbounded size, like description_html, are omitted from collection rows by default. Name one to include it:
GET …/pages/ → rows without description_html
GET …/pages/?fields=id,name,description_html → rows with it
GET …/pages/?fields=all → rows with itDeferral is only a default, and naming the field overrides it. It affects collections only. A single-object response is never deferred, so GET …/pages/{id}/ always includes description_html.
Resources that defer description_html today: customers, customer requests, initiatives, intake work items, modules, pages (project and workspace), projects, releases (workspace and project), stickies, teamspaces, work items, and all three template kinds (work item, page, and project templates). A few defer more:
- Templates also defer
template_data, since it embeds HTML too. - Pages also defer
description_strippedandproject_ids. - Stickies also defer
description_stripped. - Cycles and modules defer their progress counts (
total_issues,backlog_issues,unstarted_issues,started_issues,completed_issues,cancelled_issues). Each count costs a grouped query, so a list computes them only when?fields=names one or isall.
Detail-only fields
A detail-only field is never on a collection row, and you cannot request it there:
GET …/work-items/?fields=id,custom_fields{
"type": "invalid_request",
"code": "invalid_request",
"detail": "One or more fields failed validation.",
"errors": [
{
"field": "fields",
"code": "invalid",
"message": "custom_fields is not available on collection responses; retrieve the work item."
}
]
}Two resources have detail-only fields today:
- Work item
custom_fields. Filling it takes a property lookup for each item, which the list path can't afford. Lists leave the key out instead of returningnullon every row, and single-item responses include it. See List work items. - Workspace workflow
states,referenced_resources,project_ids, andwork_item_type_ids. Building the chain takes several queries per workflow, so the list leaves them out. Single-workflow responses include them.
Where it applies
?fields= works on every response rendered by a resource read serializer:
- list and retrieve
POSTandPATCHechoesupsert- custom list actions
It does not apply to:
APIViewendpoints: current user, workspace features, effective permissions, artifacts- the
bulk-create/bulk-update/bulk-deleteroutes, whose response is a per-row result envelope rather than a resource body - bespoke bodies such as upload credentials and grouped relation reads
- resources served by a compact
Liteserializer, such as work item property options and type-property attachments
The reference page for each endpoint lists its requestable fields when the parameter is available. The OpenAPI document enumerates them too, so a generated SDK or MCP tool schema constrains the value up front rather than discovering it from a 400.
Unknown field names are rejected
An unknown field name returns 400; the parameter is never ignored. The message names the closest match and lists the valid values, so a client can correct the request in one retry:
{
"type": "invalid_request",
"code": "invalid_request",
"detail": "One or more fields failed validation.",
"errors": [
{
"field": "fields",
"code": "invalid",
"message": "Unknown field(s): titel — did you mean 'title'? Valid fields: all, id, name, identifier, …"
}
]
}This is deliberate. If a typo were ignored, you would get the full row back with no sign that anything was wrong, and lose the size reduction.
An empty value (?fields=) is not an error. It applies no filtering.
Composing with ?expand=
?fields= and ?expand= are separate namespaces and do not interact.
?fields=id,name&expand=state → { "id": "…", "name": "…", "state": { … } }state_id is a field; state is an expandable relation. Filtering fields never drops an expansion, and expanding never replaces an id with an object.
Naming a relation in ?fields= returns 400 instead of expanding it, and the error tells you which parameter you wanted:
'state' is an expandable relation, not a response field — use ?expand=state to embed the object, or ?fields=state_id for just its id.
The reverse also holds: ?expand=state_id points you to ?fields=state_id. See Expanding relations.
The one-call identity lookup
Combined with the identity filters, ?fields= makes it cheap to resolve a name to an id when you have a human-readable name and need the UUID:
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" }
}Because the result is a list, duplicates show up as separate rows instead of being hidden behind a guess. See Filtering and ordering.
Notes for SDK authors
Read components in the OpenAPI document declare id as their only required property. This is accurate: because unrequested keys are omitted, any other key may be absent, and a generated type that marks other fields as required would be wrong.
If you are generating a typed client, you can narrow the return type from the requested field list, so that list({ fields: ["id", "name"] }) returns Pick<WorkItem, "id" | "name">[]. That requires a generator that can express const-generic narrowing.

