diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 2dafb5e752f8c686a4caaeca010e16c3044e5e93..62e8e66b1e07d4a1291a0dbfece3790af8ed510c 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -151,4 +151,9 @@ module Issuable
   def notes_with_associations
     notes.includes(:author, :project)
   end
+
+  def updated_tasks
+    Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
+                               new_content: description)
+  end
 end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 660e58b876db8d4acb48a1ca98ee9fb4a5b8e0d3..3daa4dbe24e85c3de911d8fb5617c5d63b514506 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -7,14 +7,37 @@ require 'task_list/filter'
 #
 # Used by MergeRequest and Issue
 module Taskable
+  ITEM_PATTERN = /
+    ^
+    (?:\s*[-+*]|(?:\d+\.))? # optional list prefix
+    \s*                     # optional whitespace prefix
+    (\[\s\]|\[[xX]\])       # checkbox
+    (\s.+)                  # followed by whitespace and some text.
+  /x
+
+  def self.get_tasks(content)
+    content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
+      # ITEM_PATTERN strips out the hyphen, but Item requires it. Rabble rabble.
+      TaskList::Item.new("- #{checkbox}", label.strip)
+    end
+  end
+
+  def self.get_updated_tasks(old_content:, new_content:)
+    old_tasks, new_tasks = get_tasks(old_content), get_tasks(new_content)
+
+    new_tasks.select.with_index do |new_task, i|
+      old_task = old_tasks[i]
+      next unless old_task
+
+      new_task.source == new_task.source && new_task.complete? != old_task.complete?
+    end
+  end
+
   # Called by `TaskList::Summary`
   def task_list_items
     return [] if description.blank?
 
-    @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item|
-      # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
-      TaskList::Item.new("- #{item}")
-    end
+    @task_list_items ||= Taskable.get_tasks(description)
   end
 
   def tasks
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 11d2b08bba7d8c63adea02016f0848f155b26a5d..19055fb67ff571cc593391f8af723ddedc661655 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -27,6 +27,12 @@ class IssuableBaseService < BaseService
       old_branch, new_branch)
   end
 
+  def create_task_status_note(issuable)
+    issuable.updated_tasks.each do |task|
+      SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
+    end
+  end
+
   def filter_params(issuable_ability_name = :issue)
     params[:assignee_id]  = "" if params[:assignee_id] == IssuableFinder::NONE
     params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
@@ -55,6 +61,7 @@ class IssuableBaseService < BaseService
           old_labels - issuable.labels)
       end
 
+      handle_common_system_notes(issuable)
       handle_changes(issuable)
       issuable.create_new_cross_references!(current_user)
       execute_hooks(issuable, 'update')
@@ -71,4 +78,14 @@ class IssuableBaseService < BaseService
       close_service.new(project, current_user, {}).execute(issuable)
     end
   end
+
+  def handle_common_system_notes(issuable)
+    if issuable.previous_changes.include?('title')
+      create_title_change_note(issuable, issuable.previous_changes['title'].first)
+    end
+
+    if issuable.previous_changes.include?('description') && issuable.tasks?
+      create_task_status_note(issuable)
+    end
+  end
 end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 7c112f731a7f1116567a299e1e4952cb0e196a1d..a55a04dd5e07844346e5efebf02c13aafff06372 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -13,10 +13,6 @@ module Issues
         create_assignee_note(issue)
         notification_service.reassigned_issue(issue, current_user)
       end
-
-      if issue.previous_changes.include?('title')
-        create_title_change_note(issue, issue.previous_changes['title'].first)
-      end
     end
 
     def reopen_service
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index a5db3776eb694d2a89d9cffee0c12f61d60293c7..5ff2cc03ddafb024b7bd08fd577ee6d6bc5a49ad 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -30,10 +30,6 @@ module MergeRequests
         notification_service.reassigned_merge_request(merge_request, current_user)
       end
 
-      if merge_request.previous_changes.include?('title')
-        create_title_change_note(merge_request, merge_request.previous_changes['title'].first)
-      end
-
       if merge_request.previous_changes.include?('target_branch') ||
           merge_request.previous_changes.include?('source_branch')
         merge_request.mark_as_unchecked
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 708c2f00486aef8ca0d54aacee44fcc0d166beab..7c5d523ef39a682ab09e181bf2b4d08cd933457f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -341,4 +341,21 @@ class SystemNoteService
 
     "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
   end
+
+  # Called when the status of a Task has changed
+  #
+  # noteable  - Noteable object.
+  # project   - Project owning noteable
+  # author    - User performing the change
+  # new_task  - TaskList::Item object.
+  #
+  # Example Note text:
+  #
+  #   "Soandso marked the task Whatever as completed."
+  #
+  # Returns the created Note object
+  def self.change_task_status(noteable, project, author, new_task)
+    body = "Marked the task **#{new_task.source}** as #{new_task.status_label}"
+    create_note(noteable: noteable, project: project, author: author, note: body)
+  end
 end
