diff --git a/app/models/work_items/related_work_item_link.rb b/app/models/work_items/related_work_item_link.rb
index fb0069541fb08548b762713918e5472acae660c7..6f05ff8dfbdbe94dda8e021eb6ed01708e90a47e 100644
--- a/app/models/work_items/related_work_item_link.rb
+++ b/app/models/work_items/related_work_item_link.rb
@@ -3,6 +3,8 @@
 module WorkItems
   class RelatedWorkItemLink < ApplicationRecord
     include LinkableItem
+    include CreatedAtFilterable
+    include UpdatedAtFilterable
 
     self.table_name = 'issue_links'
 
diff --git a/ee/app/models/ee/work_items/related_work_item_link.rb b/ee/app/models/ee/work_items/related_work_item_link.rb
index 344b06cbbe768dc8e57f9c00fbfee8b4891af341..b0f97ac13a7458e204f9f26dfaf760bd2438f175 100644
--- a/ee/app/models/ee/work_items/related_work_item_link.rb
+++ b/ee/app/models/ee/work_items/related_work_item_link.rb
@@ -10,6 +10,11 @@ module RelatedWorkItemLink
       prepended do
         has_one :related_epic_link, class_name: '::Epic::RelatedEpicLink', foreign_key: 'issue_link_id',
           inverse_of: :related_work_item_link
+
+        scope :for_source_type, ->(type) { joins(source: [:work_item_type]).where(source: { work_item_type_id: type }) }
+        scope :for_target_type, ->(type) { joins(target: [:work_item_type]).where(target: { work_item_type_id: type }) }
+
+        scope :preload_for_epic_link, -> { preload(:related_epic_link, source: [:synced_epic], target: [:synced_epic]) }
       end
 
       override :validate_related_link_restrictions
diff --git a/ee/app/services/work_items/legacy_epics/related_epic_links/list_service.rb b/ee/app/services/work_items/legacy_epics/related_epic_links/list_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..903682ee0504f6d28688c64c6827171f291d8520
--- /dev/null
+++ b/ee/app/services/work_items/legacy_epics/related_epic_links/list_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module WorkItems
+  module LegacyEpics
+    module RelatedEpicLinks
+      class ListService
+        include Gitlab::Utils::StrongMemoize
+
+        def initialize(legacy_epics, group)
+          @legacy_epics = legacy_epics
+          @group = group
+        end
+
+        def execute
+          if Feature.enabled?(:related_epic_links_from_work_items, group)
+            WorkItems::RelatedWorkItemLink
+              .for_source_or_target(legacy_epics.select(:issue_id))
+              .for_source_type(epic_type)
+              .for_target_type(epic_type)
+              .preload_for_epic_link
+
+          else
+            Epic::RelatedEpicLink.for_source_or_target(legacy_epics)
+          end
+        end
+
+        private
+
+        def epic_type
+          ::WorkItems::Type.default_by_type(:epic)
+        end
+        strong_memoize_attr :epic_type
+
+        attr_reader :legacy_epics, :group
+      end
+    end
+  end
+end
diff --git a/ee/config/feature_flags/beta/related_epic_links_from_work_items.yml b/ee/config/feature_flags/beta/related_epic_links_from_work_items.yml
new file mode 100644
index 0000000000000000000000000000000000000000..730e73779af1050ec1cf2a3d061502cfe949a19d
--- /dev/null
+++ b/ee/config/feature_flags/beta/related_epic_links_from_work_items.yml
@@ -0,0 +1,9 @@
+---
+name: related_epic_links_from_work_items
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502553
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179742
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516212
+milestone: '17.10'
+group: group::product planning
+type: beta
+default_enabled: false
diff --git a/ee/lib/api/entities/related_epic_link.rb b/ee/lib/api/entities/related_epic_link.rb
index 2f3afb897e47d365795de4ffcf21f182e46b96ee..40cd82230bb278494e53a729844efe73b09629b0 100644
--- a/ee/lib/api/entities/related_epic_link.rb
+++ b/ee/lib/api/entities/related_epic_link.rb
@@ -9,6 +9,33 @@ class RelatedEpicLink < Grape::Entity
       expose :link_type, documentation: { type: "string", example: "relates_to" }
       expose :created_at, documentation: { type: "dateTime", example: "2022-01-31T15:10:45.080Z" }
       expose :updated_at, documentation: { type: "dateTime", example: "2022-01-31T15:10:45.080Z" }
+
+      def id
+        case object
+        when ::Epic::RelatedEpicLink
+          object.id
+        when ::WorkItems::RelatedWorkItemLink
+          object.related_epic_link.id
+        end
+      end
+
+      def source
+        case object.source
+        when ::Epic
+          object.source
+        when ::WorkItem
+          object.source.synced_epic
+        end
+      end
+
+      def target
+        case object.target
+        when ::Epic
+          object.target
+        when ::WorkItem
+          object.target.synced_epic
+        end
+      end
     end
   end
 end
