diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
index 66ec2c45cf8ed6a50a2e0f808973322b69e1c1b8..1f90f3945211418944853c8decc16c6f218f8154 100644
--- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
+++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
@@ -21,6 +21,9 @@ module UpdateArguments
         argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
                  required: false,
                  description: 'Input for description widget.'
+        argument :assignees_widget, ::Types::WorkItems::Widgets::AssigneesInputType,
+                 required: false,
+                 description: 'Input for assignees widget.'
         argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyUpdateInputType,
                  required: false,
                  description: 'Input for hierarchy widget.'
diff --git a/app/graphql/types/work_items/widgets/assignees_input_type.rb b/app/graphql/types/work_items/widgets/assignees_input_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee61bc73054c89b62f52edafb84d8cb6ed02795f
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/assignees_input_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+  module WorkItems
+    module Widgets
+      class AssigneesInputType < BaseInputObject
+        graphql_name 'WorkItemWidgetAssigneesInput'
+
+        argument :assignee_ids, [::Types::GlobalIDType[::User]],
+          required: true,
+          description: 'Global IDs of assignees.',
+          prepare: ->(ids, _) { ids.map(&:model_id) }
+      end
+    end
+  end
+end
diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb
index 2fd84761a9bd317359315df9e05f9fa4e401ab77..1ccc152bc6bd568623b66aa196a9e6c8984d967f 100644
--- a/app/policies/work_item_policy.rb
+++ b/app/policies/work_item_policy.rb
@@ -8,6 +8,7 @@ class WorkItemPolicy < IssuePolicy
   rule { can?(:destroy_issue) | is_member_and_author }.enable :delete_work_item
 
   rule { can?(:update_issue) }.enable :update_work_item
+  rule { can?(:set_issue_metadata) }.enable :set_work_item_metadata
 
   rule { can?(:read_issue) }.enable :read_work_item
   # because IssuePolicy delegates to ProjectPolicy and
diff --git a/app/services/work_items/widgets/assignees_service/update_service.rb b/app/services/work_items/widgets/assignees_service/update_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9176b71c85eafd1e2607fe09b265ccfe84761700
--- /dev/null
+++ b/app/services/work_items/widgets/assignees_service/update_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module WorkItems
+  module Widgets
+    module AssigneesService
+      class UpdateService < WorkItems::Widgets::BaseService
+        def before_update_in_transaction(params:)
+          return unless params.present? && params.has_key?(:assignee_ids)
+          return unless has_permission?(:set_work_item_metadata)
+
+          assignee_ids = filter_assignees_count(params[:assignee_ids])
+          assignee_ids = filter_assignee_permissions(assignee_ids)
+
+          return if assignee_ids.sort == work_item.assignee_ids.sort
+
+          work_item.assignee_ids = assignee_ids
+          work_item.touch
+        end
+
+        private
+
+        def filter_assignees_count(assignee_ids)
+          return assignee_ids if work_item.allows_multiple_assignees?
+
+          assignee_ids.first(1)
+        end
+
+        def filter_assignee_permissions(assignee_ids)
+          assignees = User.id_in(assignee_ids)
+
+          assignees.select { |assignee| assignee.can?(:read_work_item, work_item) }.map(&:id)
+        end
+      end
+    end
+  end
+end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e50f9170a9d4c10f5e3881208b1e8d0b0bc9c2e9..f535f23912b5faa94fbda77653e8392dcaabf56c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -5741,6 +5741,7 @@ Input type: `WorkItemUpdateInput`
 
 | Name | Type | Description |
 | ---- | ---- | ----------- |
