diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 7ca3f20ec1c709edbd79269c75088c70026e4ca4..3885be1dadcc64b3b1c4bf09ab32a45362e6d7d5 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -129,5 +129,8 @@
     "VulnerabilityLocationGeneric",
     "VulnerabilityLocationSast",
     "VulnerabilityLocationSecretDetection"
+  ],
+  "WorkItemWidget": [
+    "WorkItemWidgetDescription"
   ]
 }
diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb
index cd784d54959fd0d3701488820351499eef694660..18b9bfd1c9aecda50d8ff46f27736a685fe0c838 100644
--- a/app/graphql/types/work_item_type.rb
+++ b/app/graphql/types/work_item_type.rb
@@ -18,6 +18,8 @@ class WorkItemType < BaseObject
           description: 'State of the work item.'
     field :title, GraphQL::Types::String, null: false,
           description: 'Title of the work item.'
+    field :widgets, [Types::WorkItems::WidgetInterface], null: true,
+          description: 'Collection of widgets that belong to the work item.'
     field :work_item_type, Types::WorkItems::TypeType, null: false,
           description: 'Type assigned to the work item.'
 
diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb
new file mode 100644
index 0000000000000000000000000000000000000000..67765c5b4325a73f37954e34ab7c31afc07c640b
--- /dev/null
+++ b/app/graphql/types/work_items/widget_interface.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+  module WorkItems
+    module WidgetInterface
+      include Types::BaseInterface
+
+      graphql_name 'WorkItemWidget'
+
+      field :type, ::Types::WorkItems::WidgetTypeEnum, null: true,
+            description: 'Widget type.'
+
+      def self.resolve_type(object, context)
+        case object
+        when ::WorkItems::Widgets::Description
+          ::Types::WorkItems::Widgets::DescriptionType
+        else
+          raise "Unknown GraphQL type for widget #{object}"
+        end
+      end
+
+      orphan_types ::Types::WorkItems::Widgets::DescriptionType
+    end
+  end
+end
diff --git a/app/graphql/types/work_items/widget_type_enum.rb b/app/graphql/types/work_items/widget_type_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4e5933bff869b7aab175a3102fa62d7894d7f83a
--- /dev/null
+++ b/app/graphql/types/work_items/widget_type_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+  module WorkItems
+    class WidgetTypeEnum < BaseEnum
+      graphql_name 'WorkItemWidgetType'
+      description 'Type of a work item widget'
+
+      ::WorkItems::Type.available_widgets.each do |widget|
+        value widget.type.to_s.upcase, value: widget.type, description: "#{widget.type.to_s.titleize} widget."
+      end
+    end
+  end
+end
diff --git a/app/graphql/types/work_items/widgets/description_type.rb b/app/graphql/types/work_items/widgets/description_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..79192d7c3d434b24a6cc0815635f5aa008dd0713
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/description_type.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+  module WorkItems
+    module Widgets
+      # Disabling widget level authorization as it might be too granular
+      # and we already authorize the parent work item
+      # rubocop:disable Graphql/AuthorizeTypes
+      class DescriptionType < BaseObject
+        graphql_name 'WorkItemWidgetDescription'
+        description 'Represents a description widget'
+
+        implements Types::WorkItems::WidgetInterface
+
+        field :description, GraphQL::Types::String, null: true,
+              description: 'Description of the work item.'
+
+        markdown_field :description_html, null: true do |resolved_object|
+          resolved_object.work_item
+        end
+      end
+      # rubocop:enable Graphql/AuthorizeTypes
+    end
+  end
+end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 557694da35ae5e6fdd444b06ca0faec8b1975114..d5cd1140ed25b7a0629699c59058214e744579e5 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -8,6 +8,12 @@ def noteable_target_type_name
     'issue'
   end
 
+  def widgets
+    work_item_type.widgets.map do |widget_class|
+      widget_class.new(self)
+    end
+  end
+
   private
 
   def record_create_action
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index 0d390fa131d47c38142b4945185d1bbbad333d63..f708a30f12866119f984703d8852351cb1539920 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -20,6 +20,14 @@ class Type < ApplicationRecord
       task:        { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 }
     }.freeze
 
+    WIDGETS_FOR_TYPE = {
+      issue: [Widgets::Description],
+      incident: [Widgets::Description],
+      test_case: [Widgets::Description],
+      requirement: [Widgets::Description],
+      task: [Widgets::Description]
+    }.freeze
+
     cache_markdown_field :description, pipeline: :single_line
 
     enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] }