diff --git a/ee/lib/api/related_epic_links.rb b/ee/lib/api/related_epic_links.rb
index f6961be18a7233f50ed757a70334a1ce84ca162f..ce68e8bc7e2c1c3e8574dba884ec73fabde0b4c5 100644
--- a/ee/lib/api/related_epic_links.rb
+++ b/ee/lib/api/related_epic_links.rb
@@ -65,26 +65,32 @@ def find_permissioned_epic!(iid, group_id: nil, permission: :admin_epic_link_rel
       end
       get ':id/related_epic_links' do
         accessible_epics = EpicsFinder.new(current_user, group_id: user_group.id).execute
-        related_epic_links = Epic::RelatedEpicLink.for_source_or_target(accessible_epics)
 
-        related_epic_links = related_epic_links.updated_before(params[:updated_before]) if params[:updated_before]
-        related_epic_links = related_epic_links.updated_after(params[:updated_after]) if params[:updated_after]
-        related_epic_links = related_epic_links.created_before(params[:created_before]) if params[:created_before]
-        related_epic_links = related_epic_links.created_after(params[:created_after]) if params[:created_after]
+        related_links = ::WorkItems::LegacyEpics::RelatedEpicLinks::ListService
+          .new(accessible_epics, user_group).execute
 
-        related_epic_links = paginate(related_epic_links).with_api_entity_associations
-        related_epic_links.each { |link| [link.source, link.target].each(&:lazy_labels) }
+        related_links = related_links.updated_before(params[:updated_before]) if params[:updated_before]
+        related_links = related_links.updated_after(params[:updated_after]) if params[:updated_after]
+        related_links = related_links.created_before(params[:created_before]) if params[:created_before]
+        related_links = related_links.created_after(params[:created_after]) if params[:created_after]
+
+        related_links = paginate(related_links)
+        related_links.each { |link| [link.source, link.target].each(&:lazy_labels) }
 
         # EpicLinks can link to other Epics the user has no access to.
         # For these epics we need to check permissions.
-        related_epic_links = related_epic_links.select do |related_epic_link|
-          related_epic_link.source.readable_by?(current_user) && related_epic_link.target.readable_by?(current_user)
+        related_links = related_links.select do |related_link|
+          related_link.source.readable_by?(current_user) && related_link.target.readable_by?(current_user)
         end
 
-        source_and_target_epics = related_epic_links.reduce(Set.new) { |acc, link| acc << link.source << link.target }
+        source_and_target_epics = related_links.reduce(Set.new) { |acc, link| acc << link.source << link.target }
+
+        if Feature.enabled?(:related_epic_links_from_work_items, user_group)
+          source_and_target_epics = source_and_target_epics.map(&:synced_epic)
+        end
 
         epics_metadata = Gitlab::IssuableMetadata.new(current_user, source_and_target_epics).data
-        present related_epic_links, issuable_metadata: epics_metadata, with: Entities::RelatedEpicLink
+        present related_links, issuable_metadata: epics_metadata, with: Entities::RelatedEpicLink
       end
 
       desc 'Get related epics' do
diff --git a/ee/spec/factories/related_epic_links.rb b/ee/spec/factories/related_epic_links.rb
index 0a98abbca5a102a9073f78c3bd753bbc06965143..21d28f72f44966e784374341e89bb1db78fb76c2 100644
--- a/ee/spec/factories/related_epic_links.rb
+++ b/ee/spec/factories/related_epic_links.rb
@@ -7,7 +7,13 @@
 
     trait :with_related_work_item_link do
       related_work_item_link do
-        association(:work_item_link, source: source&.work_item, target: target&.work_item, link_type: link_type)
+        association(:work_item_link,
+          source: source&.work_item,
+          target: target&.work_item,
+          link_type: link_type,
+          created_at: created_at,
+          updated_at: updated_at
+        )
       end
     end
   end
diff --git a/ee/spec/lib/ee/api/entities/related_epic_link_spec.rb b/ee/spec/lib/ee/api/entities/related_epic_link_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bac054dd592d7675a4a7057890052dbac8758063
--- /dev/null
+++ b/ee/spec/lib/ee/api/entities/related_epic_link_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::API::Entities::RelatedEpicLink, feature_category: :team_planning do
+  subject(:entity) { described_class.new(object).as_json }
+
+  let_it_be(:source_epic) { create(:epic) }
+  let_it_be(:target_epic) { create(:epic) }
+
+  let_it_be(:related_work_item_link) do
+    create(:work_item_link, id: 999, source: source_epic.work_item, target: target_epic.work_item)
+  end
+
+  let_it_be(:related_epic_link) do
+    # We want to ensure the `related_epic_link.id` gets used, so we set the id to a static value
+    create(:related_epic_link, id: 100, source: source_epic, target: target_epic,
+      related_work_item_link: related_work_item_link)
+  end
+
+  shared_examples 'exposes data correctly' do
+    it 'uses the data from the related epic link', :aggregate_failures do
+      expect(entity.keys).to contain_exactly(:id, :source_epic, :target_epic, :link_type, :created_at, :updated_at)
+
+      expect(entity[:id]).to eq(100)
+      expect(entity[:source_epic][:id]).to eq(source_epic.id)
+      expect(entity[:target_epic][:id]).to eq(target_epic.id)
+      expect(entity[:link_type]).to eq('relates_to')
+
+      expect(entity[:created_at]).to eq(object.created_at)
+      expect(entity[:updated_at]).to eq(object.updated_at)
+    end
+  end
+
+  context 'when related epic link' do
+    let(:object) { related_epic_link }
+
+    it_behaves_like 'exposes data correctly'
+  end
+
+  context 'when related work_item link' do
+    let(:object) { related_work_item_link }
+
+    it_behaves_like 'exposes data correctly'
+  end
+end
diff --git a/ee/spec/models/ee/work_items/related_work_item_link_spec.rb b/ee/spec/models/ee/work_items/related_work_item_link_spec.rb
index 4e9aed5cd74a806940cefe410d046284cc660d35..bd370e5a7a6eeae457252c0da37787534c9b0100 100644
--- a/ee/spec/models/ee/work_items/related_work_item_link_spec.rb
+++ b/ee/spec/models/ee/work_items/related_work_item_link_spec.rb
@@ -16,6 +16,40 @@
     end
   end
 
+  describe 'scopes' do
+    let(:epic_type) { ::WorkItems::Type.default_by_type(:epic) }
+
+    let_it_be(:epic_issue_link) do
+      create(:work_item_link, source: create(:work_item, :epic), target: create(:work_item, :issue))
+    end
+
+    let_it_be(:epic_epic_link) do
+      create(:work_item_link, source: create(:work_item, :epic), target: create(:work_item, :epic))
+    end
+
+    let_it_be(:issue_epic_link) do
+      create(:work_item_link, source: create(:work_item, :issue), target: create(:work_item, :epic))
+    end
+
+    context 'when filtered by source type' do
+      it 'returns only links with the given type on the source' do
+        expect(described_class.for_source_type(epic_type)).to contain_exactly(epic_issue_link, epic_epic_link)
+      end
+    end
+
+    context 'when filtered by target type' do
+      it 'returns only links with the given type on the target' do
+        expect(described_class.for_target_type(epic_type)).to contain_exactly(issue_epic_link, epic_epic_link)
+      end
+    end
+
+    context 'when combining for_target_type and for_source_type' do
+      it 'returns only links with the given type on the source and target' do
+        expect(described_class.for_source_type(epic_type).for_target_type(epic_type)).to contain_exactly(epic_epic_link)
+      end
+    end
+  end
+
   describe 'validations' do
     describe '#validate_related_link_restrictions' do
       using RSpec::Parameterized::TableSyntax
diff --git a/ee/spec/requests/api/related_epic_links_spec.rb b/ee/spec/requests/api/related_epic_links_spec.rb
index 969e43dd01a34e67181f362621f2e728ba4515cb..37786415b24ce771d1034b874eb3e66810cd72ec 100644
--- a/ee/spec/requests/api/related_epic_links_spec.rb
+++ b/ee/spec/requests/api/related_epic_links_spec.rb
@@ -5,427 +5,445 @@
 RSpec.describe API::RelatedEpicLinks, feature_category: :portfolio_management do
   include ExternalAuthorizationServiceHelpers
 
-  let_it_be(:user) { create(:user) }
-  let_it_be(:group) { create(:group, :private) }
-  let_it_be(:epic) { create(:epic, group: group) }
-  let_it_be(:source_group) { create(:group, :public) }
-  let_it_be(:target_group) { create(:group, :public) }
-  let_it_be(:source_epic) { create(:epic, group: source_group) }
-  let_it_be(:target_epic) { create(:epic, group: target_group) }
-
-  before do
-    stub_licensed_features(epics: true, related_epics: true)
-  end
+  shared_examples 'related epics API' do
+    let_it_be(:user) { create(:user) }
+    let_it_be(:group) { create(:group, :private) }
+    let_it_be(:epic) { create(:epic, group: group) }
+    let_it_be(:source_group) { create(:group, :public) }
+    let_it_be(:target_group) { create(:group, :public) }
+    let_it_be(:source_epic) { create(:epic, group: source_group) }
+    let_it_be(:target_epic) { create(:epic, group: target_group) }
+
+    before do
+      stub_licensed_features(epics: true, related_epics: true)
+    end
 
-  shared_examples 'forbidden resource' do |message|
-    it 'returns 403' do
-      subject
+    shared_examples 'forbidden resource' do |message|
+      it 'returns 403' do
+        subject
 
-      expect(response).to have_gitlab_http_status(:forbidden)
+        expect(response).to have_gitlab_http_status(:forbidden)
+      end
     end
-  end
 
-  shared_examples 'not found resource' do |message|
-    it 'returns 404' do
-      subject
+    shared_examples 'not found resource' do |message|
+      it 'returns 404' do
+        subject
 
-      expect(response).to have_gitlab_http_status(:not_found)
-      expect(json_response['message']).to eq(message)
+        expect(response).to have_gitlab_http_status(:not_found)
+        expect(json_response['message']).to eq(message)
+      end
     end
-  end
 
-  shared_examples 'unauthenticated resource' do
-    it 'returns 401' do
-      perform_request
+    shared_examples 'unauthenticated resource' do
+      it 'returns 401' do
+        perform_request
 
-      expect(response).to have_gitlab_http_status(:unauthorized)
+        expect(response).to have_gitlab_http_status(:unauthorized)
+      end
     end
-  end
 
-  shared_examples 'successful response' do |status|
-    it "returns #{status}" do
-      subject
+    shared_examples 'successful response' do |status|
+      it "returns #{status}" do
+        subject
 
-      expect_link_response(status: status)
+        expect_link_response(status: status)
+      end
     end
-  end
 
-  shared_examples 'endpoint with features check' do
-    context 'when epics feature is not available' do
-      before do
-        stub_licensed_features(epics: false, related_epics: true)
+    shared_examples 'endpoint with features check' do
+      context 'when epics feature is not available' do
+        before do
+          stub_licensed_features(epics: false, related_epics: true)
+        end
+
+        it { is_expected.to eq(403) }
       end
 
-      it { is_expected.to eq(403) }
-    end
+      context 'when related_epics feature is not available ' do
+        before do
+          stub_licensed_features(epics: true, related_epics: false)
+        end
 
-    context 'when related_epics feature is not available ' do
-      before do
-        stub_licensed_features(epics: true, related_epics: false)
+        it { is_expected.to eq(403) }
       end
-
-      it { is_expected.to eq(403) }
     end
-  end
 
-  describe 'GET /groups/:id/related_epic_links' do
-    let_it_be(:created_at) { Date.new(2021, 10, 14) }
-    let_it_be(:updated_at) { Date.new(2021, 10, 14) }
-    let_it_be(:group_2) { create(:group, :private) }
-
-    let_it_be(:related_epic_link_1) do
-      create(
-        :related_epic_link,
-        source: epic,
-        target: create(:epic, group: group),
-        created_at: created_at,
-        updated_at: updated_at
-      )
-    end
+    describe 'GET /groups/:id/related_epic_links' do
+      let_it_be(:created_at) { Date.new(2021, 10, 14) }
+      let_it_be(:updated_at) { Date.new(2021, 10, 14) }
+      let_it_be(:group_2) { create(:group, :private) }
+
+      let_it_be(:related_epic_link_1) do
+        create(
+          :related_epic_link,
+          source: epic,
+          target: create(:epic, group: group),
+          created_at: created_at,
+          updated_at: updated_at
+        )
+      end
 
-    let_it_be(:related_epic_link_2) do
-      create(
-        :related_epic_link,
-        source: epic,
-        target: create(:epic, group: group_2),
-        created_at: created_at,
-        updated_at: updated_at
-      )
-    end
+      let_it_be(:related_epic_link_2) do
+        create(
+          :related_epic_link,
+          source: epic,
+          target: create(:epic, group: group_2),
+          created_at: created_at,
+          updated_at: updated_at
+        )
+      end
 
-    def perform_request(user = nil, params = {})
-      get api("/groups/#{group.id}/related_epic_links", user), params: params
-    end
+      def perform_request(user = nil, params = {})
+        get api("/groups/#{group.id}/related_epic_links", user), params: params
+      end
 
-    subject { perform_request(user) }
+      subject { perform_request(user) }
 
-    context 'when user has no access to the group' do
-      it 'returns 404' do
-        perform_request
+      context 'when user has no access to the group' do
+        it 'returns 404' do
+          perform_request
 
-        expect(response).to have_gitlab_http_status(:not_found)
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
-    end
 
-    context 'when user has access to the group' do
-      before do
-        group.add_guest(user)
-      end
+      context 'when user has access to the group' do
+        before do
+          group.add_guest(user)
+        end
 
-      it_behaves_like 'endpoint with features check'
+        it_behaves_like 'endpoint with features check'
 
-      it 'returns only related epics links the user has access to' do
-        perform_request(user)
+        it 'returns only related epics links the user has access to' do
+          perform_request(user)
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response).to be_an Array
-        expect(json_response.length).to eq(1)
-        expect(json_response[0]['source_epic']['id']).to eq(related_epic_link_1.source.id)
-        expect(json_response[0]['target_epic']['id']).to eq(related_epic_link_1.target.id)
-        expect(response).to match_response_schema('public_api/v4/related_epic_links', dir: 'ee')
-      end
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response).to be_an Array
+          expect(json_response.length).to eq(1)
+          expect(json_response[0]['source_epic']['id']).to eq(related_epic_link_1.source.id)
+          expect(json_response[0]['target_epic']['id']).to eq(related_epic_link_1.target.id)
+          expect(response).to match_response_schema('public_api/v4/related_epic_links', dir: 'ee')
+        end
 