+| <a id="mutationworkitemupdateassigneeswidget"></a>`assigneesWidget` | [`WorkItemWidgetAssigneesInput`](#workitemwidgetassigneesinput) | Input for assignees widget. |
 | <a id="mutationworkitemupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
 | <a id="mutationworkitemupdateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
 | <a id="mutationworkitemupdatedescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
@@ -22496,6 +22497,7 @@ A time-frame defined as a closed inclusive range of two dates.
 
 | Name | Type | Description |
 | ---- | ---- | ----------- |
+| <a id="workitemupdatedtaskinputassigneeswidget"></a>`assigneesWidget` | [`WorkItemWidgetAssigneesInput`](#workitemwidgetassigneesinput) | Input for assignees widget. |
 | <a id="workitemupdatedtaskinputconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
 | <a id="workitemupdatedtaskinputdescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
 | <a id="workitemupdatedtaskinputhierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. |
@@ -22504,6 +22506,14 @@ A time-frame defined as a closed inclusive range of two dates.
 | <a id="workitemupdatedtaskinputstateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. |
 | <a id="workitemupdatedtaskinputtitle"></a>`title` | [`String`](#string) | Title of the work item. |
 
+### `WorkItemWidgetAssigneesInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="workitemwidgetassigneesinputassigneeids"></a>`assigneeIds` | [`[UserID!]!`](#userid) | Global IDs of assignees. |
+
 ### `WorkItemWidgetDescriptionInput`
 
 #### Arguments
diff --git a/spec/graphql/types/work_items/widgets/assignees_input_type_spec.rb b/spec/graphql/types/work_items/widgets/assignees_input_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2fcda2a43be03c72f1e29797ada24c7b7a8839ee
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/assignees_input_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Types::WorkItems::Widgets::AssigneesInputType do
+  it { expect(described_class.graphql_name).to eq('WorkItemWidgetAssigneesInput') }
+
+  it { expect(described_class.arguments.keys).to match_array(%w[assigneeIds]) }
+end
diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb
index f3e8bd6a08b5e4960ad5b93f64e1e19c57c8d076..ed76ec1eccf408acc3cf27a09257d62693fd28fa 100644
--- a/spec/policies/work_item_policy_spec.rb
+++ b/spec/policies/work_item_policy_spec.rb
@@ -181,4 +181,24 @@
       end
     end
   end
+
+  describe 'set_work_item_metadata' do
+    context 'when user is reporter' do
+      let(:current_user) { reporter }
+
+      it { is_expected.to be_allowed(:set_work_item_metadata) }
+    end
+
+    context 'when user is guest' do
+      let(:current_user) { guest }
+
+      it { is_expected.to be_disallowed(:set_work_item_metadata) }
+
+      context 'when the work item is not persisted yet' do
+        let(:work_item_subject) { build(:work_item, project: project) }
+
+        it { is_expected.to be_allowed(:set_work_item_metadata) }
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index d5e1ec25a1383eee6989966104a1028951357779..909d6549fa5aff8f51154460a9e70b8e280938b7 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -427,6 +427,50 @@
       end
     end
 
+    context 'when updating assignees' do
+      let(:fields) do
+        <<~FIELDS
+        workItem {
+          widgets {
+            type
+            ... on WorkItemWidgetAssignees {
+              assignees {
+                nodes {
+                  id
+                  username
+                }
+              }
+            }
+          }
+        }
+        errors
+        FIELDS
+      end
+
+      let(:input) do
+        { 'assigneesWidget' => { 'assigneeIds' => [developer.to_global_id.to_s] } }
+      end
+
+      it 'updates the work item assignee' do
+        expect do
+          post_graphql_mutation(mutation, current_user: current_user)
+          work_item.reload
+        end.to change(work_item, :assignee_ids).from([]).to([developer.id])
+
+        expect(response).to have_gitlab_http_status(:success)
+        expect(mutation_response['workItem']['widgets']).to include(
+          {
+            'type' => 'ASSIGNEES',
+            'assignees' => {
+              'nodes' => [
+                { 'id' => developer.to_global_id.to_s, 'username' => developer.username }
+              ]
+            }
+          }
+        )
+      end
+    end
+
     context 'when unsupported widget input is sent' do
       let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
       let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
diff --git a/spec/services/work_items/widgets/assignees_service/update_service_spec.rb b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..48927c4b05b4b9d8ebff73df00f2d83c85751b1d
--- /dev/null
+++ b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time do
+  let_it_be(:reporter) { create(:user) }
+  let_it_be(:project) { create(:project, :private) }
+  let_it_be(:new_assignee) { create(:user) }
+
+  let(:work_item) do
+    create(:work_item, project: project, updated_at: 1.day.ago)
+  end
+
+  let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Assignees) } }
+  let(:current_user) { reporter }
+  let(:params) { { assignee_ids: [new_assignee.id] } }
+
+  before_all do
+    project.add_reporter(reporter)
+    project.add_guest(new_assignee)
+  end
+
+  describe '#before_update_in_transaction' do
+    subject do
+      described_class.new(widget: widget, current_user: current_user)
+        .before_update_in_transaction(params: params)
+    end
+
+    it 'updates the assignees and sets updated_at to the current time' do
+      subject
+
+      expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
+      expect(work_item.updated_at).to be_like_time(Time.current)
+    end
+
+    context 'when passing an empty array' do
+      let(:params) { { assignee_ids: [] } }
+
+      before do
+        work_item.assignee_ids = [reporter.id]
+      end
+
+      it 'removes existing assignees' do
+        subject
+
+        expect(work_item.assignee_ids).to be_empty
+        expect(work_item.updated_at).to be_like_time(Time.current)
+      end
+    end
+
+    context 'when user does not have access' do
+      let(:current_user) { create(:user) }
+
+      it 'does not update the assignees' do
+        subject
+
+        expect(work_item.assignee_ids).to be_empty
+        expect(work_item.updated_at).to be_like_time(1.day.ago)
+      end
+    end
+
+    context 'when multiple assignees are given' do
+      let(:params) { { assignee_ids: [new_assignee.id, reporter.id] } }
+
+      context 'when work item allows multiple assignees' do
+        before do
+          allow(work_item).to receive(:allows_multiple_assignees?).and_return(true)
+        end
+
+        it 'sets all the given assignees' do
+          subject
+
+          expect(work_item.assignee_ids).to contain_exactly(new_assignee.id, reporter.id)
+          expect(work_item.updated_at).to be_like_time(Time.current)
+        end
+      end
+
+      context 'when work item does not allow multiple assignees' do
+        before do
+          allow(work_item).to receive(:allows_multiple_assignees?).and_return(false)
+        end
+
+        it 'only sets the first assignee' do
+          subject
+
+          expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
+          expect(work_item.updated_at).to be_like_time(Time.current)
+        end
+      end
+    end
+
+    context 'when assignee does not have access to the work item' do
+      let(:params) { { assignee_ids: [create(:user).id] } }
+
+      it 'does not set the assignee' do
+        subject
+
+        expect(work_item.assignee_ids).to be_empty
+        expect(work_item.updated_at).to be_like_time(1.day.ago)
+      end
+    end
+
+    context 'when assignee ids are the same as the existing ones' do
+      before do
+        work_item.assignee_ids = [new_assignee.id]
+      end
+
+      it 'does not touch updated_at' do
+        subject
+
+        expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
+        expect(work_item.updated_at).to be_like_time(1.day.ago)
+      end
+    end
+  end
+end