diff --git a/app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb
index 7f82c3dbadad60d218049f699bf3c41646d22484..b02061a091badf082cdf72cb86187e1ed48ba8ed 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/stages_resolver.rb
@@ -6,8 +6,10 @@ module CycleAnalytics
       class StagesResolver < BaseResolver
         type [Types::Analytics::CycleAnalytics::ValueStreams::StageType], null: true
 
-        def resolve
-          list_stages({ value_stream: object })
+        argument :id, ID, required: false, description: 'Value stream stage id.'
+
+        def resolve(id: nil)
+          list_stages(stage_params(id: id).merge(value_stream: object))
         end
 
         private
@@ -23,6 +25,12 @@ def list_stages(list_service_params)
         def namespace
           object.project.project_namespace
         end
+
+        def stage_params(id: nil)
+          list_params = {}
+          list_params[:stage_ids] = [::GitlabSchema.parse_gid(id).model_id] if id
+          list_params
+        end
       end
     end
   end
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb
index f3e3da86169676813f57438feb89ac22eef51697..3b71b575c483203c01636975971ebabcee4cae60 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/value_streams_resolver.rb
@@ -6,11 +6,20 @@ module CycleAnalytics
       class ValueStreamsResolver < BaseResolver
         type Types::Analytics::CycleAnalytics::ValueStreamType.connection_type, null: true
 
-        def resolve
-          # FOSS only have default value stream available
-          [
-            ::Analytics::CycleAnalytics::ValueStream.build_default_value_stream(object.project_namespace)
-          ]
+        argument :id, ID, required: false, description: 'Value stream id.'
+
+        # ignore id in FOSS
+        def resolve(id: nil)
+          ::Analytics::CycleAnalytics::ValueStreams::ListService
+            .new(**service_params(id: id))
+            .execute
+            .payload[:value_streams]
+        end
+
+        private
+
+        def service_params(*)
+          { parent: object.project_namespace, current_user: current_user, params: {} }
         end
       end
     end
diff --git a/app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb b/app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb
index c8fdf8513be32110d27973c3296f6901c0e477ef..a30927c8566a16038573538adf982f52b119af37 100644
--- a/app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb
+++ b/app/graphql/types/analytics/cycle_analytics/value_streams/stage_type.rb
@@ -8,6 +8,11 @@ module ValueStreams
         class StageType < BaseObject
           graphql_name 'ValueStreamStage'
 
+          field :id,
+            type: ::Types::GlobalIDType[::Analytics::CycleAnalytics::Stage],
+            null: false,
+            description: "ID of the value stream."
+
           field :name,
             GraphQL::Types::String,
             null: false,
@@ -33,6 +38,16 @@ class StageType < BaseObject
             null: false,
             description: 'End event identifier.'
 
+          field :start_event_html_description,
+            GraphQL::Types::String,
+            null: false,
+            description: 'HTML description of the start event.'
+
+          field :end_event_html_description,
+            GraphQL::Types::String,
+            null: false,
+            description: 'HTML description of the end event.'
+
           def start_event_identifier
             events_enum[object.start_event_identifier]
           end
@@ -41,9 +56,21 @@ def end_event_identifier
             events_enum[object.end_event_identifier]
           end
 
+          def start_event_html_description
+            stage_entity.start_event_html_description
+          end
+
+          def end_event_html_description
+            stage_entity.end_event_html_description
+          end
+
           def events_enum
             Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum.with_indifferent_access
           end
+
+          def stage_entity
+            @stage_entity ||= ::Analytics::CycleAnalytics::StageEntity.new(object)
+          end
         end
         # rubocop: enable Graphql/AuthorizeTypes
       end
diff --git a/app/models/analytics/cycle_analytics/stage.rb b/app/models/analytics/cycle_analytics/stage.rb
index 93aac0c732eb0b4ccdbd1e14bddcc906a5e2be2e..04c75b38210bb203d2601c097841cf6db0be772c 100644
--- a/app/models/analytics/cycle_analytics/stage.rb
+++ b/app/models/analytics/cycle_analytics/stage.rb
@@ -21,6 +21,13 @@ class Stage < ApplicationRecord
       alias_attribute :parent_id, :group_id
       alias_attribute :value_stream_id, :group_value_stream_id
 
+      def to_global_id
+        return super if persisted?
+
+        # Returns default name as the id for built in stages at the FOSS level
+        name
+      end
+
       def self.distinct_stages_within_hierarchy(namespace)
         # Looking up the whole hierarchy including all kinds (type) of Namespace records.
         # We're doing a custom traversal_ids query because:
diff --git a/app/services/analytics/cycle_analytics/stages/list_service.rb b/app/services/analytics/cycle_analytics/stages/list_service.rb
index 1cd7d3f5c6dd1c1eac25a39534ec41c3dbed27f9..3d8e99ab7f74d0884d153e94699d9e070397a46d 100644
--- a/app/services/analytics/cycle_analytics/stages/list_service.rb
+++ b/app/services/analytics/cycle_analytics/stages/list_service.rb
@@ -7,7 +7,10 @@ class ListService < Analytics::CycleAnalytics::Stages::BaseService
         def execute
           return forbidden unless allowed?
 
-          success(build_default_stages)
+          stages = build_default_stages
+          # In FOSS, stages are not persisted, we match them by name
+          stages = stages.select { |stage| params[:stage_ids].include?(stage.name) } if filter_by_stage_ids?
+          success(stages)
         end
 
         private
@@ -19,6 +22,10 @@ def allowed?
         def success(stages)
           ServiceResponse.success(payload: { stages: stages })
         end
+
+        def filter_by_stage_ids?
+          params[:stage_ids].present?
+        end
       end
     end
   end
diff --git a/app/services/analytics/cycle_analytics/value_streams/list_service.rb b/app/services/analytics/cycle_analytics/value_streams/list_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..17b7728bd34308ab2126a3387ab2a3915b339ea0
--- /dev/null
+++ b/app/services/analytics/cycle_analytics/value_streams/list_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Analytics
+  module CycleAnalytics
+    module ValueStreams
+      class ListService
+        include Gitlab::Allowable
+
+        def initialize(parent:, current_user:, params: {})
+          @parent = parent
+          @current_user = current_user
+          @params = params
+        end
+
+        def execute
+          return forbidden unless can?(current_user, :read_cycle_analytics, parent.project)
+
+          value_stream = ::Analytics::CycleAnalytics::ValueStream
+            .build_default_value_stream(parent)
+
+          success([value_stream])
+        end
+
+        private
+
+        attr_reader :parent, :current_user, :params
+
+        def success(value_streams)
+          ServiceResponse.success(payload: { value_streams: value_streams })
+        end
+
+        def forbidden
+          ServiceResponse.error(message: 'Forbidden', payload: {})
+        end
+      end
+    end
+  end
+end
+
+Analytics::CycleAnalytics::ValueStreams::ListService.prepend_mod
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9366fb07cb6b00f6d70a99a55795cc801771a656..c09bfcb7e517d6ad5b9b2d17f6b589f5694833e0 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -20957,7 +20957,6 @@ GPG signature for a signed commit.
 | <a id="grouptwofactorgraceperiod"></a>`twoFactorGracePeriod` | [`Int`](#int) | Time before two-factor authentication is enforced. |
 | <a id="groupuserpermissions"></a>`userPermissions` | [`GroupPermissions!`](#grouppermissions) | Permissions for the current user on the resource. |
 | <a id="groupvaluestreamanalytics"></a>`valueStreamAnalytics` | [`ValueStreamAnalytics`](#valuestreamanalytics) | Information about Value Stream Analytics within the group. |
-| <a id="groupvaluestreams"></a>`valueStreams` | [`ValueStreamConnection`](#valuestreamconnection) | Value streams available to the group. (see [Connections](#connections)) |
 | <a id="groupvisibility"></a>`visibility` | [`String`](#string) | Visibility of the namespace. |
 | <a id="groupvulnerabilityscanners"></a>`vulnerabilityScanners` | [`VulnerabilityScannerConnection`](#vulnerabilityscannerconnection) | Vulnerability scanners reported on the project vulnerabilities of the group and its subgroups. (see [Connections](#connections)) |
 | <a id="groupweburl"></a>`webUrl` | [`String!`](#string) | Web URL of the group. |
@@ -22026,6 +22025,22 @@ Returns [`ValueStreamDashboardCount`](#valuestreamdashboardcount).
 | <a id="groupvaluestreamdashboardusageoverviewidentifier"></a>`identifier` | [`ValueStreamDashboardMetric!`](#valuestreamdashboardmetric) | Type of counts to retrieve. |
 | <a id="groupvaluestreamdashboardusageoverviewtimeframe"></a>`timeframe` | [`Timeframe!`](#timeframe) | Counts recorded during this time frame, usually from beginning of the month until the end of the month (the system runs monthly aggregations). |
 
+##### `Group.valueStreams`
+
+Value streams available to the group.
+
+Returns [`ValueStreamConnection`](#valuestreamconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="groupvaluestreamsid"></a>`id` | [`ID`](#id) | Value stream id. |
+
 ##### `Group.vulnerabilities`
 
 Vulnerabilities reported on the projects in the group and its subgroups.
@@ -26509,7 +26524,6 @@ Represents generic policy violation information.
 | <a id="projectuseraccessauthorizedagents"></a>`userAccessAuthorizedAgents` | [`ClusterAgentAuthorizationUserAccessConnection`](#clusteragentauthorizationuseraccessconnection) | Authorized cluster agents for the project through user_access keyword. (see [Connections](#connections)) |
 | <a id="projectuserpermissions"></a>`userPermissions` | [`ProjectPermissions!`](#projectpermissions) | Permissions for the current user on the resource. |
 | <a id="projectvaluestreamanalytics"></a>`valueStreamAnalytics` | [`ValueStreamAnalytics`](#valuestreamanalytics) | Information about Value Stream Analytics within the project. |
-| <a id="projectvaluestreams"></a>`valueStreams` | [`ValueStreamConnection`](#valuestreamconnection) | Value streams available to the project. (see [Connections](#connections)) |
 | <a id="projectvisibility"></a>`visibility` | [`String`](#string) | Visibility of the project. |
 | <a id="projectvulnerabilityimages"></a>`vulnerabilityImages` | [`VulnerabilityContainerImageConnection`](#vulnerabilitycontainerimageconnection) | Container images reported on the project vulnerabilities. (see [Connections](#connections)) |
 | <a id="projectvulnerabilityscanners"></a>`vulnerabilityScanners` | [`VulnerabilityScannerConnection`](#vulnerabilityscannerconnection) | Vulnerability scanners reported on the project vulnerabilities. (see [Connections](#connections)) |
@@ -27987,6 +28001,22 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="projecttimelogsstarttime"></a>`startTime` | [`Time`](#time) | List timelogs within a time range where the logged time is equal to or after startTime. |
 | <a id="projecttimelogsusername"></a>`username` | [`String`](#string) | List timelogs for a user. |
 
+##### `Project.valueStreams`
+
+Value streams available to the project.
+
+Returns [`ValueStreamConnection`](#valuestreamconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectvaluestreamsid"></a>`id` | [`ID`](#id) | Value stream id. |
+
 ##### `Project.visibleForks`
 
 Visible forks of the project.
@@ -30491,7 +30521,20 @@ fields relate to interactions between the two entities.
 | <a id="valuestreamname"></a>`name` | [`String!`](#string) | Name of the value stream. |
 | <a id="valuestreamnamespace"></a>`namespace` | [`Namespace!`](#namespace) | Namespace the value stream belongs to. |
 | <a id="valuestreamproject"></a>`project` **{warning-solid}** | [`Project`](#project) | **Introduced** in GitLab 15.6. **Status**: Experiment. Project the value stream belongs to, returns empty if it belongs to a group. |
-| <a id="valuestreamstages"></a>`stages` | [`[ValueStreamStage!]`](#valuestreamstage) | Value Stream stages. |
+
+#### Fields with arguments
+
+##### `ValueStream.stages`
+
+Value Stream stages.
+
+Returns [`[ValueStreamStage!]`](#valuestreamstage).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="valuestreamstagesid"></a>`id` | [`ID`](#id) | Value stream stage id. |
 
 ### `ValueStreamAnalytics`
 
@@ -30543,10 +30586,13 @@ Represents a recorded measurement (object count) for the requested group.
 | Name | Type | Description |
 | ---- | ---- | ----------- |
 | <a id="valuestreamstagecustom"></a>`custom` | [`Boolean!`](#boolean) | Whether the stage is customized. |
+| <a id="valuestreamstageendeventhtmldescription"></a>`endEventHtmlDescription` | [`String!`](#string) | HTML description of the end event. |
 | <a id="valuestreamstageendeventidentifier"></a>`endEventIdentifier` | [`ValueStreamStageEvent!`](#valuestreamstageevent) | End event identifier. |
 | <a id="valuestreamstageendeventlabel"></a>`endEventLabel` | [`Label`](#label) | Label associated with end event. |
 | <a id="valuestreamstagehidden"></a>`hidden` | [`Boolean!`](#boolean) | Whether the stage is hidden. |
+| <a id="valuestreamstageid"></a>`id` | [`AnalyticsCycleAnalyticsStageID!`](#analyticscycleanalyticsstageid) | ID of the value stream. |
 | <a id="valuestreamstagename"></a>`name` | [`String!`](#string) | Name of the stage. |
+| <a id="valuestreamstagestarteventhtmldescription"></a>`startEventHtmlDescription` | [`String!`](#string) | HTML description of the start event. |
 | <a id="valuestreamstagestarteventidentifier"></a>`startEventIdentifier` | [`ValueStreamStageEvent!`](#valuestreamstageevent) | Start event identifier. |
 | <a id="valuestreamstagestarteventlabel"></a>`startEventLabel` | [`Label`](#label) | Label associated with start event. |
 
@@ -34762,6 +34808,12 @@ A `AlertManagementHttpIntegrationID` is a global ID. It is encoded as a string.
 
 An example `AlertManagementHttpIntegrationID` is: `"gid://gitlab/AlertManagement::HttpIntegration/1"`.
 
+### `AnalyticsCycleAnalyticsStageID`
+
+A `AnalyticsCycleAnalyticsStageID` is a global ID. It is encoded as a string.
+
+An example `AnalyticsCycleAnalyticsStageID` is: `"gid://gitlab/Analytics::CycleAnalytics::Stage/1"`.
+
 ### `AnalyticsCycleAnalyticsValueStreamID`
 
 A `AnalyticsCycleAnalyticsValueStreamID` is a global ID. It is encoded as a string.
diff --git a/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/stages_resolver.rb b/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/stages_resolver.rb
index c4cf416e64a639fbfe2b086268544839efd6dc4e..42564a37d2c873a06cd56664f62602f0e07505bb 100644
--- a/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/stages_resolver.rb
+++ b/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/stages_resolver.rb
@@ -8,16 +8,18 @@ module StagesResolver
           extend ::Gitlab::Utils::Override
 
           override :resolve
-          def resolve
+          def resolve(id: nil)
             return super unless ::Gitlab::Analytics::CycleAnalytics.licensed?(namespace)
 
-            BatchLoader::GraphQL.for(object.id).batch(key: object.class.name, cache: false) do |ids, loader, _|
-              stages = list_stages({ value_streams_ids: ids })
+            BatchLoader::GraphQL.for(object.id).batch(key: object.class.name,
+              cache: false) do |value_stream_ids, loader, _|
+              list_params = stage_params(id: id).merge(value_streams_ids: value_stream_ids)
+              stages = list_stages(list_params)
 
               grouped_stages = stages.present? ? stages.group_by(&:value_stream_id) : {}
 
-              ids.each do |id|
-                loader.call(id, grouped_stages[id] || [])
+              value_stream_ids.each do |value_stream_id|
+                loader.call(value_stream_id, grouped_stages[value_stream_id] || [])
               end
             end
           end
diff --git a/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/value_streams_resolver.rb b/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/value_streams_resolver.rb
index 1ed110bdeb97c0951535a28e3771d5d9c362f63d..f5d58b3f77b2bd797c01b7f952838fdbb4da2740 100644
--- a/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/value_streams_resolver.rb
+++ b/ee/app/graphql/ee/resolvers/analytics/cycle_analytics/value_streams_resolver.rb
@@ -8,18 +8,22 @@ module ValueStreamsResolver
           extend ::Gitlab::Utils::Override
 
           override :resolve
-          def resolve
-            unless ::Gitlab::Analytics::CycleAnalytics.licensed?(parent_namespace)
-              return if object.is_a?(Group) # Group value streams only exists on EE
+          def resolve(id: nil)
+            # FOSS VSA is not supported for groups
+            return if object.is_a?(Group) && !::Gitlab::Analytics::CycleAnalytics.licensed?(parent_namespace)
 
-              return super
-            end
-
-            parent_namespace.value_streams.preload_associated_models.order_by_name_asc
+            super
           end
 
           private
 
+          override :service_params
+          def service_params(id: nil)
+            params = { parent: parent_namespace, current_user: current_user, params: {} }
+            params[:params][:value_stream_ids] = [::GitlabSchema.parse_gid(id).model_id] if id
+            params
+          end
+
           def parent_namespace
             object.try(:project_namespace) || object
           end
diff --git a/ee/app/services/ee/analytics/cycle_analytics/stages/list_service.rb b/ee/app/services/ee/analytics/cycle_analytics/stages/list_service.rb
index 9f8e4a626c25bb28dc4b66096a5e81e4dd66d56d..018c87b8df9fd70da233d1c750a7910c4dc6dc2e 100644
--- a/ee/app/services/ee/analytics/cycle_analytics/stages/list_service.rb
+++ b/ee/app/services/ee/analytics/cycle_analytics/stages/list_service.rb
@@ -14,6 +14,7 @@ def execute
             stages = persisted_stages
             stages = filter_by_value_stream(stages)
             stages = filter_by_value_stream_ids(stages)
+            stages = filter_by_stage_ids(stages)
 
             success(stages.for_list)
           end
@@ -33,7 +34,13 @@ def filter_by_value_stream(stages)
           def filter_by_value_stream_ids(stages)
             return stages unless filter_by_value_stream_ids?
 
-            stages.by_value_streams_ids(params[:value_streams_ids])
+            stages.by_value_streams_ids(params[:value_stream_ids])
+          end
+
+          def filter_by_stage_ids(stages)
+            return stages unless filter_by_stage_ids?
+
+            stages.id_in(params[:stage_ids])
           end
 
           def filter_by_value_stream?
@@ -41,7 +48,7 @@ def filter_by_value_stream?
           end
 
           def filter_by_value_stream_ids?
-            params[:value_streams_ids].present?
+            params[:value_stream_ids].present?
           end
 
           def allowed?
diff --git a/ee/app/services/ee/analytics/cycle_analytics/value_streams/list_service.rb b/ee/app/services/ee/analytics/cycle_analytics/value_streams/list_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1c10794959048a695709513f7b099ce931b2145
--- /dev/null
+++ b/ee/app/services/ee/analytics/cycle_analytics/value_streams/list_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module EE
+  module Analytics
+    module CycleAnalytics
+      module ValueStreams
+        module ListService
+          extend ::Gitlab::Utils::Override
+
+          def execute
+            return forbidden unless ::Gitlab::Analytics::CycleAnalytics.allowed?(current_user, parent)
+            return super unless ::Gitlab::Analytics::CycleAnalytics.licensed?(parent)
+
+            scope = parent.value_streams
+            scope = filter_by_value_stream_ids(scope)
+            scope = scope.preload_associated_models.order_by_name_asc
+            success(scope)
+          end
+
+          private
+
+          def filter_by_value_stream_ids(scope)
+            return scope unless params[:value_stream_ids].present?
+
+            scope.id_in(params[:value_stream_ids])
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/api/graphql/analytics/cycle_analytics/value_streams_spec.rb b/ee/spec/requests/api/graphql/analytics/cycle_analytics/value_streams_spec.rb
index 1ff1ce03e2e4aa0981d9d3087d3cf5c7207f8ce0..c1c36babb2debba0cd356a796019ba5e0e2fb10f 100644
--- a/ee/spec/requests/api/graphql/analytics/cycle_analytics/value_streams_spec.rb
+++ b/ee/spec/requests/api/graphql/analytics/cycle_analytics/value_streams_spec.rb
@@ -7,15 +7,19 @@
 
   let_it_be(:current_user) { create(:user) }
 
+  let(:variables) { { fullPath: resource.full_path } }
+
   let(:query) do
     <<~QUERY
-      query($fullPath: ID!) {
+      query($fullPath: ID!, $valueStreamId: ID, $stageId: ID) {
         #{resource_type}(fullPath: $fullPath) {
-          valueStreams {
+          valueStreams(id: $valueStreamId) {
             nodes {
               name
-              stages {
+              stages(id: $stageId) {
                 name
+                endEventHtmlDescription
+                startEventHtmlDescription
                 startEventLabel {
                   title
                 }
@@ -60,16 +64,38 @@
         end
 
         it 'returns custom value streams' do
-          post_graphql(query, current_user: current_user, variables: { fullPath: resource.full_path })
+          post_graphql(query, current_user: current_user, variables: variables)
 
           expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes)).to have_attributes(size: 2)
           expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes, 0, :name)).to eq('Custom 1')
           expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes, 1, :name)).to eq('Custom 2')
         end
 
+        context 'when specifying the value stream id argument' do
+          let(:value_stream) { value_streams.last }
+          let(:variables) { { fullPath: resource.full_path, valueStreamId: value_stream.to_gid.to_s } }
+
+          before do
+            post_graphql(query, current_user: current_user, variables: variables)
+          end
+
+          it 'returns only one value stream' do
+            expect(graphql_data_at(resource_type.to_sym, :value_streams,
+              :nodes)).to match([hash_including('name' => 'Custom 2')])
+          end
+
+          context 'when value stream id outside of the group is given' do
+            let(:value_stream) { create(:cycle_analytics_value_stream, name: 'outside') }
+
+            it 'returns no data error' do
+              expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes)).to be_empty
+            end
+          end
+        end
+
         context 'when value stream has stages' do
           def perform_request
-            post_graphql(query, current_user: current_user, variables: { fullPath: resource.full_path })
+            post_graphql(query, current_user: current_user, variables: variables)
           end
 
           context 'with associated labels' do
@@ -93,6 +119,17 @@ def perform_request
               expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes, 0, :stages, 0, :end_event_label,
                 :title)).to eq('End Label')
             end
+
+            it 'renders the html descriptions' do
+              perform_request
+
+              expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes, 0, :stages, 0)).to match(
+                hash_including(
+                  'startEventHtmlDescription' => include("#{start_label.title}</span>"),
+                  'endEventHtmlDescription' => include("#{end_label.title}</span>")
+                )
+              )
+            end
           end
 
           it 'prevents n+1 queries' do
@@ -108,6 +145,44 @@ def perform_request
 
             expect { perform_request }.to issue_same_number_of_queries_as(control)
           end
+
+          context 'when specifying the stage id argument' do
+            let(:value_stream) { value_streams.first }
+            let!(:stage) { create(:cycle_analytics_stage, value_stream: value_stream, namespace: namespace) }
+
+            let(:variables) do
+              {
+                fullPath: resource.full_path,
+                valueStreamId: value_stream.to_gid.to_s,
+                stageId: stage.to_gid.to_s
+              }
+            end
+
+            before do
+              # should not show up in the results
+              create(:cycle_analytics_stage, value_stream: value_stream, namespace: namespace)
+            end
+
+            it 'returns the queried stage' do
+              perform_request
+
+              expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes, 0, :stages)).to match([
+                hash_including('name' => stage.name)
+              ])
+            end
+
+            context 'when passing bogus stage id' do
+              before do
+                variables[:stageId] = create(:cycle_analytics_stage).to_gid.to_s
+              end
+
+              it 'returns no stages' do
+                perform_request
+
+                expect(graphql_data_at(resource_type.to_sym, :value_streams, :nodes, 0, :stages)).to be_empty
+              end
+            end
+          end
         end
       end
     end
@@ -129,7 +204,7 @@ def perform_request
       end
 
       it 'returns default value stream' do
-        post_graphql(query, current_user: current_user, variables: { fullPath: resource.full_path })
+        post_graphql(query, current_user: current_user, variables: variables)
 
         expect(graphql_data_at(:project, :value_streams, :nodes, 0, :name)).to eq('default')
         expect(graphql_data_at(:project, :value_streams)).to have_attributes(size: 1)
@@ -149,7 +224,7 @@ def perform_request
 
     context 'when current user does not have permissions' do
       it 'does not return value streams' do
-        post_graphql(query, current_user: current_user, variables: { fullPath: resource.full_path })
+        post_graphql(query, current_user: current_user, variables: variables)
 
         expect(graphql_data_at(:group, :value_streams)).to be_nil
       end
diff --git a/ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
index 4e43df6fdb559c833a0ea8011ff1bd0e6f966409..b41d371b5bcf55a31fbd5ce58c190f2588f312ff 100644
--- a/ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
+++ b/ee/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
@@ -67,5 +67,19 @@
     it 'returns stages filtered by value streams' do
       expect(stages).to match_array([stage1, stage2])
     end
+
+    context 'when filtering stage ids' do
+      let(:stage_ids) { [stage1.id] }
+
+      subject { described_class.new(parent: group, current_user: user, params: { value_stream_ids: [value_stream.id], stage_ids: [stage1.id] }).execute.payload[:stages] }
+
+      it { is_expected.to eq([stage1]) }
+
+      context 'when stage id does not belong to the value stream' do
+        let(:stage_ids) { [stage1.id, stage2.id] }
+
+        it { is_expected.to eq([stage1]) }
+      end
+    end
   end
 end
diff --git a/ee/spec/services/analytics/cycle_analytics/value_streams/list_service_spec.rb b/ee/spec/services/analytics/cycle_analytics/value_streams/list_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b24cf40c1d9bc6e211832ac7f1db3c3388ed5cd8
--- /dev/null
+++ b/ee/spec/services/analytics/cycle_analytics/value_streams/list_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::ValueStreams::ListService, feature_category: :value_stream_management do
+  let_it_be(:user) { create(:user) }
+
+  let(:params) { {} }
+  let(:service) { described_class.new(parent: parent, params: params, current_user: user) }
+
+  subject(:service_response) { service.execute }
+
+  shared_examples 'value stream list service examples' do
+    context 'when the resource is licensed' do
+      before do
+        stub_licensed_features(licensed_feature_name => true)
+      end
+
+      it 'returns the no value streams' do
+        expect(service_response).to be_success
+        expect(service_response.payload[:value_streams]).to be_empty
+      end
+
+      context 'when value stream records are present' do
+        let_it_be(:value_stream1) { create(:cycle_analytics_value_stream, namespace: parent, name: 'bbb') }
+        let_it_be(:value_stream2) { create(:cycle_analytics_value_stream, namespace: parent, name: 'aaa') }
+
+        it 'returns the value streams' do
+          expect(service_response).to be_success
+          expect(service_response.payload[:value_streams]).to match([
+            have_attributes(name: value_stream2.name),
+            have_attributes(name: value_stream1.name)
+          ])
+        end
+
+        context 'when filtering by value stream ids' do
+          before do
+            params[:value_stream_ids] = [value_stream2.id]
+          end
+
+          it 'returns the filtered value stream' do
+            expect(service_response).to be_success
+            expect(service_response.payload[:value_streams]).to match([
+              have_attributes(name: value_stream2.name)
+            ])
+          end
+        end
+      end
+
+      context 'when the user is not allowed to access the service' do
+        let(:user) { create(:user) }
+
+        it 'returns failed service response' do
+          expect(service_response).to be_error
+        end
+      end
+    end
+  end
+
+  context 'when project namespace is given' do
+    let(:licensed_feature_name) { :cycle_analytics_for_projects }
+
+    let_it_be(:parent) do
+      project = create(:project)
+      project.add_developer(user)
+      project.project_namespace
+    end
+
+    it_behaves_like 'value stream list service examples'
+
+    context 'when project is not licensed' do
+      it 'returns the default value stream' do
+        expect(service_response).to be_success
+        expect(service_response.payload[:value_streams]).to match([have_attributes(name: 'default')])
+      end
+    end
+  end
+
+  context 'when group is given' do
+    let(:licensed_feature_name) { :cycle_analytics_for_groups }
+
+    let_it_be(:parent) { create(:group).tap { |g| g.add_developer(user) } }
+
+    it_behaves_like 'value stream list service examples'
+
+    context 'when group is not licensed' do
+      it 'returns failed service response' do
+        expect(service_response).to be_error
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/project/value_streams_spec.rb b/spec/requests/api/graphql/project/value_streams_spec.rb
index 01e937c1e4746d0e2f46636f8f1a513f793185b5..0805f386ed8975566a5599df7a6256d433b2f191 100644
--- a/spec/requests/api/graphql/project/value_streams_spec.rb
+++ b/spec/requests/api/graphql/project/value_streams_spec.rb
@@ -8,14 +8,18 @@
   let_it_be(:project) { create(:project) }
   let_it_be(:user) { create(:user) }
 
+  let(:variables) { { fullPath: project.full_path } }
+
   let(:query) do
     <<~QUERY
       query($fullPath: ID!) {
         project(fullPath: $fullPath) {
           valueStreams {
             nodes {
+              id
               name
               stages {
+                id
                 name
                 startEventIdentifier
                 endEventIdentifier
@@ -34,6 +38,8 @@
           'valueStreams' => {
             'nodes' => [
               {
+                'id' => Gitlab::GlobalId.as_global_id('default',
+                  model_name: Analytics::CycleAnalytics::ValueStream.to_s).to_s,
                 'name' => 'default',
                 'stages' => expected_stages
               }
@@ -47,43 +53,53 @@
       [
         {
           'name' => 'issue',
+          'id' => stage_id('issue'),
           'startEventIdentifier' => 'ISSUE_CREATED',
           'endEventIdentifier' => 'ISSUE_STAGE_END'
         },
         {
           'name' => 'plan',
+          'id' => stage_id('plan'),
           'startEventIdentifier' => 'PLAN_STAGE_START',
           'endEventIdentifier' => 'ISSUE_FIRST_MENTIONED_IN_COMMIT'
         },
         {
           'name' => 'code',
+          'id' => stage_id('code'),
           'startEventIdentifier' => 'CODE_STAGE_START',
           'endEventIdentifier' => 'MERGE_REQUEST_CREATED'
         },
         {
           'name' => 'test',
+          'id' => stage_id('test'),
           'startEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_STARTED',
           'endEventIdentifier' => 'MERGE_REQUEST_LAST_BUILD_FINISHED'
         },
         {
           'name' => 'review',
+          'id' => stage_id('review'),
           'startEventIdentifier' => 'MERGE_REQUEST_CREATED',
           'endEventIdentifier' => 'MERGE_REQUEST_MERGED'
         },
         {
           'name' => 'staging',
+          'id' => stage_id('staging'),
           'startEventIdentifier' => 'MERGE_REQUEST_MERGED',
           'endEventIdentifier' => 'MERGE_REQUEST_FIRST_DEPLOYED_TO_PRODUCTION'
         }
       ]
     end
 
+    def stage_id(name)
+      Gitlab::GlobalId.as_global_id(name, model_name: Analytics::CycleAnalytics::Stage.to_s).to_s
+    end
+
     before_all do
       project.add_guest(user)
     end
 
     before do
-      post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
+      post_graphql(query, current_user: user, variables: variables)
     end
 
     it_behaves_like 'a working graphql query'
@@ -91,11 +107,62 @@
     it 'returns only `default` value stream' do
       expect(graphql_data).to eq(expected_value_stream)
     end
+
+    context 'when specifying the value stream id argument' do
+      let(:variables) { { fullPath: project.full_path } }
+
+      let(:query) do
+        <<~QUERY
+          query($fullPath: ID!, $stageId: ID) {
+            project(fullPath: $fullPath) {
+              valueStreams {
+                nodes {
+                  name
+                  stages(id: $stageId) {
+                    name
+                  }
+                }
+              }
+            }
+          }
+        QUERY
+      end
+
+      it 'locates the default value stream' do
+        expect(graphql_data_at(:project, :value_streams, :nodes)).to match([hash_including('name' => 'default')])
+      end
+
+      context 'when specifying the stage id argument' do
+        let(:stage_id) { Gitlab::GlobalId.as_global_id('test', model_name: Analytics::CycleAnalytics::Stage.to_s).to_s }
+        let(:variables) { { fullPath: project.full_path, stageId: stage_id } }
+
+        it 'returns only the test stage' do
+          expected_value_stream = hash_including(
+            'name' => 'default',
+            'stages' => [hash_including('name' => 'test')]
+          )
+
+          expect(graphql_data_at(:project, :value_streams, :nodes)).to match([expected_value_stream])
+        end
+
+        context 'when bogus stage id is given' do
+          let(:stage_id) do
+            Gitlab::GlobalId.as_global_id('bogus', model_name: Analytics::CycleAnalytics::Stage.to_s).to_s
+          end
+
+          let(:variables) { { fullPath: project.full_path, stageId: stage_id } }
+
+          it 'returns no data error' do
+            expect(graphql_data_at(:project, :value_streams, :nodes, 0, :stages, :nodes)).to be_empty
+          end
+        end
+      end
+    end
   end
 
   context 'when user does not have permission to read value streams' do
     before do
-      post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
+      post_graphql(query, current_user: user, variables: variables)
     end
 
     it 'returns nil' do
diff --git a/spec/services/analytics/cycle_analytics/value_streams/list_service_spec.rb b/spec/services/analytics/cycle_analytics/value_streams/list_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c7891c4a44859ae6c4c2fdf5d242f85fb83a0e8c
--- /dev/null
+++ b/spec/services/analytics/cycle_analytics/value_streams/list_service_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::ValueStreams::ListService, feature_category: :value_stream_management do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project) }
+
+  let(:service) { described_class.new(parent: project.project_namespace, current_user: user) }
+
+  subject(:service_response) { service.execute }
+
+  it 'returns the default value stream' do
+    project.add_developer(user)
+    expect(service_response).to be_success
+    expect(service_response.payload[:value_streams]).to match([have_attributes(name: 'default')])
+  end
+
+  context 'when the user is not part of the project' do
+    it 'fails' do
+      expect(service_response).to be_error
+    end
+  end
+end