-      context 'when filtered by updated_before' do
-        it 'returns related epic links updated before the given parameter' do
-          perform_request(user, { updated_before: '2021-10-15=T00:00:00.000Z' })
+        context 'when filtered by updated_before' do
+          it 'returns related epic links updated before the given parameter' do
+            perform_request(user, { updated_before: '2021-10-15=T00:00:00.000Z' })
 
-          expect(json_response[0]['id']).to eq(related_epic_link_1.id)
-        end
+            expect(json_response[0]['id']).to eq(related_epic_link_1.id)
+          end
 
-        it 'returns no related epic links' do
-          perform_request(user, { updated_before: '2021-10-13=T00:00:00.000Z' })
+          it 'returns no related epic links' do
+            perform_request(user, { updated_before: '2021-10-13=T00:00:00.000Z' })
 
-          expect(json_response.length).to eq(0)
+            expect(json_response.length).to eq(0)
+          end
         end
-      end
 
-      context 'when filtered by updated_after' do
-        it 'returns related epic links updated before the given parameter' do
-          perform_request(user, { updated_after: '2021-10-14=T00:00:00.000Z' })
+        context 'when filtered by updated_after' do
+          it 'returns related epic links updated before the given parameter' do
+            perform_request(user, { updated_after: '2021-10-14=T00:00:00.000Z' })
 
