Remove OKRs
As we are not pursuing the OKR functionality we've decided to remove the Objective and Key result types. That means as we pursue custom work item types, users should be able to utilize "Objective" and "Key result" type names, and we do not need any logic to show/hide OKR types.
No in-product deprecation notice should be needed as the OKR feature was never generally available.
**General todos (to be updated by engineering)**
* Remove Objective and Key result types
* Remove OKRs docs
Initial research is following:
# OKR Removal from GitLab -- Research and Plan (Engineering)
## Executive Summary
OKRs (Objectives and Key Results) are an EE-only feature gated behind the okrs_mvc feature flag (disabled by default) and the okrs licensed feature. The feature was introduced in milestone 15.6 and has never been generally available. Removing it touches roughly 60+ files across backend, frontend, GraphQL, workers, mailers, and policies. The primary complexity is not the code removal itself but the data migration strategy for existing objectives and key results on SaaS and self-managed instances.
This document maps the full surface area, evaluates migration strategies, and proposes a phased removal plan.
## 1. Current State
### Feature Flags
- okrs_mvc (EE, development, default disabled) -- gates type visibility, creation permissions, UI rendering
- okr_checkin_reminders (CE-side definition, EE-only consumption, default disabled) -- gates the checkin_reminder quick action and the cron worker
### Licensed Feature
The :okrs licensed feature is registered in GitlabSubscriptions::Features and controls both type availability and the progress widget. Both Objective (id: 6) and KeyResult (id: 7) map to this license.
### Type System Context
GitLab has two parallel type definition systems coexisting:
1. Legacy DB-backed: WorkItems::Type with BASE_TYPES hash (9 types, ids 1-9) (will be removed)
2. New in-memory FixedItemsModel: WorkItems::TypesFramework::SystemDefined::Type and custom types
Objective is id 6, KeyResult is id 7. These are listed in EE_BASE_TYPES. The issues table references types via work_item_type_id (bigint, NOT NULL). A DB trigger (validate_work_item_type_id_is_valid) accepts ids 1-9 unconditionally and validates ids >= 1001 against the work_item_custom_types table. The FK from issues to work_item_types has been removed in milestone 18.10.
## 2. Full Codebase Inventory
### 2.1 Backend -- Models and Associations
OKR-specific model: ee/app/models/work_items/progress.rb
Table: work_item_progresses (PK is issue_id, 1:1 with issues)
Columns: progress, start_value, end_value, current_value, rollup_progress, reminder_frequency, last_reminder_sent_at, namespace_id
Validates progress 0-100, computes from start/current/end values
After-commit triggers UpdateParentObjectivesProgressWorker
Widget: ee/app/models/work_items/widgets/progress.rb
Delegates progress fields to the Progress record
Type definitions (new system):
ee/app/models/work_items/types_framework/system_defined/definitions/objective.rb (id: 6, widgets include progress)
ee/app/models/work_items/types_framework/system_defined/definitions/key_result.rb (id: 7, widgets include progress, can_promote_to_objective)
Key model extensions:
ee/app/models/ee/work_item.rb -- has_one :progress, scopes for reminder queries, average_progress_of_children
ee/app/models/ee/issue.rb -- show_as_work_item?/use_work_item_url? for OKR types, okr_available_and_enabled?
ee/app/models/ee/todo.rb -- OKR_CHECKIN_REQUESTED action (id: 12)
app/models/todo.rb -- OKR_CHECKIN_REQUESTED = 12 constant (FOSS side)
app/models/issue.rb -- TYPES_FOR_LIST includes objective and key_result
app/models/concerns/issue_available_features.rb -- confidentiality for OKR types
Type filtering:
app/models/work_items/types_filter.rb -- OKR_TYPES constant, filter_okr method (FOSS excludes all OKR types)
ee/app/models/ee/work_items/types_filter.rb -- conditionally re-adds OKR types when licensed + flag enabled
ee/app/models/ee/work_items/type.rb -- LICENSED_WIDGETS, LICENSED_TYPES mappings for :okrs
ee/app/models/ee/work_items/types_framework/system_defined/type.rb -- okrs_enabled? gating
Hierarchy: Objective allows children: Objective (depth 9), KeyResult (depth 1). KeyResult is a leaf type.
### 2.2 Backend -- Services
ee/app/services/work_items/callbacks/progress.rb -- before_update callback for progress widget CRUD
ee/app/services/work_items/data_sync/widgets/progress.rb -- stub for move/clone (empty methods)
Note: The progress widget and its backing table/model may be worth preserving independently of OKRs. It is a general-purpose percentage-tracking widget that could apply to any work item type. If retained, the removal scope shrinks: we keep the Progress model, widget, callback service, GraphQL types, and the work_item_progresses table, but decouple them from OKR-specific types. The widget definitions would simply need to be reassigned to whichever types should support progress tracking going forward. This decision affects Phase 3 scope significantly.
ee/app/services/ee/todo_service.rb -- request_okr_checkin creates OKR_CHECKIN_REQUESTED todo
ee/app/services/ee/system_note_service.rb -- change_progress_note, change_checkin_reminder_note
Open decision: The check-in reminder system (cron worker, finder, mailer, todo action, quick action) is OKR-branded but architecturally generic -- it sends periodic nudges to assignees of items with a configurable frequency. This could be generalised into a "reminder" feature for any work item type rather than removed outright. If generalised, the OKR naming gets stripped but the worker, finder, mailer infrastructure, and the reminder_frequency/last_reminder_sent_at columns on work_item_progresses survive (potentially moved to a different table if progress is decoupled). If removed, all of it goes. This decision should be made before Phase 3 scoping.
ee/app/services/ee/system_notes/issuables_service.rb -- implementations of the above
### 2.3 Backend -- Policies
ee/app/policies/ee/project_policy.rb -- okrs_enabled condition, :create_objective/:create_key_result abilities
ee/app/policies/ee/namespaces/group_project_namespace_shared_policy.rb -- same pattern at namespace level
### 2.4 Backend -- Workers
ee/app/workers/okrs/checkin_reminder_emails_cron_worker.rb -- daily cron (01:00 UTC), sends reminder emails
ee/app/workers/work_items/update_parent_objectives_progress_worker.rb -- rolls up child progress to parent
config/initializers/1_settings.rb -- cron schedule for okr_checkin_reminder_emails
### 2.5 Backend -- Finders, Mailers, Quick Actions
ee/app/finders/okrs/checkin_reminder_key_result_finder.rb -- finds KRs needing reminders
ee/app/mailers/emails/okr.rb -- okr_checkin_reminder_notification
ee/app/mailers/ee/notify.rb -- includes Emails::Okr
ee/app/views/notify/okr_checkin_reminder_notification.html.haml -- HTML template
ee/app/views/notify/okr_checkin_reminder_email.text.erb -- text template
ee/lib/ee/gitlab/quick_actions/work_item_actions.rb -- /checkin_reminder, /promote_to objective
### 2.6 Backend -- Controllers and Helpers
ee/app/controllers/ee/groups_controller.rb -- pushes okrs_mvc frontend flag
ee/app/controllers/ee/projects/issues_controller.rb -- pushes okrs_mvc frontend flag
ee/app/controllers/ee/projects/work_items_controller.rb -- pushes okrs_mvc frontend flag
app/helpers/search_helper.rb -- OKR_TYPES constant, okrs_mvc_enabled? (returns false in FOSS)
ee/app/helpers/ee/search_helper.rb -- overrides okrs_mvc_enabled? to check flag
ee/app/helpers/ee/issues_helper.rb -- passes has_okrs_feature to frontend
ee/app/helpers/ee/boards_helper.rb -- passes has_okrs_feature to boards
### 2.7 GraphQL
app/graphql/types/issue_type_enum.rb -- OBJECTIVE, KEY_RESULT values
app/graphql/types/work_items/type_type.rb -- can_promote_to_objective field
app/graphql/types/namespaces/available_features_type.rb -- has_okrs_feature field
ee/app/graphql/types/work_items/widgets/progress_type.rb -- WorkItemWidgetProgress type
ee/app/graphql/types/work_items/widgets/progress_input_type.rb -- input type
ee/app/graphql/types/work_items/widget_definitions/progress_type.rb -- definition type with show_popover
ee/app/graphql/ee/mutations/work_items/update.rb -- progress_widget argument
ee/app/graphql/ee/resolvers/work_items_resolver.rb -- progress preload
ee/app/graphql/resolvers/concerns/ee/work_items/look_ahead_preloads.rb -- progress look-ahead
### 2.8 Frontend (Key Areas)
Vue components for progress widget, OKR-specific tree actions (add objective/key result children), type icons, promote-to-objective action. Feature flag glue_okrs_mvc injected by controllers. Work item list and detail views conditionally show OKR types based on hasOkrsFeature/okrsMvc provides.
Frontend files are spread across both app/assets/javascripts/ (FOSS, shared components) and ee/app/assets/javascripts/ (EE-specific progress widget, change type modal extensions, list utils).
### 2.9 Other
app/services/mcp/tools/concerns/constants.rb -- GROUP_ONLY_TYPES includes Objective, KeyResult
ee/app/models/gitlab_subscriptions/features.rb -- :okrs licensed feature
app/models/work_items/types_framework/icon_definitions.rb -- KEY_RESULT and OBJECTIVE icon constants
## 3. Database Impact
### 3.1 Tables Affected
work_item_progresses -- the only OKR-specific table. PK is issue_id, FK to issues (CASCADE), FK to namespaces (CASCADE). No other tables reference it. If the progress widget is retained for use on other types (see Section 2.2 note), this table stays and the OKR-specific columns (reminder_frequency, last_reminder_sent_at) may be dropped or repurposed depending on the check-in reminder decision. If progress is removed entirely, the table can be dropped cleanly.
issues -- rows with work_item_type_id = 6 (Objective) or 7 (KeyResult) need to be retyped. Column is NOT NULL.
work_item_types -- rows for id 6 and 7 exist (legacy DB-backed types). These can be left or removed; the application no longer uses the DB table for type resolution (FixedItemsModel handles it).
work_item_widget_definitions -- rows linking progress widget to type ids 6 and 7. Seeded by BaseTypeImporter. Can be cleaned up.
Potentially affected (rows may exist for type ids 6/7):
work_item_type_user_preferences
work_item_type_custom_fields
work_item_type_custom_lifecycles
work_item_custom_status_mappings
### 3.2 The DB Trigger Question
The validate_work_item_type_id_is_valid trigger unconditionally accepts ids 1-9. Removing OKR types from the application does NOT require changing this trigger. The trigger is a safety net, not an authorization gate. The application layer prevents creation of OKR work items when the types are not defined. Leaving ids 6 and 7 valid in the trigger is harmless and avoids a risky trigger migration.
### 3.3 Partial Indexes
No partial indexes currently filter on work_item_type_id = 6 or 7. The only hardcoded type ID in a partial index is work_item_type_id = 2 (Incident). No index changes needed.
## 4. Migration Strategies for Existing Data
This is the crux of the problem. Customers on SaaS and self-managed may have existing objectives and key results, even if the feature was behind a flag. We need a graceful degradation path.
### 4.1 Option A: Convert to Issues via Batched Background Migration (Recommended)
How it works:
1. Batched background migration iterates issues WHERE work_item_type_id IN (6, 7)
2. Updates work_item_type_id to 1 (Issue)
3. Separate batch deletes orphaned work_item_progresses rows (or rely on a later table drop)
Pros:
- Simple, well-understood pattern (see BackfillWorkItemTransitions for prior art)
- Issues are the universal fallback type; every feature available on issues works
- No data loss on the core issue fields (title, description, labels, assignees, milestones, dates)
- Parent-child links remain valid (Issue can be a child of Epic; Issue allows Task children)
- Progress data is lost, but it was behind a disabled feature flag anyway
Cons:
- Customers who actively used OKRs lose the type distinction and progress tracking
- Hierarchy between objectives and their child key results becomes flat issue-to-issue, which is not a valid parent-child relationship (Issue cannot parent Issue unless epic hierarchy is involved). The parent_links for objective-to-key_result would need to be either: (a) removed, or (b) the hierarchy restriction for Issue updated to allow Issue children temporarily
Hierarchy complication detail:
Currently Issue can only have Task children. If we convert objectives/key_results to issues, existing parent_links where an objective parented a key_result become an issue parenting an issue, which violates hierarchy restrictions. We must either:
- Delete these parent_links in the migration (losing hierarchy info)
- Convert the parent to Epic if it was a top-level objective (but this changes semantics)
- Temporarily add Issue-to-Issue as a valid hierarchy (messy, undesirable)
Recommendation: Delete parent_links between converted items. The hierarchy was OKR-specific and does not map cleanly to other types. Create system notes on affected items documenting that they were converted from OKR types and their parent/child relationships were removed.
### 4.2 Option B: Convert to Custom Types
How it works:
1. For each namespace/organization that has objectives or key results, create custom types named "Objective" and "Key Result" with converted_from_system_defined_type_identifier pointing to the original system type ids (6, 7)
2. Update issues.work_item_type_id to the new custom type id (>= 1001)
3. Because custom types delegate widgets and hierarchy to their source system type, the progress widget and hierarchy rules would continue to work... except we are removing the system type definitions
Problem: This approach has a fatal flaw. Custom types delegate ALL behaviour to the system-defined type they were converted from. If we remove the Objective and KeyResult system type definitions, the delegation source disappears. The custom types would fall back to the Issue type (the default delegation source), defeating the purpose.
To make this work, we would need to:
- Keep the system type definitions in code but mark them as non-creatable
- Or implement actual widget customization on custom types (currently stubbed)
This is significantly more work and keeps OKR code alive in the codebase, which is the opposite of what we want.
Verdict: Not viable unless we invest in custom type independence from system types first. Too much coupling.
### 4.3 Option C: Convert to Custom Types with Independent Configuration
A more ambitious variant of Option B where we first implement custom type widget independence, then migrate OKR items to truly standalone custom types. The custom types would own their own widget definitions rather than delegating to system types.
Pros:
- Cleanest outcome for customers -- they keep their data and can customize further
- Pushes the custom types system forward
Cons:
- Requires building a feature (custom type widget independence) that may not be on the roadmap
- Significantly delays OKR removal
- The progress widget code must remain in the codebase to serve these custom types
- Defeats the purpose of removing OKR code
Verdict: Only makes sense if custom type widget independence is already planned and imminent.
### 4.4 Option D: Soft Deprecation (Hide but Keep)
How it works:
1. Add objective and key_result to DISABLED_WORKFLOW_TYPES in TypesFilter (same pattern as requirement and test_case)
2. Remove the feature flags
3. Keep the type definitions but make them non-creatable
4. Existing items remain as-is but users cannot create new ones
Pros:
- Zero migration risk
- Existing data is untouched
- Minimal code change for the initial step
Cons:
- OKR code remains in the codebase indefinitely
- Progress widget, workers, mailers, finders all stay
- Technical debt accumulates
- Not a real removal, just a hide
Verdict: Acceptable as Phase 1 of a multi-phase approach, but not a final state.
### 4.5 Recommended Approach: Phase 1 Soft Deprecation, Phase 2 Hard Migration
Phase 1 -- Soft Deprecation (low risk, fast):
1. Add objective and key_result to DISABLED_WORKFLOW_TYPES
2. Remove okrs_mvc and okr_checkin_reminders feature flags
3. Remove the :okrs licensed feature
4. Stop the cron worker (remove schedule from 1_settings.rb)
5. Hide OKR types from all UI surfaces, API filters, and search
6. Existing items remain accessible (read-only effectively, since type is hidden from creation)
Phase 2 -- Data Migration (requires release note / deprecation notice):
1. Add deprecation notice in release notes: "Objectives and Key Results will be converted to Issues in version X.Y"
2. Batched background migration: convert work_item_type_id from 6/7 to 1 (Issue) on all issues
3. Batched background migration: delete work_item_parent_links where both parent and child were OKR types (objective-to-objective, objective-to-key_result) that would violate Issue hierarchy restrictions
4. Create system notes on converted items: "This item was automatically converted from [Objective/Key Result] to Issue as part of the OKR feature removal."
5. Batched cleanup of work_item_progresses table rows for converted items
Phase 3 -- Code Removal:
1. Remove type definitions (definitions/objective.rb, definitions/key_result.rb)
2. Remove from BASE_TYPES, TYPE_NAMES, EE_BASE_TYPES
3. Remove OKR-specific workers, finders, mailers, email templates
4. Remove policies, GraphQL types (after deprecation cycle for can_promote_to_objective), frontend components
5. Remove factories, specs, fixtures
6. Remove icon definitions for objective and key_result
7. Clean up work_item_types rows for ids 6 and 7
8. Clean up work_item_widget_definitions rows for those types
9. If progress widget is NOT retained: remove Progress model, widget, callbacks, services, GraphQL progress types; drop work_item_progresses table
10. If progress widget IS retained: decouple it from OKR types, reassign widget definitions to target types, strip OKR-specific naming
11. If check-in reminders are NOT generalised: remove cron worker, finder, mailer, quick action, todo action; drop reminder columns from work_item_progresses
12. If check-in reminders ARE generalised: rename and rebrand the infrastructure, decouple from OKR types
## 5. Migration Implementation Details
### 5.1 Batched Background Migration for Type Conversion
The migration would follow the pattern established by BackfillWorkItemTransitions:
Class: BackfillOkrWorkItemsToIssueType
Table: issues
Column: id
Scope: WHERE work_item_type_id IN (6, 7)
Batch size: 10,000
Sub-batch size: 100
Delay: 2 minutes
Each sub-batch:
1. SELECT id, work_item_type_id FROM issues WHERE id IN (sub_batch_ids) AND work_item_type_id IN (6, 7)
2. UPDATE issues SET work_item_type_id = 1, updated_at = NOW() WHERE id IN (matched_ids)
### 5.2 Parent Link Cleanup Migration
Class: CleanupOkrParentLinks
Table: work_item_parent_links
Scope: WHERE work_item_id IN (SELECT id FROM issues WHERE work_item_type_id IN (6, 7)) OR work_item_parent_id IN (SELECT id FROM issues WHERE work_item_type_id IN (6, 7))
Note: This migration MUST run BEFORE the type conversion migration, because after conversion the items will be type 1 (Issue) and we will not be able to distinguish former OKR items from real issues.
Alternative: Run the parent link cleanup in the same migration class as the type conversion, within the same transaction per sub-batch. That way we can identify OKR items and clean their links before changing their type.
### 5.3 Progress Data Cleanup
Depends on the progress widget retention decision (see Section 2.2 note):
If progress widget is removed entirely:
Drop the work_item_progresses table in a post_migrate. The FK with CASCADE on the issues side means nothing breaks. Cleanest option.
If progress widget is retained for other types:
Delete work_item_progresses rows for converted items (former OKR types) in the same batched migration as the type conversion. The table stays for future use. The OKR-specific columns (reminder_frequency, last_reminder_sent_at) can be dropped in a separate migration if the check-in reminder feature is also removed, or repurposed if it is generalised.
### 5.4 Self-Managed Considerations
Self-managed instances run batched background migrations during upgrades. The migration will execute automatically. Key concerns:
- Instances that enabled okrs_mvc manually may have significant OKR data
- The deprecation notice must be in release notes at least one milestone before the migration ships
- The migration should be idempotent (safe to re-run)
- Consider adding a Rake task for administrators to preview how many items will be affected before upgrading
## 6. Estimated Effort
### Phase 1 -- Soft Deprecation
- Add to DISABLED_WORKFLOW_TYPES: 1 line change
- Remove feature flags: 2 YAML files, update all flag checks to return false/remove conditionals
- Remove licensed feature: 1 line in features.rb, update license checks
- Stop cron worker: 1 line in 1_settings.rb
- Estimated: 1-2 MRs, low risk
### Phase 2 -- Data Migration
- Write batched background migration (type conversion + parent link cleanup): 1 MR
- Write post-migration to queue it: 1 MR
- System note creation for converted items: part of the migration
- Estimated: 1-2 MRs, medium risk
### Phase 3 -- Code Removal
Approximate file count by area:
- Type definitions and constants: ~10 files
- Workers: 2 files (or 1 if check-in reminders generalised)
- Finders: 1 file (or 0 if check-in reminders generalised)
- Mailers and templates: 4 files (or 0 if check-in reminders generalised)
- Quick actions: 1 file
- Policies: 2 files
- GraphQL types and mutations: ~8 files (deprecation cycle needed for can_promote_to_objective)
- Controllers and helpers: ~8 files
- Frontend components: ~15 files
- Specs and factories: ~50 files
- Database: 2-3 migrations
- If progress widget removed: +5 files (model, widget, callbacks, GraphQL types) + table drop
- If progress widget retained: refactor/rename pass on ~5 files
Total: ~80-110 files across 3-5 MRs depending on progress/check-in decisions, low-medium risk (mostly deletion)
## 7. Open Questions
1. Should we preserve a record of the OKR hierarchy in system notes when converting?
This is worth doing. During the parent link cleanup migration (Phase 2, step 3), before deleting each parent_link, we can create a system note on both the parent and the child recording the former relationship. For example, on the child: "This item was a Key Result under Objective #123. The parent-child link was removed as part of the OKR feature deprecation." On the parent: "Key Result #456 was removed as a child of this Objective during OKR deprecation." This adds cost to the migration (one Note record per affected link per side, so two notes per link) but preserves audit trail. The notes should be created by the Ghost User to avoid cluttering real user activity. The migration would need to batch note creation carefully to avoid overwhelming the notes table on instances with deep OKR hierarchies.
2. The OKR_CHECKIN_REQUESTED todo action (id: 12) is defined in the FOSS Todo model. After removal, existing todos with action 12 will have an orphaned action type. Should we migrate these to a generic action or delete them?
3. If the progress widget is kept, should existing progress data on converted items (now Issues) be preserved or stripped? Keeping it means Issues would display progress, which may be unexpected. Stripping it means deleting work_item_progresses rows for converted items while keeping the table for future use.
4. The can_promote_to_objective field on WorkItems::TypeType (GraphQL) will need a standard deprecation cycle before removal.
## 8. Risks
- GraphQL breaking changes: removing OBJECTIVE and KEY_RESULT from IssueTypeEnum and removing has_okrs_feature, can_promote_to_objective fields requires deprecation per GitLab API deprecation policy
- Self-managed instances with large OKR datasets could have slow migrations
- The parent link cleanup may surface unexpected hierarchy configurations
- System notes created during migration will appear in activity feeds, potentially confusing users
## 9. Sources
All findings based on codebase analysis of the gitlab repository at the current HEAD. Key entry points:
ee/config/feature_flags/development/okrs_mvc.yml
config/feature_flags/development/okr_checkin_reminders.yml
app/models/work_items/type.rb (BASE_TYPES, EE_BASE_TYPES)
ee/app/models/work_items/types_framework/system_defined/definitions/objective.rb
ee/app/models/work_items/types_framework/system_defined/definitions/key_result.rb
ee/app/models/work_items/progress.rb
app/models/work_items/types_filter.rb (DISABLED_WORKFLOW_TYPES pattern)
ee/app/models/work_items/types_framework/custom/type.rb (custom types delegation model)
db/structure.sql (trigger, table definitions, constraints)
issue