diff --git a/config/initializers/task_list_ext.rb b/config/initializers/task_list_ext.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c05b683b5beab9887da4d55c819a08881aa4acc3
--- /dev/null
+++ b/config/initializers/task_list_ext.rb
@@ -0,0 +1,12 @@
+require 'task_list'
+
+class TaskList
+  class Item
+    COMPLETED = 'completed'.freeze
+    INCOMPLETE = 'incomplete'.freeze
+
+    def status_label
+      complete? ? COMPLETED : INCOMPLETE
+    end
+  end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index f55527ee9a353602ac0cf2a5c496dcab6d78ceee..adb3aa143ae9838ed717592e6289eb3c27161af8 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -15,6 +15,17 @@ describe Issues::UpdateService do
   end
 
   describe 'execute' do
+    def find_note(starting_with)
+      @issue.notes.find do |note|
+        note && note.note.start_with?(starting_with)
+      end
+    end
+
+    def update_issue(opts)
+      @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+      @issue.reload
+    end
+
     context "valid params" do
       before do
         opts = {
@@ -44,12 +55,6 @@ describe Issues::UpdateService do
         expect(email.subject).to include(issue.title)
       end
 
-      def find_note(starting_with)
-        @issue.notes.find do |note|
-          note && note.note.start_with?(starting_with)
-        end
-      end
-
       it 'should create system note about issue reassign' do
         note = find_note('Reassigned to')
 
@@ -71,5 +76,52 @@ describe Issues::UpdateService do
         expect(note.note).to eq 'Title changed from **Old title** to **New title**'
       end
     end
+
+    context 'when Issue has tasks' do
+      before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+
+      it { expect(@issue.tasks?).to eq(true) }
+
+      context 'when tasks are marked as completed' do
+        before { update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) }
+
+        it 'creates system note about task status change' do
+          note1 = find_note('Marked the task **Task 1** as completed')
+          note2 = find_note('Marked the task **Task 2** as completed')
+
+          expect(note1).not_to be_nil
+          expect(note2).not_to be_nil
+        end
+      end
+
+      context 'when tasks are marked as incomplete' do
+        before do
+          update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
+          update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" })
+        end
+
+        it 'creates system note about task status change' do
+          note1 = find_note('Marked the task **Task 1** as incomplete')
+          note2 = find_note('Marked the task **Task 2** as incomplete')
+
+          expect(note1).not_to be_nil
+          expect(note2).not_to be_nil
+        end
+      end
+
+      context 'when tasks position has been modified' do
+        before do
+          update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
+          update_issue({ description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2" })
+        end
+
+        it 'does not create a system note' do
+          note = find_note('Marked the task **Task 2** as incomplete')
+
+          expect(note).to be_nil
+        end
+      end
+    end
+
   end
 end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 2ed51d223b7770ebff22bff473f677ba10bf0f73..97f5c009aec1f64538e195d43167a0cd1ce229f0 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -14,6 +14,17 @@ describe MergeRequests::UpdateService do
   end
 
   describe 'execute' do
+    def find_note(starting_with)
+      @merge_request.notes.find do |note|
+        note && note.note.start_with?(starting_with)
+      end
+    end
+
+    def update_merge_request(opts)
+      @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+      @merge_request.reload
+    end
+
     context 'valid params' do
       let(:opts) do
         {
@@ -56,12 +67,6 @@ describe MergeRequests::UpdateService do
         expect(email.subject).to include(merge_request.title)
       end
 
-      def find_note(starting_with)
-        @merge_request.notes.find do |note|
-          note && note.note.start_with?(starting_with)
-        end
-      end
-
       it 'should create system note about merge_request reassign' do
         note = find_note('Reassigned to')
 
@@ -90,5 +95,39 @@ describe MergeRequests::UpdateService do
         expect(note.note).to eq 'Target branch changed from `master` to `target`'
       end
     end
+
+    context 'when MergeRequest has tasks' do
+      before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+
+      it { expect(@merge_request.tasks?).to eq(true) }
+
+      context 'when tasks are marked as completed' do
+        before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) }
+
+        it 'creates system note about task status change' do
+          note1 = find_note('Marked the task **Task 1** as completed')
+          note2 = find_note('Marked the task **Task 2** as completed')
+
+          expect(note1).not_to be_nil
+          expect(note2).not_to be_nil
+        end
+      end
+
+      context 'when tasks are marked as incomplete' do
+        before do
+          update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" })
+          update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" })
+        end
+
+        it 'creates system note about task status change' do
+          note1 = find_note('Marked the task **Task 1** as incomplete')
+          note2 = find_note('Marked the task **Task 2** as incomplete')
+
+          expect(note1).not_to be_nil
+          expect(note2).not_to be_nil
+        end
+      end
+    end
+
   end
 end