-          expect(json_response[0]['id']).to eq(related_epic_link_1.id)
-        end
+            expect(json_response[0]['id']).to eq(related_epic_link_1.id)
+          end
 
-        it 'returns no related epic links' do
-          perform_request(user, { updated_after: '2021-10-15=T00:00:00.000Z' })
+          it 'returns no related epic links' do
+            perform_request(user, { updated_after: '2021-10-15=T00:00:00.000Z' })
 
-          expect(json_response.length).to eq(0)
+            expect(json_response.length).to eq(0)
+          end
         end
-      end
 
-      context 'when filtered by created_after' do
-        it 'returns related epic links created after the given parameter' do
-          perform_request(user, { created_after: '2021-10-14=T00:00:00.000Z' })
+        context 'when filtered by created_after' do
+          it 'returns related epic links created after the given parameter' do
+            perform_request(user, { created_after: '2021-10-14=T00:00:00.000Z' })
 
-          expect(json_response[0]['id']).to eq(related_epic_link_1.id)
-        end
+            expect(json_response[0]['id']).to eq(related_epic_link_1.id)
+          end
 
-        it 'returns no related epic links' do
-          perform_request(user, { created_after: '2021-10-15=T00:00:00.000Z' })
+          it 'returns no related epic links' do
+            perform_request(user, { created_after: '2021-10-15=T00:00:00.000Z' })
 