@@ -40,6 +48,10 @@ class Type < ApplicationRecord
     scope :order_by_name_asc, -> { order(arel_table[:name].lower.asc) }
     scope :by_type, ->(base_type) { where(base_type: base_type) }
 
+    def self.available_widgets
+      WIDGETS_FOR_TYPE.values.flatten.uniq
+    end
+
     def self.default_by_type(type)
       found_type = find_by(namespace_id: nil, base_type: type)
       return found_type if found_type
@@ -60,6 +72,10 @@ def default?
       namespace.blank?
     end
 
+    def widgets
+      WIDGETS_FOR_TYPE[base_type.to_sym]
+    end
+
     private
 
     def strip_whitespace
diff --git a/app/models/work_items/widgets/base.rb b/app/models/work_items/widgets/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9d1e48690e01e0a23decc45fa6aa29a46e3a7337
--- /dev/null
+++ b/app/models/work_items/widgets/base.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module WorkItems
+  module Widgets
+    class Base
+      def self.type
+        name.demodulize.underscore.to_sym
+      end
+
+      def type
+        self.class.type
+      end
+
+      def initialize(work_item)
+        @work_item = work_item
+      end
+
+      attr_reader :work_item
+    end
+  end
+end
diff --git a/app/models/work_items/widgets/description.rb b/app/models/work_items/widgets/description.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e84d172befb69fec9a50c3fcae92940c5d1751b
--- /dev/null
+++ b/app/models/work_items/widgets/description.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module WorkItems
+  module Widgets
+    class Description < Base
+      delegate :description, to: :work_item
+    end
+  end
+end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 82b3ae84f622ad1d9dd83809570c99153f31879d..9a8718aef09aa2abf2060d073f1c47ce6171c59e 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -17840,6 +17840,7 @@ Represents vulnerability letter grades with associated projects.
 | <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title of the work item. |
 | <a id="workitemtitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
 | <a id="workitemuserpermissions"></a>`userPermissions` | [`WorkItemPermissions!`](#workitempermissions) | Permissions for the current user on the resource. |
+| <a id="workitemwidgets"></a>`widgets` | [`[WorkItemWidget!]`](#workitemwidget) | Collection of widgets that belong to the work item. |
 | <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. |
 
 ### `WorkItemPermissions`
@@ -17864,6 +17865,18 @@ Check permissions for the current user on a work item.
 | <a id="workitemtypeid"></a>`id` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of the work item type. |
 | <a id="workitemtypename"></a>`name` | [`String!`](#string) | Name of the work item type. |
 
+### `WorkItemWidgetDescription`
+
+Represents a description widget.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="workitemwidgetdescriptiondescription"></a>`description` | [`String`](#string) | Description of the work item. |
+| <a id="workitemwidgetdescriptiondescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
+| <a id="workitemwidgetdescriptiontype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
+
 ## Enumeration types
 
 Also called _Enums_, enumeration types are a special kind of scalar that
@@ -19608,6 +19621,14 @@ Values for work item state events.
 | <a id="workitemstateeventclose"></a>`CLOSE` | Closes the work item. |
 | <a id="workitemstateeventreopen"></a>`REOPEN` | Reopens the work item. |
 
+### `WorkItemWidgetType`
+
+Type of a work item widget.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="workitemwidgettypedescription"></a>`DESCRIPTION` | Description widget. |
+
 ## Scalar types
 
 Scalar values are atomic values, and do not have fields of their own.
@@ -20803,6 +20824,18 @@ four standard [pagination arguments](#connection-pagination-arguments):
 | <a id="usertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
 | <a id="usertodostype"></a>`type` | [`[TodoTargetEnum!]`](#todotargetenum) | Type of the todo. |
 
+#### `WorkItemWidget`
+
+Implementations:
+
+- [`WorkItemWidgetDescription`](#workitemwidgetdescription)
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="workitemwidgettype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
+
 ## Input types
 
 Types that may be used as arguments (all scalar types may also
diff --git a/lib/gitlab/graphql/markdown_field.rb b/lib/gitlab/graphql/markdown_field.rb
index 6188d860aba35a46af25bd19b51464a91a94db18..43dddf4c4bc4ad522e63c58ec81ba3272126a2f9 100644
--- a/lib/gitlab/graphql/markdown_field.rb
+++ b/lib/gitlab/graphql/markdown_field.rb
@@ -22,8 +22,10 @@ def self.markdown_field(name, **kwargs)
           field name, GraphQL::Types::String, **kwargs
 
           define_method resolver_method do
+            markdown_object = block_given? ? yield(object) : object
+
             # We need to `dup` the context so the MarkdownHelper doesn't modify it
-            ::MarkupHelper.markdown_field(object, method_name.to_sym, context.to_h.dup)
+            ::MarkupHelper.markdown_field(markdown_object, method_name.to_sym, context.to_h.dup)
           end
         end
       end
diff --git a/spec/graphql/types/work_item_type_spec.rb b/spec/graphql/types/work_item_type_spec.rb
index a048050615641cad1e13248ddf0f5b7329c09027..7ed58786b5be992e132c4baed4194691ff16da7a 100644
--- a/spec/graphql/types/work_item_type_spec.rb
+++ b/spec/graphql/types/work_item_type_spec.rb
@@ -10,7 +10,18 @@
   specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::WorkItem) }
 
   it 'has specific fields' do
-    fields = %i[description description_html id iid lock_version state title title_html userPermissions work_item_type]
+    fields = %i[
+      description
+      description_html
+      id
+      iid
+      lock_version
+      state title
+      title_html
+      userPermissions
+      widgets
+      work_item_type
+    ]
 
     fields.each do |field_name|
       expect(described_class).to have_graphql_fields(*fields)
diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63d7782df28f9811208db6ce4534daf6cd60ec2d
--- /dev/null
+++ b/spec/graphql/types/work_items/widget_interface_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::WidgetInterface do
+  include GraphqlHelpers
+
+  it 'exposes the expected fields' do
+    expected_fields = %i[type]
+
+    expect(described_class).to have_graphql_fields(*expected_fields)
+  end
+
+  describe ".resolve_type" do
+    it 'knows the correct type for objects' do
+      expect(
+        described_class.resolve_type(WorkItems::Widgets::Description.new(build(:work_item)), {})
+      ).to eq(Types::WorkItems::Widgets::DescriptionType)
+    end
+
+    it 'raises an error for an unknown type' do
+      project = build(:project)
+
+      expect_graphql_error_to_be_created("Unknown GraphQL type for widget #{project}") do
+        described_class.resolve_type(project, {})
+      end
+    end
+  end
+end
diff --git a/spec/graphql/types/work_items/widget_type_enum_spec.rb b/spec/graphql/types/work_items/widget_type_enum_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e7ac9b9c317c6260bc9c1ad411889152f53a57ee
--- /dev/null
+++ b/spec/graphql/types/work_items/widget_type_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['WorkItemWidgetType'] do
+  specify { expect(described_class.graphql_name).to eq('WorkItemWidgetType') }
+
+  it 'exposes all the existing widget type values' do
+    expect(described_class.values.transform_values { |v| v.value }).to include(
+      'DESCRIPTION' => :description
+    )
+  end
+end
diff --git a/spec/graphql/types/work_items/widgets/description_type_spec.rb b/spec/graphql/types/work_items/widgets/description_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5ade1fe4aa2e6d60c3bc75709ee56ed487f8d45d
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/description_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::Widgets::DescriptionType do
+  it 'exposes the expected fields' do
+    expected_fields = %i[description description_html type]
+
+    expect(described_class).to have_graphql_fields(*expected_fields)
+  end
+end
diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb
index 84494d3dd68584c84fcbdc00e6f9e21d5bbf21b5..a73eba1e9db2c57fe73f188d4a52eeef9ce34327 100644
--- a/spec/lib/gitlab/graphql/markdown_field_spec.rb
+++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb
@@ -55,6 +55,20 @@
         end
       end
 
+      context 'when a block is passed for the resolved object' do
+        let(:type_class) do
+          class_with_markdown_field(:note_html, null: false) do |resolved_object|
+            resolved_object.object
+          end
+        end
+
+        let(:type_instance) { type_class.authorized_new(class_wrapped_object(note), context) }
+
+        it 'renders markdown from the same property as the field name without the `_html` suffix' do
+          expect(field.resolve(type_instance, {}, context)).to eq(expected_markdown)
+        end
+      end
+
       describe 'basic verification that references work' do
         let_it_be(:project) { create(:project, :public) }
 
@@ -83,12 +97,22 @@
     end
   end
 
-  def class_with_markdown_field(name, **args)
+  def class_with_markdown_field(name, **args, &blk)
     Class.new(Types::BaseObject) do
       prepend Gitlab::Graphql::MarkdownField
       graphql_name 'MarkdownFieldTest'
 
-      markdown_field name, **args
+      markdown_field name, **args, &blk
     end
   end
+
+  def class_wrapped_object(object)
+    Class.new do
+      def initialize(object)
+        @object = object
+      end
+
+      attr_accessor :object
+    end.new(object)
+  end
 end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index e92ae746911b90c3a721eb851b1b8f9d16c80c66..dfb8f9787e08944b27b5b4ab3834b1e4c5497d7b 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -11,6 +11,12 @@
     end
   end
 
+  describe '#widgets' do
+    subject { build(:work_item).widgets }
+
+    it { is_expected.to contain_exactly(instance_of(WorkItems::Widgets::Description)) }
+  end
+
   describe 'callbacks' do
     describe 'record_create_action' do
       it 'records the creation action after saving' do
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index 6e9f3210e65ab7b711415318d1e04a238b613bbc..6936b8f558e97e6622df66be1111d13f8bc4cc2c 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -60,7 +60,13 @@
     it { is_expected.not_to allow_value('s' * 256).for(:icon_name) }
   end
 
-  describe 'default?' do
+  describe '.available_widgets' do
+    subject { described_class.available_widgets }
+
+    it { is_expected.to contain_exactly(::WorkItems::Widgets::Description) }
+  end
+
+  describe '#default?' do
     subject { build(:work_item_type, namespace: namespace).default? }
 
     context 'when namespace is nil' do
diff --git a/spec/models/work_items/widgets/base_spec.rb b/spec/models/work_items/widgets/base_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b4b4d9e98fae3c43ae2ab8f0fb0ec30697b091c
--- /dev/null
+++ b/spec/models/work_items/widgets/base_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::Base do
+  let_it_be(:work_item) { create(:work_item, description: '# Title') }
+
+  describe '.type' do
+    subject { described_class.type }
+
+    it { is_expected.to eq(:base) }
+  end
+
+  describe '#type' do
+    subject { described_class.new(work_item).type }
+
+    it { is_expected.to eq(:base) }
+  end
+end
diff --git a/spec/models/work_items/widgets/description_spec.rb b/spec/models/work_items/widgets/description_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8359db31bff547b4ba1cf34777389175b49b45c2
--- /dev/null
+++ b/spec/models/work_items/widgets/description_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::Description do
+  let_it_be(:work_item) { create(:work_item, description: '# Title') }
+
+  describe '.type' do
+    subject { described_class.type }
+
+    it { is_expected.to eq(:description) }
+  end
+
+  describe '#type' do
+    subject { described_class.new(work_item).type }
+
+    it { is_expected.to eq(:description) }
+  end
+
+  describe '#description' do
+    subject { described_class.new(work_item).description }
+
+    it { is_expected.to eq(work_item.description) }
+  end
+end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 5b34c21989a34fe867dcec4f61209370a32acec0..1c93ae453c0a4693626bac6c5f5576ce96743e06 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -7,7 +7,7 @@
 
   let_it_be(:developer) { create(:user) }
   let_it_be(:project) { create(:project, :private).tap { |project| project.add_developer(developer) } }
-  let_it_be(:work_item) { create(:work_item, project: project) }
+  let_it_be(:work_item) { create(:work_item, project: project, description: '- List item') }
 
   let(:current_user) { developer }
   let(:work_item_data) { graphql_data['workItem'] }
@@ -38,6 +38,34 @@
       )
     end
 
+    context 'when querying widgets' do
+      let(:work_item_fields) do
+        <<~GRAPHQL
+          id
+          widgets {
+            type
+            ... on WorkItemWidgetDescription {
+              description
+              descriptionHtml
+            }
+          }
+        GRAPHQL
+      end
+
+      it 'returns widget information' do
+        expect(work_item_data).to include(
+          'id' => work_item.to_gid.to_s,
+          'widgets' => contain_exactly(
+            hash_including(
+              'type' => 'DESCRIPTION',
+              'description' => work_item.description,
+              'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {})
+            )
+          )
+        )
+      end
+    end
+
     context 'when an Issue Global ID is provided' do
       let(:global_id) { Issue.find(work_item.id).to_gid.to_s }