Work Items Custom Status
Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
ongoing |
msaleiko
|
ntepluhina
|
donaldcook
gweaver
|
devops plan | 2025-02-25 |
Summary
This document outlines our approach to implementing a flexible status system for work items in GitLab. We’re evolving beyond the binary open/closed states and label-based status tracking by introducing proper status fields with customization capabilities.
The solution introduces system-defined statuses as the foundation for Premium and Ultimate users, with users able to add statuses, change status name or color, and alter the order of statuses. We’ll initially target Tasks before expanding to Issues, Epics and other work item types, while Free users will continue using the binary open/closed state system.
This initiative lays the groundwork for users to more effectively manage work item lifecycles and addresses problems caused by requiring labels for status management.
Timeline & status updates
- Custom Status is the Plan Stage’s highest priority in FY26Q2, and we’ve planned to deliver the first two iterations to General Availability by the 18.2 milestone
- Status updates can be derived from the custom statuses epic and the iteration child epics.
Glossary
Existing concepts and terms
- Work Item Type: A classification that determines a work item’s available features and behaviors through its associated widgets.
- Widget: A functional component that provides specific capabilities to a work item type (for example “assignees” and “labels”).
- State: The fundamental binary classification of a work item as either ‘open’ or ‘closed’.
New concepts and terms
- Status: A specific step in a work item’s workflow (“In progress”, “Done”, “Won’t do”) that belongs to a category and maps to a binary state (open/closed).
- Status category: A logical grouping for statuses (triage, to_do, in_progress) that determines their effect on a work item’s state (and icon).
- System-defined status: A system-provided status that cannot be modified and that is available to get started with work item statuses.
- Custom Status: A namespace-defined status that replaces system-defined statuses for all groups and projects within that namespace.
- Lifecycle: A collection of statuses that can be applied to a work item type. It allows statuses to be grouped into meaningful workflows that can be reused consistently across types and namespaces.
- Status Widget: The component that displays status and allows users to modify the status of a work item on list, board, and detail views.
Motivation
GitLab currently relies on a combination of labels and two formal states (open/closed) to represent work item status. This approach has significant limitations for teams managing complex workflows. The binary open/closed system doesn’t provide enough context about where items stand in a process, and overusing labels for status tracking creates inconsistency and management overhead.
Goals
- Expand work item status beyond the binary open/closed system to represent more workflow stages
- Improve context on the status of work items
- Enable better reporting on the reasons for closing issues (completed vs. duplicated/moved/won’t do)
- Enhance lead/cycle time calculations by distinguishing active time from waiting time
- Build on top of our work item system to leverage status for all work item types
- Reduce reliance on labels for tracking status
Non-Goals
- Make status usable beyond the work items framework (for example for other entities outside the framework).
- Force every status-like functionality into work item status (for example incident paging status and requirements verification status)
Proposal
We propose implementing a flexible status system for work items in GitLab, built around these core concepts:
- Status is available for Premium and Ultimate customers.
- Free customers continue to use state (open/closed) and labels.
- Customizable workflows through lifecycles that can be applied to work item types and that hold statuses.
- System-defined statuses as a starting point.
- Allow users to create and modify status and lifecycles as custom statuses and custom lifecycles.
- Sync state and status along the workflow to ensure consistency and compatibility.
- Use the work item framework and widget concepts and the GraphQL API.
- Migration tools to assist users in transitioning from workflow labels to the new status system.
Design and implementation details
This section outlines the core concepts, implementation architecture, and rollout plan for the new work item status system in GitLab. We’ll explore the key components and detail how they interact to create a flexible, scalable status management solution.
Core Concepts
Lifecycles
Lifecycles function as a holder for statuses and can be applied to work item types. They define which statuses are available for items of a given work item type.
For example: In a customers root group, issues should use the “Engineering” lifecycle, tasks “Kanban”, and epics, objectives, and key results the “Strategy” lifecycle. All lifecycles reuse the “Done” and “Won’t do” statuses, but overall the number of statuses and their position is different.
We start with providing one default lifecycle for work item types like tasks, issues, epics, objectives and key results.
Service Desk tickets may use a different lifecycle with different statuses like waiting for first response
,
waiting for customer
and waiting for next response
. But that is to be defined.
See this discussion to learn more.
Statuses
A status can be used in multiple lifecycles and can be attached to a work item based on the available lifecycle of the work item type and namespace. The default lifecycle contains these statuses:
To do
In progress
Done
Won't do
Duplicate
Status Categories
Statuses are organized into categories that are not user-configurable that determine their behavior:
CATEGORIES = {
triage: 1, # Exists but without system-defined status
to_do: 2,
in_progress: 3,
done: 4,
canceled: 5
}.freeze
The category is only visible in the management area of statuses and not in list, detail and board views. The category defines the icon of the status.
Status-State Relationship and Transitions
Status builds on top of the existing state system (open and closed). We need to transition work items automatically when either state or status change according to the following rules:
- Statuses in
done
andcanceled
categories automatically set work items toclosed
state - All other categories maintain work items in
open
state - The lifecycle defines default transition statuses (this is true for both system-defined and custom lifecycles):
- Default Open Status: Applied when creating and reopening items
- Default Closed Status: Applied when closing items
- Default Duplicated Status: Applied when marking items as duplicates (or moved, promoted TBD)
Implementation Architecture
Distinction between system-defined and custom entities
We’ve decided against prepopulating statuses for each root namespace, as this approach would make modifying or extending statuses more challenging and significantly increase the database table size. Instead, we’re introducing system-defined statuses and lifecycles as defaults, which can then be customized into custom statuses and lifecycles.
The Cells initiative aims to make our platform horizontally scalable and resilient.
To achieve this, we need a sharding key in every database table.
Sharding keys cannot be null
, and we’re discouraged from using a custom sequence (such as using -1
for
system-defined statuses or lifecycles, or reserving the first 1,000
IDs for system-defined entities).
Because system-defined data doesn’t belong to a namespace and can be considered global, we need to handle it separately.
For all other tables, we’ll use namespace_id
as the sharding key.
We considered three options for managing system-defined entities:
- Define system-defined entities in code (chosen approach)
- Store system-defined data in a separate table
- Use a single table and reserve first
1_000
IDs with a customCHECK
constraint
We’ve chosen to implement the first option: defining system-defined entities in code. This approach offers the best balance of simplicity, scalability, and maintainability for our system.
Fixed items models and associations
Since system-defined data is static per definition we can hard-code it and benefit from not hitting the database. The challenge is that we cannot make joins and use ActiveRecord methods.
We introduce the concept of fixed items models that include a module that adds ActiveRecord-like methods and defines
the static data in an array of hashes called ITEMS
.
For example:
class StaticModel
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveRecord::FixedItemsModel::Model
ITEMS = [
{
id: 1,
name: 'To do'
}
]
attribute :id, :integer
attribute :name, :string
end
ActiveRecord-like methods can be used like:
StaticModel.find(1)
StaticModel.where(name: 'To do')
StaticModel.find_by(name: 'To do')
StaticModel.all
To make associations possible we introduce a custom association belongs_to_fixed_items
which resolves associations
to fixed items models like this (*_id
column needs to be present):
class MyModel < ApplicationRecord
include ActiveRecord::FixedItemsModel::HasOne
belongs_to_fixed_items :static_model, fixed_items_class: StaticModel
end
The association can then be used as:
m = MyModel.last
m.static_model # Returns fixed items model instance
m.static_model = StaticModel.find(1)
m.static_model_id = 1 # still possible
m.static_model? # Bool
m.save! # Saves association
Overview of classes
To encapsulate everything status related we use the WorkItems::Statuses
namespace.
Starting from the WorkItem
we’ll create a join model called WorkItems::Statuses::CurrentStatus
that holds the
status associations of a work item. We have separate columns system_defined_status_id
and custom_status_id
which map
to the different concepts of how we store system-defined and custom data.
The model itself abstracts that away and we just set and get a status (using #status
and #status=
).
We can determine on the root namespace level whether all descendants of the root namespace use system-defined statuses or custom statuses.
For lists that collect work items from various root namespaces we won’t check which status to use but instead check
for the availability of data on the join model.
If custom_status_id
is set, use the custom status. If not use the system-defined status.
To efficiently fetch this data for work item lists, we use a
bulk status resolver
which only adds two additional queries. One to load the join model and another to load custom statuses.
We use the fields default_open_status_id
, default_closed_status_id
, and default_duplicate_status_id
to make
automatic state/status transitions possible.
Namespace Configuration
Each root namespace exclusively uses either system-defined statuses or custom statuses. When editing statuses for the first time, the system creates copies of all system-defined statuses as custom statuses. All work items of this namespace and its descendants will need to be migrated from the old to the new statuses (which will happen in the background).
Lifecycles and statuses can only be configured on the root namespace level in the first iterations. It’s to be defined how the mechanic to configure on a lower level will work and which restrictions will apply.
In dashboards and lists that render items from different root namespaces:
Statuses with the same name across different namespaces will be grouped in frontend filters (for example in dashboards).
The API call will include a filter with the grouped statuses in a OR
condition.
For example: we filter for status Done
which is available in two root namespaces (A
and B
).
The frontend filter only shows Done
, but the API call will filter for A::Done OR B::Done
.
API design
We use the existing work items GraphQL API and build on top of the work items widgets concept.
The API returns status related data of a work item in the STATUS
widget.
We can get a list of available statuses for a given work item type in a namespace
by querying the widget definitions for the work item type.
We’ll add concrete queries once the widget API is finalized.
Permissions
We’ve decided not to introduce new permissions for work item statuses. Instead, authorization is handled
by existing work item permissions like read_work_item
or update_work_item
.
This approach avoids redundant permission checks by leveraging GraphQL’s higher-level query execution for authorization, improving query performance by reducing the number of Permission checks.
Additionally, work item status-specific resolvers like BulkStatusResolver
and AllowedStatusesResolver
ensure that the licensed feature is available and the feature flag is enabled before proceeding.
Status widget
We use the STATUS
widget.
We already introduced the mock API using the widget name CUSTOM_STATUS
because STATUS
was already taken.
This legacy widget represents the verification status of requirements and should be renamed to VERIFICATION_STATUS
.
We marked both widgets and fields as experiment in 17.9
, so we can rename them like this:
Backfill status data for existing work items
Each work item of a work item type that supports status should have a status assigned.
We’ll backfill the work_items_current_statuses
table before adding status support for a work item type.
To conserve database storage we’ll only backfill status data for open
work items.
Although custom status is a licensed feature, we will backfill status data for all work items of a given work item type regardless of license. We will also perform automatic status transitions for all items, including those that are open, closed, or marked as duplicates.
For example a newly created work item will receive the default open status, and when closed, it will transition to the default closed status.
See the discussion on this topic on this issue.
This approach ensures:
- Status is immediately available when a namespace adds a license.
- Work items maintain correct status assignments when a namespace changes tiers.
This significantly reduces complexity by eliminating the need for additional data migrations during namespace tier changes.
Status migration and migration wizard
We need to migrate statuses of work items in the following cases:
- A namespace transitions from system-defined statuses to custom statuses
- A user applies a different lifecycle to a work item type
- A user creates statuses from labels or scoped labels
- A status will be deleted and its assigned work items need to be migrated to a new status
We’ll persist a mapping from one status to another in the database and run a job in the background that updates the status association.
Optionally we’ll also consider the mapping in list queries and include the old and new status during the migration.
We acknowledge that there might be a short time where status data is inconsistent in list views during migration. This is especially true for namespaces with a large number of work items.
Details about the database structure and service architecture are to be defined.
Namespaces downgrade to free tier
We discussed implications of downtiering from using custom statuses to system-defined statuses. The main takeaway is that it would introduce a decent amount of complexity to handle mapping existing custom statuses to previously existing system-defined statuses.
Because of that we decided that status will only be available in Premium and Ultimate tier. When a customer transitions to the free tier, they won’t see status anymore. But we keep all status relevant data and associations and continue to do status/state transitions. If they decide to uptier again, they’ll see their custom statuses again and all work items will be in a correct status.
Other status-like functionality
In GitLab we have entities with functionality that is comparable to work item status but represent something different or are highly custom integrations for a specific purpose. While we have ideas on how to include these into work item statuses, we refrain from doing so in the forseable future. Specifically we’ve evaluated this for:
- Incident paging status. We plan to relabel this in the UI from “Status” to something else and when we migrate incidents to work items, we’ll introduce this in a separate widget for now. See this discussion for details
- Requirements verification status. We’re renaming the existing
STATUS
widget toVERIFICATION_STATUS
and keep the functionality separate.
Challenges
- Because the incidents detail view won’t be migrated to the work item detail view short-term, status won’t be available for this work item type. It’s to be defined whether we’ll work on an intermittent solution or postpone adding status to incidents until the migration is completed. See this discussion for details
Feature flags and licensed feature
We’ll use the feature flag work_item_status_feature_flag
throughout the development of this feature.
The actor needs to be the root group.
For testing purposes, the feature flag is currently enabled in production for the Plan Stage testing group called gl-demo-ultimate-plan-stage.
We’re using this feature flag rollout issue.
Since the feature will only be available in Premium and Ultimate tier, we consider it a licensed feature.
The feature name is work_item_status
.
The name differs from the feature flag because we cannot use the same name.
Implementation and release plan
We’ve identified these iterations for this initiative:
Iteration 1 (internal dogfooding)
- Iteration 1 epic
- Implement system-defined status and join model
- Make the status widget visible on Tasks only
- Implement status setting and viewing functionality
- Implement state/status transitions
- Add
/status
quick action
We want to dogfood the first iteration internally to gather early feedback. To make this happen we’ll use the following approach:
- Continue to use the
work_item_status_feature_flag
for the full GA release. - Move the parts we want to dogfood to
work_items_beta
feature flag which is enabled for thegitlab-org
andgitlab-com
groups. This way we only release the feature internally and are still able to disable the feature. - We’ll use this rollout issue.
Iteration 2 (GA)
- Iteration 2 epic
- Implement custom statuses
- Board integration
- Filter by a single status on list views (if ready only work item list, else legacy list)
- Status management (create, update, reorder, delete)
- Migration from labels to statuses
- Expand support to Issues and Epics
Iteration 2 is the GA release. The following changes need to happen to change from internal dogfooding to GA:
- Change the feature flag of the internal dogfooding paths back to
work_item_status_feature_flag
. - Enable the feature flag by default in the same MR.
Iteration 3
- Iteration 3 epic
- Customizable status for all work item types
- Status available on dashboards (more complex because they can be fed by different root namespaces)
- Calculations for milestone and iteration burndown
Future iterations
- Future iterations epic
- Customize lifecycles / statuses at any hierarchy level (group, subgroup project)
Internationalization
System-defined statuses will only use english names. Customers can customize statuses to match their preferred language.
Alternative Solutions
Do nothing and continue to use state and labels
Pros:
- No change to existing workflows
- No development cost
Cons:
- Doesn’t address customer feedback about label overuse
- Doesn’t provide a first-class status integration into the product
- Limits reporting capabilities
Decision registry
This section documents key architectural and implementation decisions made during the development of this feature.
- Define system-defined entities in code rather than database tables.
- Status will only be available in Premium and Ultimate tier.
- No new permissions for work item statuses. Reuse
read_work_item
andupdate_work_item
. - Use the name
STATUS
widget for custom status. Rename existingSTATUS
widget toVERIFICATION_STATUS
and renameCUSTOM_STATUS
widget toSTATUS
. - Statuses are unique across the namespace and are attached to a lifecycle.
We don’t create new statuses for each lifecycle. So the status
done
may be attached to multiple lifecycles. - System-defined statuses won’t be internationalized. We only use english names.
- We’ll dogfood iteration 1 internally. and use the release plan outlined in this document.
- We’ll backfill only open work items with a default open status.
- We’ll always add status data regardless of license to eliminate the need for additional data migrations during tier changes.
- We will be implementing work item status badge and filters in legacy issues list and epic work item list. We will not be supporting legacy epics list.
Resources
- Top level epic for this initiative
- Designs for list/detail/board
- Designs for status management
- Designs for status migration wizard
- Initial spike work for first iteration
- Proof of concept (POC) for system-defined and custom statuses
Team
Please mention the current team in all MRs related to this document to keep everyone updated. We don’t expect everyone to approve changes.
@gweaver @nickleonard @donaldcook @ntepluhina @msaleiko @aslota @deepika.guliani @stefanosxan
Feel free to mention the following people to spread the word:
@johnhope @amandarueda @caitlinsteele
75800ce2
)