-          expect(json_response.length).to eq(0)
+            expect(json_response.length).to eq(0)
+          end
         end
-      end
 
-      context 'when filtered by created_before' do
-        it 'returns related epic links created before the given parameter' do
-          perform_request(user, { created_before: '2021-10-15=T00:00:00.000Z' })
+        context 'when filtered by created_before' do
+          it 'returns related epic links created before the given parameter' do
+            perform_request(user, { created_before: '2021-10-15=T00:00:00.000Z' })
 
-          expect(json_response[0]['id']).to eq(related_epic_link_1.id)
+            expect(json_response[0]['id']).to eq(related_epic_link_1.id)
+          end
+
+          it 'returns no related epic links' do
+            perform_request(user, { created_before: '2021-10-13=T00:00:00.000Z' })
+
+            expect(json_response.length).to eq(0)
+          end
         end
 
-        it 'returns no related epic links' do
-          perform_request(user, { created_before: '2021-10-13=T00:00:00.000Z' })
+        context 'when epics links are in a sub-group' do
+          let_it_be(:sub_group) { create(:group, :private, parent: group) }
+          let_it_be(:related_sub_epic_link) { create(:related_epic_link, source: create(:epic, group: sub_group), target: create(:epic, group: sub_group)) }
 
-          expect(json_response.length).to eq(0)
+          it 'returns linked epic from sub-group' do
+            perform_request(user)
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response).to be_an Array
+            expect(json_response.length).to eq(2)
+          end
         end
       end
 
-      context 'when epics links are in a sub-group' do
-        let_it_be(:sub_group) { create(:group, :private, parent: group) }
-        let_it_be(:related_sub_epic_link) { create(:related_epic_link, source: create(:epic, group: sub_group), target: create(:epic, group: sub_group)) }
+      context 'when user has access to both groups' do
+        before do
+          group.add_guest(user)
+          group_2.add_guest(user)
+        end
 
-        it 'returns linked epic from sub-group' do
+        it 'returns related epic links' do
           perform_request(user)
 
           expect(response).to have_gitlab_http_status(:ok)
           expect(json_response).to be_an Array
           expect(json_response.length).to eq(2)
+          expect(response).to match_response_schema('public_api/v4/related_epic_links', dir: 'ee')
         end
-      end
-    end
-
-    context 'when user has access to both groups' do
-      before do
-        group.add_guest(user)
-        group_2.add_guest(user)
-      end
-
-      it 'returns related epic links' do
-        perform_request(user)
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response).to be_an Array
-        expect(json_response.length).to eq(2)
-        expect(response).to match_response_schema('public_api/v4/related_epic_links', dir: 'ee')
-      end
-
-      it 'returns multiple links without N + 1' do
-        perform_request(user)
+        it 'returns multiple links without N + 1' do
+          perform_request(user)
 
-        control = ActiveRecord::QueryRecorder.new(skip_cached: false) { perform_request(user) }
+          control = ActiveRecord::QueryRecorder.new(skip_cached: false) { perform_request(user) }
 
-        create(:related_epic_link, source: epic, target: create(:epic, group: group))
+          create(:related_epic_link, source: epic, target: create(:epic, group: group))
 
-        expect { perform_request(user) }.not_to exceed_query_limit(control)
-        expect(response).to have_gitlab_http_status(:ok)
+          expect { perform_request(user) }.not_to exceed_query_limit(control)
+          expect(response).to have_gitlab_http_status(:ok)
+        end
       end
-    end
 
-    context 'with pagination' do
-      let_it_be(:target_epic) { create(:epic, group: group) }
-      let_it_be(:related_epic_link_3) { create(:related_epic_link, source: epic, target: target_epic) }
+      context 'with pagination' do
+        let_it_be(:target_epic) { create(:epic, group: group) }
+        let_it_be(:related_epic_link_3) { create(:related_epic_link, source: epic, target: target_epic) }
 
