Work Item REST API endpoints
This API is in active development and sits behind a feature flag, so it has no public doc/api/ reference page yet. What follows is a draft of that future documentation.
All paths below are relative to /api/v4. Every logical endpoint exists in three variants:
/namespaces/:id/-/work_items.../projects/:id/-/work_items.../groups/:id/-/work_items...
In all three, :id accepts either a numeric ID or an unescaped full path.
Availability is stated per license tier rather than per edition, because gitlab.com runs EE while its free namespaces behave like CE. Free means every tier on both CE and EE builds. Premium and Ultimate name the tier whose license unlocks the item. A few items ship only in EE builds but need no license; those say EE builds.
Endpoint summary
| Verb | Path | Purpose | Availability |
|---|---|---|---|
GET |
— | List work items | Free |
GET |
/:work_item_iid |
Get one work item | Free |
POST |
— | Create work item | Free |
PATCH |
/:work_item_iid |
Update work item | Free |
DELETE |
/:work_item_iid |
Delete work item | Free |
GET |
/:work_item_iid/children |
List children | Free |
POST |
/:work_item_iid/children/:child_id |
Attach child | Free |
DELETE |
/:work_item_iid/children/:child_id |
Detach child | Free |
PUT |
/:work_item_iid/children/:child_id |
Reorder child | Free |
GET |
/:work_item_iid/notes |
List notes | Free |
GET |
/:work_item_iid/discussions |
List discussions | Free |
GET |
/:work_item_iid/discussions/:discussion_id |
Get one discussion | Free |
GET |
/:work_item_iid/closing_merge_requests |
MRs that close the item | Free |
GET |
/:work_item_iid/related_merge_requests |
MRs referencing the item | Free |
GET |
/:work_item_iid/related_branches |
Branches referencing the item | Free |
GET |
/:work_item_iid/feature_flags |
Feature flags on the item | EE builds |
GET |
/:work_item_iid/linked_items |
List linked work items | Free |
GET |
/:work_item_iid/linked_resources |
List linked resources (Zoom) | Free |
GET |
/:work_item_iid/award_emoji |
List emoji reactions | Free |
GET |
/:work_item_iid/current_user_todos |
List current user’s to-dos | Free |
GET |
/:work_item_iid/email_participants |
List Service Desk email participants | Free |
Selecting fields and features
Every response always includes exactly four fields: id, iid, global_id, title. Everything else is opt-in and must be requested explicitly through fields=.
All 25 `fields` values
| Field | Type | Example |
|---|---|---|
id |
integer | 1 |
global_id |
string | gid://gitlab/WorkItem/1 |
iid |
integer | 1 |
title |
string | Fix the bug |
state |
string | opened |
confidential |
boolean | false |
imported |
boolean | false |
hidden |
boolean | false |
lock_version |
integer | 0 |
created_at |
datetime | 2022-08-17T12:46:35.053Z |
updated_at |
datetime | 2022-11-14T17:22:01.470Z |
closed_at |
datetime | 2022-11-15T08:30:55.232Z |
title_html |
string | <p>Fix the bug</p> |
author |
object | an Author entity |
work_item_type |
object | a Type entity |
namespace |
object | a NamespaceBasic entity |
create_note_email |
string | issue-1@example.com |
duplicated_to_work_item_url |
string | https://gitlab.example.com/groups/gitlab-org/-/work_items/2 |
moved_to_work_item_url |
string | https://gitlab.example.com/groups/gitlab-org/-/work_items/3 |
reference |
string | gitlab-org#1 |
web_url |
string | https://gitlab.example.com/groups/gitlab-org/-/work_items/1 |
web_path |
string | /groups/gitlab-org/-/work_items/1 |
user_permissions |
object | a Permissions entity |
user_discussions_count |
integer | 3 |
features |
object | keyed by feature name, see the next table |
features defaults to empty, so nothing feature-related is returned unless requested. Every feature is an object except assignees, which is an array. Twelve are available in every tier; the other ten depend on the license.
All 22 `features` values with example payloads
| Feature | Availability | Example |
|---|---|---|
description |
Free | {"description": "Fix the login bug", "description_html": "<p>Fix the login bug</p>", "edited": false, "last_edited_at": "2022-11-15T08:30:55.232Z", "last_edited_by": {…}, "task_completion_status": {…}} |
assignees |
Free | [{"id": 1, "username": "alice", …}] |
labels |
Free | {"allows_scoped_labels": true, "labels": [{…}]} |
milestone |
Free | {"id": 1, "iid": 1, "title": "19.2", …} |
start_and_due_date |
Free | {"start_date": "2022-08-17", "due_date": "2022-08-30", "roll_up": false} |
designs |
Free | {"design_collection": {"copy_state": "ready"}} |
time_tracking |
Free | {"time_estimate": 12600, "total_time_spent": 4500, "human_readable_attributes": {"time_estimate": "3h 30m", "total_time_spent": "1h 15m"}, "timelogs": […]} |
error_tracking |
Free | {"identifier": 12345} |
hierarchy |
Free | {"parent": {…}, "has_parent": true} |
award_emoji |
Free | {"upvotes": 5, "downvotes": 1, "new_custom_emoji_path": "/groups/gitlab-org/-/custom_emoji/new"} |
development |
Free | {"closing_merge_requests_count": 2, "will_auto_close_by_merge_request": false} |
notifications |
Free | {"subscribed": true} |
color |
Premium | {"color": "#A8DADC", "text_color": "#1D3557"} |
progress |
Ultimate | {"progress": 65, "current_value": 13, "start_value": 0, "end_value": 20, "updated_at": "2024-02-12T09:45:00Z"} |
iteration |
Premium | {"iteration": {…}} |
health_status |
Ultimate | {"health_status": "needs_attention"} |
weight |
Premium | {"weight": 3, "rolled_up_weight": 8, "rolled_up_completed_weight": 5} |
requirement_legacy |
Ultimate | {"legacy_iid": 1} |
status |
Premium | {"status": {"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1", "name": "To do", "category": "to_do", "color": "#737278", …}} |
verification_status |
Ultimate | {"verification_status": "satisfied"} |
linked_items |
EE builds | {"blocking_count": 2, "blocked_by_count": 1} |
custom_fields |
Premium | {"custom_field_values": [{"custom_field": {"id": 1, "name": "Priority", "field_type": "single_select", …}, "value": "In progress", "selected_options": […]}]} |
Both fields and features are comma-separated and downcased. A value that is not a known field or feature name returns 400 listing the unknown values, so a typo is caught rather than ignored. A feature that is valid but that the work item’s type does not support, or that is not licensed for the namespace, is omitted from that item’s response instead of erroring. This mirrors GraphQL: a list spans several work item types, and each item returns only the features it has. hierarchy is explicitly permitted on the list endpoint, it is preloaded there specifically.
Pagination
Every collection endpoint uses keyset pagination, exposed to clients as an opaque cursor. Requests take cursor and per_page. Responses carry X-Next-Cursor and X-Prev-Cursor, plus a Link header with rel="next" and rel="prev".
Some orderings have no keyset form, for example the list endpoint’s popularity and milestone_due sorts. For those the server pages by offset behind the same opaque cursor, so the contract does not change with the sort or the endpoint.
Unreadable records are filtered out after the page is fetched, so a page can hold fewer than per_page items, or none at all, while X-Next-Cursor is still present. Clients must follow the cursor rather than treat a short or empty page as the end of the collection.
Discussions are paged by Issuable::DiscussionsListService, which REST shares with the GraphQL resolver and the Rails controller.
Authorization
- Authentication. Every endpoint calls
authenticate!except the list endpoint, which is reachable anonymously against public projects and groups. There is no unauthenticated write path. - Permission. Each route declares the ability the caller must hold, evaluated against the work item’s container.
- CI job token policies. A narrower gate that only runs when the caller is a CI job token. It has no bearing on personal access tokens, OAuth, or session requests.
| Group | Authentication | Permission | CI job token |
|---|---|---|---|
| List | not required | :read_work_item |
read_work_items, on the namespaces and projects variants |
| Show, all GET sub-resources | required | :read_work_item |
read_work_items, on the namespaces and projects variants (see exception below) |
| Create | required | :create_work_item |
not accepted |
| Update, children attach/detach/reorder | required | :update_work_item |
not accepted |
| Delete | required | :delete_work_item |
not accepted |
“Not accepted” in the last column is an active rejection, not a missing check. A route that declares no job_token_policies fails the job token gate with 403 and the message “This action is unauthorized for CI/CD job tokens.” Those routes are still authenticated and still permission-checked in the usual way; the column only says that a CI job token is not a usable credential for them. The gate is bypassed in a handful of configurations, such as a token used against its own project or an allowlist entry still on default permissions, so it describes the route’s declaration rather than a guarantee for every deployment.
Container boundaries: the namespaces variant resolves to either group or project, depending on what the id points at.
Exception: the groups variant of email_participants does declare job token policies, unlike the rest of the GET sub-resources.
A denied request returns 404, not 403, both when the container itself is invisible and when the specific item is unreadable. A feature flag being off is a separate case and returns 403.
Feature flags
work_item_rest_api(typewip, default off, per-user, introduced in 18.9) gates show, create, update, delete, and the GET sub-resources. They return403while it is off. It does not gate the list endpoint. Rollout issue.work_item_rest_api_index(typewip, introduced in 19.1, since removed) previously gated the list endpoint on its own. Rollout issue.work_item_rest_api_frontend_users(typewip, default off, introduced in 19.0) does not gate any endpoint. It only controls whether the Vue list and board views fetch work items through REST instead of GraphQL. Rollout issue.
One further flag changes what an endpoint returns without being ours to roll out: explicit_mr_work_item_relations, owned by group::work items, widens the related merge requests response. It is described with that endpoint rather than listed here.
Core endpoints
Each endpoint below shows its verb, path, and purpose. Parameters and response details are collapsed, expand an endpoint to see them.
List
GET /-/work_items
Lists work items visible to the current user (or anonymously, see Authorization) in the given container.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page |
integer | no | items per page, default 20 |
cursor |
string | no | keyset pagination cursor |
order_by |
string | no | default created_at |
sort |
string | no | default desc |
fields |
string | no | comma-separated field list, see Selecting fields and features |
features |
string | no | comma-separated feature list, default empty |
Filters (all optional, array-type params accept comma-separated values):
| Group | Filters |
|---|---|
| Basic | ids, iids, state (opened/closed/all), work_item_type_ids XOR work_item_type_names, author_username, assignee_usernames XOR assignee_wildcard_id (None/Any/Me), label_name, milestone_title XOR milestone_wildcard_id (None/Any/Upcoming/Started), my_reaction_emoji, created_before/created_after, updated_before/updated_after, closed_before/closed_after, due_before/due_after, search, in (title,description), timeframe[start]/timeframe[end], confidential, subscribed, parent_ids XOR parent_wildcard_id, include_descendant_work_items, release_tag XOR release_tag_wildcard_id, crm_contact_id, crm_organization_id |
not[...] |
assignee_usernames, author_username, label_name, milestone_title XOR milestone_wildcard_id, my_reaction_emoji, parent_ids, release_tag, work_item_type_ids XOR work_item_type_names |
not[...] (licensed) |
iteration_id XOR iteration_wildcard_id (Premium), health_status_filter (Ultimate), weight (Premium). Negated health_status_filter takes an array of health status values, unlike the top-level filter, which is a single string that also accepts none and any. |
or[...] |
assignee_usernames, author_usernames, label_names |
| Licensed | iteration_id XOR iteration_wildcard_id, iteration_cadence_id (Premium), health_status_filter (Ultimate), weight XOR weight_wildcard_id (Premium), custom_field[] (Premium), status[id] XOR status[name] (Premium), verification_status_widget[verification_status] (Ultimate) |
Group/namespace-only (not accepted by the projects variant) |
include_ancestors, include_descendants, include_archived (default false) |
Response: an array of WorkItemBasic.
Get
GET /-/work_items/:work_item_iid
Returns a single work item.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
fields |
string | no | comma-separated field list |
features |
string | no | comma-separated feature list |
Response: WorkItemDetail, whose root fields are identical to WorkItemBasic. With Ultimate, the health_status feature carries extra detail (count, rolled_up_health_status). This is the only endpoint reachable with an ai_workflows-scoped token.
Create
POST /-/work_items returns 201.
Creates a work item.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
title |
string | yes | — |
work_item_type_name / work_item_type_id |
string / integer | at least one required | selects the work item type |
confidential |
boolean | no | — |
created_at |
datetime | no | admins/owners only |
fields |
string | no | fields to include in the response |
features |
object | no | feature payloads keyed by feature name, see Feature payloads |
Response: WorkItemBasic.
Update
PATCH /-/work_items/:work_item_iid returns 200.
Updates a work item. This is PATCH only, there is no PUT on the work item resource itself (the only PUT in the API is child reorder).
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
title |
string | no | — |
confidential |
boolean | no | — |
state_event |
string | no | close or reopen |
fields |
string | no | fields to include in the response |
features |
object | no | feature payloads keyed by feature name, see Feature payloads |
The features keys submitted in the request also select which feature payloads come back in the response. A widget the work item’s type does not support returns 400 with an unsupported_widgets list; a widget the type supports but that is disabled for the container is silently dropped instead of erroring.
Response: WorkItemBasic.
Delete
DELETE /-/work_items/:work_item_iid returns 204, no body.
Feature payloads
features is an object keyed by feature name, and each feature has its own payload. Create and update accept different sets, and a few features take different keys on each, so both are shown side by side below. “Not accepted” means the feature is rejected on that verb.
| Feature | Availability | Create payload | Update payload |
|---|---|---|---|
description |
Free | description required |
description required |
assignees |
Free | assignee_ids required, max 30 |
assignee_ids required, max 30 |
labels |
Free | label_ids required, max 30 |
add_label_ids, remove_label_ids max 30 each |
milestone |
Free | milestone_id, send null to clear it |
same as create |
hierarchy |
Free | parent_id |
parent_id, null removes the parent |
start_and_due_date |
Free | start_date, due_date |
same as create |
linked_items |
Free | work_items_ids required, max 30, link_type (only relates_to, the default) |
not accepted |
crm_contacts |
Free | contact_ids required |
contact_ids required, operation_mode (REPLACE default, APPEND, REMOVE) |
notes |
Free | not accepted | discussion_locked required |
notifications |
Free | not accepted | subscribed required |
current_user_todos |
Free | not accepted | action required (mark_as_done or add), todo_id (omit to update every to-do on the item) |
award_emoji |
Free | not accepted | action required (add, remove, or toggle), name required |
time_tracking |
Free | not accepted | time_estimate in human-readable form such as 1h 30m, and timelog with time_spent required, spent_at, summary |
color |
Premium | color required, hex code such as #e24329 |
same as create |
health_status |
Ultimate | health_status (on_track, needs_attention, at_risk) |
same as create |
iteration |
Premium | iteration_id, send null to clear it |
same as create |
weight |
Premium | weight, send null to clear it |
same as create |
status |
Premium | status_id, resolved against the namespace as either a custom or a system-defined status |
same as create |
custom_fields |
Premium | array of entries, each carrying custom_field_id and whichever of text_value, number_value, date_value, or selected_option_ids matches the field’s type |
same as create, max 30 entries |
progress |
Ultimate | not accepted | current_value required, start_value, end_value |
verification_status |
Ultimate | not accepted | verification_status required |
labels is worth calling out: it takes label_ids on create but add_label_ids/remove_label_ids on update, so the same feature name means “set these” on one verb and “change these” on the other.
Sub-resource endpoints
The remaining endpoints all hang off a single work item. Every one of them takes work_item_iid in the path, requires :read_work_item, and is gated by the work_item_rest_api flag. All are GET unless stated otherwise. They are grouped below by the concept they expose.
Hierarchy endpoints
These cover a work item’s children. The three write operations all return an identical generic 404 when the child cannot be found, whatever the reason, so existence cannot be probed.
List children
GET /-/work_items/:work_item_iid/children
Lists a work item’s children.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the parent work item, path parameter |
state |
string | no | opened or closed, omitted returns both |
fields |
string | no | comma-separated field list |
features |
string | no | comma-separated feature list |
cursor |
string | no | keyset pagination cursor |
per_page |
integer | no | items per page, default 20 |
Response: an array of WorkItemBasic.
Attach a child
POST /-/work_items/:work_item_iid/children/:child_id returns 201.
Attaches an existing work item as a child.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the parent work item, path parameter |
child_id |
integer | yes | numeric ID of the work item to attach, path parameter |
child_id is the child’s numeric ID, not its iid, because a child can live in another namespace. It is not the gid:// value returned in the global_id field.
Response: the child as WorkItemBasic. Returns 409 when the work item is already a child of this parent, and 422 for an invalid hierarchy.
Detach a child
DELETE /-/work_items/:work_item_iid/children/:child_id returns 204, no body.
Removes the parent/child link.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the parent work item, path parameter |
child_id |
integer | yes | numeric ID of the child, path parameter |
Returns 422 when the link cannot be removed.
Reorder a child
PUT /-/work_items/:work_item_iid/children/:child_id returns 200.
Moves a child within its siblings.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the parent work item, path parameter |
child_id |
integer | yes | numeric ID of the child to move, path parameter |
move_before_id |
integer | no | numeric ID of the sibling to move before |
move_after_id |
integer | no | numeric ID of the sibling to move after |
At least one of move_before_id or move_after_id is required, and the anchor sibling must be readable by the caller.
Response: the child as WorkItemBasic. Returns 422 when the move is invalid.
Notes endpoints
Notes are the individual comments and system-generated activity entries on a work item. Listing is the only note operation available today, with more on the way.
List notes
GET /-/work_items/:work_item_iid/notes
Lists notes on the work item.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
order_by |
string | no | default created_at, or updated_at |
sort |
string | no | default asc, or desc |
activity_filter |
string | no | default all_notes, or only_comments, only_activity |
cursor |
string | no | keyset pagination cursor |
per_page |
integer | no | items per page, default 20 |
Requires :read_note in addition to :read_work_item.
Response: an array of Note.
Discussion endpoints
A discussion is a thread of notes. Both endpoints require :read_note in addition to :read_work_item.
List discussions
GET /-/work_items/:work_item_iid/discussions
Lists the discussions on the work item.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
sort |
string | no | default asc, or desc |
activity_filter |
string | no | default all_notes, or only_comments, only_activity |
cursor |
string | no | keyset pagination cursor |
per_page |
integer | no | default 20, maximum 100 |
Response: an array of Discussion.
Get a discussion
GET /-/work_items/:work_item_iid/discussions/:discussion_id
Returns a single discussion.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
discussion_id |
string | yes | ID of the discussion, path parameter |
Response: a single Discussion. Discussions generated from resource events, such as label or state changes, have no rows in notes and cannot be fetched here, so an id taken from the list response can return 404. GraphQL behaves the same way.
Development widget endpoints
These four back the development widget, which links a work item to the merge requests, branches, and feature flags related to it. All of them describe project-scoped concepts, so a group-level work item returns an empty collection from each.
Closing merge requests
GET /-/work_items/:work_item_iid/closing_merge_requests
Lists the merge requests that close the work item.
Parameters and response
Takes work_item_iid in the path, plus cursor and per_page.
Response: an array of ClosingMergeRequest, which exposes id, from_mr_description, and a nested merge_request.
The query is not scoped to the work item’s project, because a merge request in another project can close it. Visibility is enforced in SQL, so pages are never short.
Related merge requests
GET /-/work_items/:work_item_iid/related_merge_requests
Lists the merge requests that reference the work item.
Parameters and response
Takes work_item_iid in the path, plus cursor and per_page.
Response: an array of MergeRequestBasic.
The set is derived by the same service that backs the GraphQL development widget, so REST tracks GraphQL rather than deriving the set a second way. Results are ordered by iid with id as a tiebreak, which keeps pagination stable across pages. When the explicit_mr_work_item_relations flag is enabled, the response also includes merge requests explicitly related to the work item, not only those that reference it in a note. A merge request that is both is returned once.
Related branches
GET /-/work_items/:work_item_iid/related_branches
Lists the branches whose name references the work item.
Parameters and response
Takes work_item_iid in the path, plus cursor and per_page.
Response: an array of RelatedBranch with name, compare_path, and pipeline_status.
Branches come from Gitaly rather than the database, so they are sorted by name and paginated in memory. A user without read_code gets an empty array.
Feature flags
GET /-/work_items/:work_item_iid/feature_flags
Lists the feature flags linked to the work item. EE builds only, no license required.
Parameters and response
Takes work_item_iid in the path, plus cursor and per_page.
Response: an array of FeatureFlag with id, name, active, path, and reference.
Unreadable flags are filtered out before pagination runs, so pages are never short, unlike the endpoints described in Pagination that filter afterwards.
Other sub-resource endpoints
These expose one concept each and share nothing beyond the conventions described above.
Linked items
GET /-/work_items/:work_item_iid/linked_items
Lists work items linked to this one.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
state |
string | no | opened or closed |
link_type |
string | no | relates_to; blocks and is_blocked_by with Premium |
fields |
string | no | comma-separated field list |
features |
string | no | comma-separated feature list |
cursor |
string | no | keyset pagination cursor |
per_page |
integer | no | items per page, default 20 |
Response: an array of LinkedWorkItem, which is WorkItemBasic plus link_id, link_type, link_created_at, and link_updated_at.
Linked resources
GET /-/work_items/:work_item_iid/linked_resources
Lists linked external resources, currently Zoom links only.
Parameters and response
Takes work_item_iid in the path, plus cursor and per_page.
Response: an array of LinkedResource, which exposes a single url field.
Emoji reactions
GET /-/work_items/:work_item_iid/award_emoji
Lists emoji reactions on the work item.
Parameters and response
Takes work_item_iid in the path, plus cursor and per_page.
Response: an array of AwardEmoji.
Current user to-dos
GET /-/work_items/:work_item_iid/current_user_todos
Lists the calling user’s to-dos on the work item. It is always scoped to that user, there is no way to read another user’s to-dos.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
state |
string | no | pending or done, omitted returns both |
cursor |
string | no | keyset pagination cursor |
per_page |
integer | no | items per page, default 20 |
Response: an array of Todo.
Email participants
GET /-/work_items/:work_item_iid/email_participants
Lists the Service Desk email participants on the work item.
Parameters and response
| Parameter | Type | Required | Description |
|---|---|---|---|
work_item_iid |
integer | yes | IID of the work item, path parameter |
fields |
string | no | comma-separated field list, see below |
cursor |
string | no | keyset pagination cursor |
per_page |
integer | no | items per page, default 20 |
This endpoint has its own fields vocabulary, unrelated to the work item field list: id and email by default, plus created_at and updated_at. Email addresses are masked according to the caller’s permissions.
Response: an array of EmailParticipant. Returns 404 when the work item type has no email participants widget.
Two things set this endpoint apart from every other route in the API: its feature category is service_desk rather than portfolio_management, and it is the only group-scoped route that declares job token policies.
478121f6)