-      before do
-        group.add_guest(user)
-        group_2.add_guest(user)
-      end
+        before do
+          group.add_guest(user)
+          group_2.add_guest(user)
+        end
 
-      it 'returns first page of related epics' do
-        perform_request(user, { per_page: 2, page: 1 })
+        it 'returns first page of related epics' do
+          perform_request(user, { per_page: 2, page: 1 })
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response).to be_an Array
-        expect(json_response.length).to eq(2)
-        expect(json_response.pluck("id")).to match_array([related_epic_link_1.id, related_epic_link_2.id])
-      end
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response).to be_an Array
+          expect(json_response.length).to eq(2)
+          expect(json_response.pluck("id")).to match_array([related_epic_link_1.id, related_epic_link_2.id])
+        end
 
-      it 'returns the last page of related epics' do
-        perform_request(user, { per_page: 2, page: 2 })
+        it 'returns the last page of related epics' do
+          perform_request(user, { per_page: 2, page: 2 })
 
-        expect(json_response.length).to eq(1)
-        expect(json_response.pluck("id")).to match_array([related_epic_link_3.id])
+          expect(json_response.length).to eq(1)
+          expect(json_response.pluck("id")).to match_array([related_epic_link_3.id])
+        end
       end
     end
-  end
 
-  describe 'GET /groups/:id/epics/:epic_id/related_epics' do
-    def perform_request(user = nil, params = {})
-      get api("/groups/#{group.id}/epics/#{epic.iid}/related_epics", user), params: params
-    end
+    describe 'GET /groups/:id/epics/:epic_id/related_epics' do
+      def perform_request(user = nil, params = {})
+        get api("/groups/#{group.id}/epics/#{epic.iid}/related_epics", user), params: params
+      end
 
-    subject { perform_request(user) }
+      subject { perform_request(user) }
 
-    context 'when user cannot read epics' do
-      it 'returns 404' do
-        perform_request
+      context 'when user cannot read epics' do
+        it 'returns 404' do
+          perform_request
 
-        expect(response).to have_gitlab_http_status(:not_found)
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
-    end
 
-    context 'when user can read epics' do
-      let_it_be(:group_2) { create(:group) }
-      let_it_be(:related_epic_link_1) { create(:related_epic_link, source: epic, target: create(:epic, group: group)) }
-      let_it_be(:related_epic_link_2) { create(:related_epic_link, source: epic, target: create(:epic, group: group_2)) }
+      context 'when user can read epics' do
+        let_it_be(:group_2) { create(:group) }
+        let_it_be(:related_epic_link_1) { create(:related_epic_link, source: epic, target: create(:epic, group: group)) }
+        let_it_be(:related_epic_link_2) { create(:related_epic_link, source: epic, target: create(:epic, group: group_2)) }
 
-      before do
-        group.add_guest(user)
-      end
+        before do
+          group.add_guest(user)
+        end
 
-      it_behaves_like 'endpoint with features check'
+        it_behaves_like 'endpoint with features check'
 
-      it 'returns related epics' do
-        perform_request(user)
+        it 'returns related epics' do
+          perform_request(user)
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response).to be_an Array
-        expect(json_response.length).to eq(2)
-        expect(response).to match_response_schema('public_api/v4/related_epics', dir: 'ee')
-      end
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response).to be_an Array
+          expect(json_response.length).to eq(2)
+          expect(response).to match_response_schema('public_api/v4/related_epics', dir: 'ee')
+        end
 
-      it 'returns multiple links without N + 1' do
-        perform_request(user)
+        it 'returns multiple links without N + 1' do
+          perform_request(user)
 
-        control = ActiveRecord::QueryRecorder.new(skip_cached: false) { perform_request(user) }
+          control = ActiveRecord::QueryRecorder.new(skip_cached: false) { perform_request(user) }
 
-        create(:related_epic_link, source: epic, target: create(:epic, group: group))
+          create(:related_epic_link, source: epic, target: create(:epic, group: group))
 
-        expect { perform_request(user) }.not_to exceed_query_limit(control)
-        expect(response).to have_gitlab_http_status(:ok)
+          expect { perform_request(user) }.not_to exceed_query_limit(control)
+          expect(response).to have_gitlab_http_status(:ok)
+        end
       end
     end
-  end
 
-  describe 'POST /groups/:id/epics/:epic_id/related_epics' do
-    let(:target_epic_iid) { target_epic.iid }
+    describe 'POST /groups/:id/epics/:epic_id/related_epics' do
+      let(:target_epic_iid) { target_epic.iid }
 
-    subject { perform_request(user, target_group_id: target_group.id, target_epic_iid: target_epic_iid) }
+      subject { perform_request(user, target_group_id: target_group.id, target_epic_iid: target_epic_iid) }
 
-    def perform_request(user = nil, params = {})
-      post api("/groups/#{source_group.id}/epics/#{source_epic.iid}/related_epics", user), params: params
-    end
+      def perform_request(user = nil, params = {})
+        post api("/groups/#{source_group.id}/epics/#{source_epic.iid}/related_epics", user), params: params
+      end
 
-    it_behaves_like 'unauthenticated resource'
+      it_behaves_like 'unauthenticated resource'
 
-    context 'when user can not access source epic' do
-      # user is not a member of the public source group
-      it_behaves_like 'forbidden resource'
-    end
-
-    context 'when user can access source epic' do
-      before do
-        source_group.add_guest(user)
+      context 'when user can not access source epic' do
+        # user is not a member of the public source group
+        it_behaves_like 'forbidden resource'
       end
 
-      context 'when user cannot access target epic' do
-        context 'when group is private' do
-          let(:target_group) { group }
-
-          # user is not a member of the private target group
-          it_behaves_like 'not found resource', '404 Group Not Found'
+      context 'when user can access source epic' do
+        before do
+          source_group.add_guest(user)
         end
 
-        context 'when epic_relations_for_non_members is disabled' do
-          before do
-            stub_feature_flags(epic_relations_for_non_members: false)
+        context 'when user cannot access target epic' do
+          context 'when group is private' do
+            let(:target_group) { group }
+
+            # user is not a member of the private target group
+            it_behaves_like 'not found resource', '404 Group Not Found'
           end
 
-          # user is not a member of the public target group
-          it_behaves_like 'forbidden resource'
-        end
-      end
+          context 'when epic_relations_for_non_members is disabled' do
+            before do
+              stub_feature_flags(epic_relations_for_non_members: false)
+            end
 
-      context 'when user can access target epic group' do
-        before do
-          target_group.add_guest(user)
+            # user is not a member of the public target group
+            it_behaves_like 'forbidden resource'
+          end
         end
 
-        it_behaves_like 'successful response', :created
-        it_behaves_like 'endpoint with features check'
+        context 'when user can access target epic group' do
+          before do
+            target_group.add_guest(user)
+          end
 
-        it 'returns 201 when sending full path of target group' do
-          perform_request(user, target_group_id: target_group.full_path, target_epic_iid: target_epic.iid, link_type: 'blocks')
+          it_behaves_like 'successful response', :created
+          it_behaves_like 'endpoint with features check'
 
-          expect_link_response(link_type: 'blocks')
-          expect(json_response['source_epic']['id']).to eq(source_epic.id)
-          expect(json_response['target_epic']['id']).to eq(target_epic.id)
-        end
+          it 'returns 201 when sending full path of target group' do
+            perform_request(user, target_group_id: target_group.full_path, target_epic_iid: target_epic.iid, link_type: 'blocks')
 
-        it 'returns 201 status for is_blocked_by link and contains the expected link response' do
-          perform_request(user, target_group_id: target_group.full_path, target_epic_iid: target_epic.iid, link_type: 'is_blocked_by')
+            expect_link_response(link_type: 'blocks')
+            expect(json_response['source_epic']['id']).to eq(source_epic.id)
+            expect(json_response['target_epic']['id']).to eq(target_epic.id)
+          end
 
-          # For `is_blocked_by` we swap the source and target and use `block` as type.
-          expect_link_response(link_type: 'blocks')
-          expect(json_response['source_epic']['id']).to eq(target_epic.id)
-          expect(json_response['target_epic']['id']).to eq(source_epic.id)
-        end
+          it 'returns 201 status for is_blocked_by link and contains the expected link response' do
+            perform_request(user, target_group_id: target_group.full_path, target_epic_iid: target_epic.iid, link_type: 'is_blocked_by')
 
-        context 'when target epic is confidential' do
-          let(:target_epic) { create(:epic, :confidential, group: target_group) }
+            # For `is_blocked_by` we swap the source and target and use `block` as type.
+            expect_link_response(link_type: 'blocks')
+            expect(json_response['source_epic']['id']).to eq(target_epic.id)
+            expect(json_response['target_epic']['id']).to eq(source_epic.id)
+          end
 
-          it_behaves_like 'forbidden resource'
-        end
+          context 'when target epic is confidential' do
+            let(:target_epic) { create(:epic, :confidential, group: target_group) }
 
-        context 'when target epic is not found' do
-          let(:target_epic_iid) { non_existing_record_iid }
+            it_behaves_like 'forbidden resource'
+          end
 
-          it_behaves_like 'not found resource', '404 Not found'
+          context 'when target epic is not found' do
+            let(:target_epic_iid) { non_existing_record_iid }
+
+            it_behaves_like 'not found resource', '404 Not found'
+          end
         end
       end
     end
-  end
 
-  describe 'DELETE /groups/:id/epics/:epic_id/related_epics' do
-    let_it_be(:related_epic_link) { create(:related_epic_link, source: source_epic, target: target_epic) }
+    describe 'DELETE /groups/:id/epics/:epic_id/related_epics' do
+      let_it_be(:related_epic_link) { create(:related_epic_link, source: source_epic, target: target_epic) }
 
-    subject { perform_request(user) }
+      subject { perform_request(user) }
 
-    def perform_request(user = nil, link_id = related_epic_link.id)
-      delete api("/groups/#{source_group.id}/epics/#{source_epic.iid}/related_epics/#{link_id}", user)
-    end
+      def perform_request(user = nil, link_id = related_epic_link.id)
+        delete api("/groups/#{source_group.id}/epics/#{source_epic.iid}/related_epics/#{link_id}", user)
+      end
 
-    it_behaves_like 'unauthenticated resource'
+      it_behaves_like 'unauthenticated resource'
 
-    context 'when user can not access source epic' do
-      it_behaves_like 'forbidden resource'
-    end
-
-    context 'when user can access source epic' do
-      before do
-        source_group.add_guest(user)
-        target_group.add_guest(user)
+      context 'when user can not access source epic' do
+        it_behaves_like 'forbidden resource'
       end
 
-      context 'when target group is private' do
-        let(:related_epic_link) do
-          create(:related_epic_link, source: source_epic, target: create(:epic, group: group))
+      context 'when user can access source epic' do
+        before do
+          source_group.add_guest(user)
+          target_group.add_guest(user)
         end
 
-        it_behaves_like 'not found resource', 'No Related Epic Link found'
-      end
+        context 'when target group is private' do
+          let(:related_epic_link) do
+            create(:related_epic_link, source: source_epic, target: create(:epic, group: group))
+          end
 
-      context 'when user can access target group' do
-        it_behaves_like 'successful response', :ok
-        it_behaves_like 'endpoint with features check'
+          it_behaves_like 'not found resource', 'No Related Epic Link found'
+        end
 
-        context 'when related_epic_link_id belongs to a different epic' do
-          let_it_be(:other_epic) { create(:epic, group: target_group) }
-          let_it_be(:other_epic_link) { create(:related_epic_link, source: other_epic, target: target_epic) }
+        context 'when user can access target group' do
+          it_behaves_like 'successful response', :ok
+          it_behaves_like 'endpoint with features check'
 
-          subject do
-            perform_request(user, other_epic_link.id)
-          end
+          context 'when related_epic_link_id belongs to a different epic' do
+            let_it_be(:other_epic) { create(:epic, group: target_group) }
+            let_it_be(:other_epic_link) { create(:related_epic_link, source: other_epic, target: target_epic) }
+
+            subject do
+              perform_request(user, other_epic_link.id)
+            end
 
-          it_behaves_like 'not found resource', '404 Not found'
+            it_behaves_like 'not found resource', '404 Not found'
+          end
         end
       end
     end
+
+    def expect_link_response(link_type: 'relates_to', status: :created)
+      expect(response).to have_gitlab_http_status(status)
+      expect(response).to match_response_schema('public_api/v4/related_epic_link')
+      expect(json_response['link_type']).to eq(link_type)
+    end
   end
 
-  def expect_link_response(link_type: 'relates_to', status: :created)
-    expect(response).to have_gitlab_http_status(status)
-    expect(response).to match_response_schema('public_api/v4/related_epic_link')
-    expect(json_response['link_type']).to eq(link_type)
+  context 'when related_epic_links_from_work_items is disabled' do
+    before do
+      stub_feature_flags(related_epic_links_from_work_items: false)
+    end
+
+    it_behaves_like 'related epics API'
+  end
+
+  context 'when related_epic_links_from_work_items is enabled' do
+    before do
+      stub_feature_flags(related_epic_links_from_work_items: true)
+    end
+
+    it_behaves_like 'related epics API'
   end
 end
diff --git a/ee/spec/services/work_items/legacy_epics/related_epic_links/list_service_spec.rb b/ee/spec/services/work_items/legacy_epics/related_epic_links/list_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c03f6b56ff156bd6adb973edcf857fdfd1abcdc3
--- /dev/null
+++ b/ee/spec/services/work_items/legacy_epics/related_epic_links/list_service_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::LegacyEpics::RelatedEpicLinks::ListService, feature_category: :team_planning do
+  let(:epics) { Epic.where(id: [epic1.id, epic2.id, epic3.id]) }
+
+  let_it_be(:group) { create(:group) }
+  let_it_be(:epic1) { create(:epic, group: group) }
+  let_it_be(:epic2) { create(:epic, group: group) }
+  let_it_be(:epic3) { create(:epic, group: group) }
+  let_it_be(:epic4) { create(:epic, group: group) }
+  let_it_be(:epic5) { create(:epic, group: group) }
+  let_it_be(:related_epic_link1) { create(:related_epic_link, source: epic1, target: epic2) }
+  let_it_be(:related_epic_link2) { create(:related_epic_link, source: epic3, target: epic4) }
+  let_it_be(:other_related_epic) { create(:related_epic_link, source: epic4, target: epic5) }
+  let_it_be(:other_work_item_link) do
+    create(:work_item_link, source: epic1.work_item, target: create(:work_item, :issue, namespace: group))
+  end
+
+  subject(:execute) { described_class.new(epics, group).execute }
+
+  describe '#execute' do
+    context 'when related_epic_links_from_work_items feature flag is enabled' do
+      before do
+        stub_feature_flags(related_epic_links_from_work_items: group)
+      end
+
+      it 'returns related work item links for epics' do
+        expect(execute).to contain_exactly(related_epic_link1.related_work_item_link,
+          related_epic_link2.related_work_item_link)
+        expect(execute.first.class).to eq(::WorkItems::RelatedWorkItemLink)
+      end
+    end
+
+    context 'when related_epic_links_from_work_items feature flag is disabled' do
+      before do
+        stub_feature_flags(related_epic_links_from_work_items: false)
+      end
+
+      it 'returns related epic links' do
+        expect(execute).to contain_exactly(related_epic_link1, related_epic_link2)
+        expect(execute.first.class).to eq(Epic::RelatedEpicLink)
+      end
+    end
+  end
+end