diff --git a/ee/app/models/ai/amazon_q/request_payload.rb b/ee/app/models/ai/amazon_q/request_payload.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2cf8c05efb6976e4e1e3aa5570e14342c0d72966
--- /dev/null
+++ b/ee/app/models/ai/amazon_q/request_payload.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module Ai
+  module AmazonQ
+    class RequestPayload
+      PayloadGenerationError = Class.new(StandardError)
+
+      def initialize(
+        command:, note:, source:, service_account_notes:, discussion_id:, input:,
+        line_position_for_comment: nil)
+        @command = command
+        @discussion_id = discussion_id
+        @line_position_for_comment = line_position_for_comment
+        @service_account_notes = service_account_notes
+        @source = source
+        @note = note
+        @input = input
+      end
+
+      def payload
+        data = base_payload
+        data.merge!(note_payload)
+
+        if source.is_a?(MergeRequest)
+          data.merge!(merge_request_payload)
+          data.merge!(test_command_payload) if command == 'test'
+        end
+
+        data
+      rescue ArgumentError => e
+        Gitlab::AppLogger.error(message: "[amazon_q] #{e.class}: #{e.message}")
+        raise
+      end
+
+      private
+
+      attr_reader :command, :source, :line_position_for_comment, :service_account_notes, :discussion_id, :note, :input
+
+      def base_payload
+        {
+          command: command,
+          source: source.issuable_type,
+          role_arn: ::Ai::Setting.instance.amazon_q_role_arn,
+          project_path: source.project.full_path,
+          project_id: source.project.id.to_s,
+          "#{source.issuable_type.underscore}_id": source.id.to_s,
+          "#{source.issuable_type.underscore}_iid": source.iid.to_s
+        }
+      end
+
+      def merge_request_payload
+        {
+          source_branch: source.source_branch,
+          target_branch: source.target_branch,
+          last_commit_id: source.recent_commits&.first&.id
+        }
+      end
+
+      def test_command_payload
+        {
+          start_sha: note.position.start_sha,
+          head_sha: note.position.head_sha,
+          file_path: note.position.new_path,
+          user_message: input&.to_s
+        }.merge(line_position_for_comment)
+      end
+
+      def note_payload
+        note_reference = if use_existing_thread?
+                           service_account_notes&.first
+                         else
+                           @progress_note
+                         end
+
+        {
+          note_id: note_reference&.id.to_s,
+          discussion_id: (note_reference&.discussion_id || discussion_id).to_s
+        }
+      end
+
+      def use_existing_thread?
+        %w[dev fix review transform].include?(command)
+      end
+    end
+  end
+end
diff --git a/ee/app/services/ai/amazon_q/amazon_q_trigger_service.rb b/ee/app/services/ai/amazon_q/amazon_q_trigger_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..58021b33fa6931ded06b80e11f7fa7e7d5132de1
--- /dev/null
+++ b/ee/app/services/ai/amazon_q/amazon_q_trigger_service.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+module Ai
+  module AmazonQ
+    class AmazonQTriggerService < BaseService
+      include ::Gitlab::Utils::StrongMemoize
+
+      CloudConnectorTokenError = Class.new(StandardError)
+      ServiceAccountError = Class.new(StandardError)
+      CompositeIdentityEnforcedError = Class.new(StandardError)
+      MissingPrerequisiteError = Class.new(StandardError)
+
+      REVIEW_FINDING_KEYWORDS = ["We detected", "We recommend", "Severity:"].freeze
+
+      def initialize(user:, command:, source:, note: nil, discussion_id: nil, input: nil)
+        @user = user
+        @command = command
+        @input = input
+        @source = source
+        @note = note
+        @discussion_id = discussion_id
+      end
+
+      attr_reader :user, :command, :source, :note, :discussion_id, :input
+
+      def execute
+        validate_cloud_connector_token!
+        validate_service_account!
+        validate_source!
+        validate_command!
+        validate_code_position! if command == 'test'
+
+        add_service_account_to_project
+
+        SystemNoteService.amazon_q_called(source, user, command)
+        create_note_if_needed
+
+        response = make_ai_gateway_request
+        handle_response(response)
+        response
+      rescue StandardError => e
+        Gitlab::AppLogger.error(message: "[amazon_q] Command #{command} encountered #{e.class.name}", error: e.message)
+        handle_note_error(e.message)
+      end
+
+      private
+
+      def make_ai_gateway_request
+        client = Gitlab::Llm::QAi::Client.new(user)
+
+        client.create_event(
+          payload: payload,
+          auth_grant: create_auth_grant_new,
+          role_arn: ai_settings.amazon_q_role_arn
+        )
+      end
+
+      def service_name
+        :amazon_q_integration
+      end
+
+      def cloud_connector_token
+        ::CloudConnector::AvailableServices.find_by_name(service_name).access_token
+      end
+      strong_memoize_attr :cloud_connector_token
+
+      def handle_response(response)
+        return if response.success?
+
+        update_failure_note
+      end
+
+      def payload
+        ::Ai::AmazonQ::RequestPayload.new(
+          command: command,
+          source: source,
+          note: note,
+          service_account_notes: service_account_notes,
+          discussion_id: discussion_id,
+          input: input,
+          line_position_for_comment: line_position_for_comment
+        ).payload
+      end
+      strong_memoize_attr :payload
+
+      def validate_source!
+        Ai::AmazonQValidateCommandSourceService.new(command: command, source: source).validate
+      end
+
+      def validate_cloud_connector_token!
+        return if cloud_connector_token.present?
+
+        raise CloudConnectorTokenError, "Unable to generate valid cloud connector token for #{service_name}"
+      end
+
+      def validate_code_position!
+        position = note&.position
+
+        raise ArgumentError, "Invalid code position" if position.nil?
+        raise ArgumentError, "Invalid code position" if position.start_sha.nil? || position.head_sha.nil?
+        raise ArgumentError, "Unknown code line position" unless line_position_for_comment
+      end
+
+      def use_existing_thread?
+        %w[dev fix review transform].include?(command)
+      end
+
+      def reply_only?
+        %w[fix].include?(command)
+      end
+
+      def create_note_if_needed
+        return if use_existing_thread?
+
+        create_note
+      end
+
+      def line_position_for_comment
+        return unless note.position
+
+        if note.position.line_range.present?
+          {
+            comment_start_line: note.position.line_range.dig("start", "new_line").to_s,
+            comment_end_line: note.position.line_range.dig("end", "new_line").to_s
+          }
+        elsif note.position.new_line.present?
+          {
+            comment_start_line: note.position.new_line.to_s,
+            comment_end_line: note.position.new_line.to_s
+          }
+        end
+      end
+      strong_memoize_attr :line_position_for_comment
+
+      def create_auth_grant_new
+        OauthAccessGrant.create!(
+          resource_owner_id: ai_settings.amazon_q_service_account_user_id,
+          application_id: ai_settings.amazon_q_oauth_application_id,
+          redirect_uri: Gitlab::Routing.url_helpers.root_url,
+          expires_in: 1.hour,
+          scopes: Gitlab::Auth::Q_SCOPES + dynamic_user_scope
+        ).plaintext_token
+      end
+
+      def dynamic_user_scope
+        ["user:#{user.id}"]
+      end
+
+      def create_note
+        @progress_note = ::Ai::AmazonQ::CreateNoteService.new(
+          author: amazon_q_service_account,
+          note: note,
+          source: source,
+          command: command
+        ).execute
+      end
+
+      def update_failure_note
+        if @progress_note.nil?
+          @progress_note = Notes::CreateService.new(
+            source.project,
+            amazon_q_service_account,
+            author: amazon_q_service_account,
+            noteable: source,
+            note: failure_message,
+            discussion_id: note&.discussion_id
+          ).execute
+        else
+          update_note_params = { note: failure_message }
+
+          Notes::UpdateService.new(
+            source.project,
+            amazon_q_service_account,
+            update_note_params
+          ).execute(@progress_note)
+        end
+      end
+
+      def failure_message
+        msg = s_("AmazonQ|Sorry, I'm not able to complete the request at this moment. Please try again later.")
+        request_id = Labkit::Correlation::CorrelationId.current_id
+        msg + format("\n\nRequest ID: %{request_id}", request_id: request_id)
+      end
+
+      def amazon_q_service_account
+        User.find_by_id(ai_settings.amazon_q_service_account_user_id)
+      end
+      strong_memoize_attr :amazon_q_service_account
+
+      def validate_service_account!
+        if amazon_q_service_account.blank?
+          raise ServiceAccountError,
+            "#{command} failed due to Amazon Q service account ID is not configured"
+        elsif !amazon_q_service_account.composite_identity_enforced?
+          raise CompositeIdentityEnforcedError,
+            "Cannot find the service account with composite identity enabled."
+        end
+
+        true
+      end
+
+      def add_service_account_to_project
+        ::Ai::AmazonQ::ServiceAccountMemberAddService.new(source.project).execute
+      end
+
+      def validate_command!
+        # Check the discussions involved in the reply.
+        return true unless reply_only?
+
+        # Filter only notes authored by the Amazon Q service user.
+        # Search for any of the keywords relating to review findings in the note.
+        comments = search_comments_by_service_account(REVIEW_FINDING_KEYWORDS)
+        return true unless comments.blank?
+
+        raise MissingPrerequisiteError,
+          "#{command} can only be executed as a response to the review command"
+      end
+
+      def search_comments_by_service_account(keywords = nil)
+        filtered_notes = service_account_notes
+
+        return filtered_notes if keywords.blank?
+
+        keywords = Array(keywords).map(&:downcase)
+
+        filtered_notes.select do |note|
+          note_content = note.note.to_s.downcase
+          keywords.any? { |keyword| note_content.include?(keyword) }
+        end
+      end
+
+      def handle_note_error(error_message)
+        return unless note
+
+        note.errors.add(:quick_action, "command /q: #{error_message}")
+      end
+
+      def service_account_notes
+        source.notes.authored_by(amazon_q_service_account).find_discussion(discussion_id)&.notes.to_a
+      end
+      strong_memoize_attr :service_account_notes
+
+      def ai_settings
+        Ai::Setting.instance
+      end
+      strong_memoize_attr :ai_settings
+    end
+  end
+end
diff --git a/ee/app/services/ai/amazon_q/create_note_service.rb b/ee/app/services/ai/amazon_q/create_note_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..612d93c89b0bad0acb36d97bff1a3cdcbacf1f16
--- /dev/null
+++ b/ee/app/services/ai/amazon_q/create_note_service.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Ai
+  module AmazonQ
+    class CreateNoteService
+      def initialize(author:, note:, source:, command:)
+        @author = author
+        @note = note
+        @source = source
+        @command = command
+      end
+
+      def execute
+        return unless note
+
+        Notes::UpdateService.new(
+          source.project,
+          author,
+          update_note_params
+        ).execute(new_note)
+      end
+
+      private
+
+      attr_reader :author, :note, :source, :command
+
+      def new_note
+        # preserve attributes needed for diff notes (such as old/new line position)
+        note.dup
+      end
+
+      def update_note_params
+        { note: generate_note_message, author: author }
+      end
+
+      def generate_note_message
+        command_map = case source
+                      when MergeRequest then q_merge_request_sub_commands
+                      when Issue then q_issue_sub_commands
+                      end
+        command_map&.[](command.to_sym)
+      end
+
+      def q_issue_sub_commands
+        {
+          dev: s_("AmazonQ|I'm generating code for this issue. " \
+            "I'll update this comment and open a merge request when I'm done."),
+          transform: s_("AmazonQ|I'm upgrading your code to Java 17. " \
+            "I'll update this comment and open a merge request when I'm done.")
+        }.freeze
+      end
+
+      def q_merge_request_sub_commands
+        {
+          dev: s_("AmazonQ|I'm revising this merge request based on your feedback. " \
+            "I'll update this comment and this merge request when I'm done."),
+          fix: s_("AmazonQ|I'm generating a fix for this review finding. I'll update this comment when I'm done."),
+          test: s_("AmazonQ|:hourglass_flowing_sand: I'm creating unit tests for the selected lines of code. " \
+            "I'll update this comment when I'm done."),
+          review: s_("AmazonQ|I'm reviewing this merge request for security vulnerabilities, " \
+            "quality issues, and deficiencies. I'll provide an update when I'm done.")
+        }.freeze
+      end
+    end
+  end
+end
diff --git a/ee/app/services/ai/amazon_q/service_account_member_add_service.rb b/ee/app/services/ai/amazon_q/service_account_member_add_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7210ddcfa0079029e4fd31aabdd430d3498eb798
--- /dev/null
+++ b/ee/app/services/ai/amazon_q/service_account_member_add_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Ai
+  module AmazonQ
+    class ServiceAccountMemberAddService
+      def initialize(project)
+        @project = project
+      end
+
+      def execute
+        q_user_id = Ai::Setting.instance.amazon_q_service_account_user_id
+
+        existing_member = project.member(q_user_id)
+        return ServiceResponse.success(message: "Membership already exists. Nothing to do.") if existing_member
+
+        existing_user = User.find_by_id(q_user_id)
+        return ServiceResponse.error(message: "Service account user not found") unless existing_user
+
+        result = project.add_developer(existing_user)
+        ServiceResponse.success(payload: result)
+      end
+
+      private
+
+      attr_reader :project
+    end
+  end
+end
diff --git a/ee/app/services/ai/amazon_q_validate_command_source_service.rb b/ee/app/services/ai/amazon_q_validate_command_source_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..73a34e919e4a2a3c56bbc44a6c620d451a8bbbe3
--- /dev/null
+++ b/ee/app/services/ai/amazon_q_validate_command_source_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Ai
+  class AmazonQValidateCommandSourceService
+    UnsupportedCommandError = Class.new(StandardError)
+    UnsupportedSourceError = Class.new(StandardError)
+
+    def initialize(command:, source:)
+      @command = command
+      @source = source
+    end
+
+    def validate
+      case source
+      when Issue
+        command_list = ::Ai::AmazonQ::Commands::ISSUE_SUBCOMMANDS
+        message = "Unsupported issue command: #{command}"
+        raise UnsupportedCommandError, message unless command_list.include?(command)
+      when MergeRequest
+        command_list = ::Ai::AmazonQ::Commands::MERGE_REQUEST_SUBCOMMANDS
+        message = "Unsupported merge request command: #{command}"
+        raise UnsupportedCommandError, message unless command_list.include?(command)
+
+      else
+        raise UnsupportedSourceError, "Unsupported source type: #{source.class}"
+      end
+    end
+
+    private
+
+    attr_reader :command, :source
+  end
+end
diff --git a/ee/lib/ee/gitlab/quick_actions/amazon_q_actions.rb b/ee/lib/ee/gitlab/quick_actions/amazon_q_actions.rb
index e9a25b648ea76f19897d41acc9940496d7e00d85..24c2b818c16fa93e41987848ff8a90a08b2c2d27 100644
--- a/ee/lib/ee/gitlab/quick_actions/amazon_q_actions.rb
+++ b/ee/lib/ee/gitlab/quick_actions/amazon_q_actions.rb
@@ -26,18 +26,12 @@ module AmazonQActions
           end
           command :q do |input = "dev"|
             sub_command, *comment_words = input.strip.split(' ', 2)
-            case quick_action_target
-            when ::Issue
-              unless ::Ai::AmazonQ::Commands::ISSUE_SUBCOMMANDS.include?(sub_command)
-                @execution_message[:q] = "Could not apply Amazon Q command for issue"
-                next
-              end
-            when ::MergeRequest
-              unless ::Ai::AmazonQ::Commands::MERGE_REQUEST_SUBCOMMANDS.include?(sub_command)
-                @execution_message[:q] = "Could not apply Amazon Q command for merge request"
-                next
-              end
-            end
+
+            ::Ai::AmazonQValidateCommandSourceService.new(
+              command: sub_command,
+              source: quick_action_target
+            ).validate
+
             comment = comment_words.join(' ')
             action_data = {
               command: sub_command,
@@ -47,6 +41,8 @@ module AmazonQActions
             }
             action_data[:input] = comment unless comment.empty?
             @updates[:amazon_q] = action_data
+          rescue Ai::AmazonQValidateCommandSourceService::StandardError => error
+            @execution_message[:q] = error.message
           end
         end
       end
diff --git a/ee/lib/gitlab/llm/q_ai/client.rb b/ee/lib/gitlab/llm/q_ai/client.rb
index 69f87247f3febfed4a6dc2cc566b5e8c554b9d6d..2ff1d66ebbda50e881c3456c6870abc8dcdbd2ec 100644
--- a/ee/lib/gitlab/llm/q_ai/client.rb
+++ b/ee/lib/gitlab/llm/q_ai/client.rb
@@ -18,18 +18,31 @@ def perform_create_auth_application(oauth_app, secret, role_arn)
           }
 
           Gitlab::HTTP.post(
-            "#{url}/v1/amazon_q/oauth/application",
+            url(path: "/v1/amazon_q/oauth/application"),
             body: payload.to_json,
             headers: request_headers
           )
         end
 
+        def create_event(payload:, auth_grant:, role_arn:)
+          Gitlab::HTTP.post(
+            url(path: "/v1/amazon_q/events"),
+            body: {
+              payload: payload,
+              code: auth_grant,
+              role_arn: role_arn
+            }.to_json,
+            headers: request_headers
+          )
+        end
+
         private
 
         attr_reader :user
 
-        def url
-          Gitlab::AiGateway.url
+        def url(path:)
+          # use append_path to handle potential trailing slash in AI Gateway URL
+          Gitlab::Utils.append_path(Gitlab::AiGateway.url, path)
         end
 
         def service_name
diff --git a/ee/spec/lib/gitlab/llm/q_ai/client_spec.rb b/ee/spec/lib/gitlab/llm/q_ai/client_spec.rb
index f03b1cb112d85adc05b94d971144ca0e78378e77..62ba734c3fb4b94fa3c1907a54d9bb96f11984b2 100644
--- a/ee/spec/lib/gitlab/llm/q_ai/client_spec.rb
+++ b/ee/spec/lib/gitlab/llm/q_ai/client_spec.rb
@@ -3,16 +3,50 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::Llm::QAi::Client, feature_category: :ai_agents do
-  describe '#perform_create_auth_application' do
-    let_it_be(:user) { create(:user) }
-    let_it_be(:oauth_app) { create(:doorkeeper_application) }
-    let(:service_data) { instance_double(CloudConnector::SelfManaged::AvailableServiceData) }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:oauth_app) { create(:doorkeeper_application) }
+
+  let(:service_data) { instance_double(CloudConnector::SelfManaged::AvailableServiceData) }
+
+  let(:cc_token) { 'cc_token' }
+  let(:response) { 'response' }
+  let(:role_arn) { 'role_arn' }
+  let(:secret) { 'secret' }
+
+  describe '#create_event' do
+    subject(:create_event) do
+      described_class.new(user)
+        .create_event(
+          payload: {},
+          auth_grant: '1234',
+          role_arn: '5678'
+        )
+    end
+
+    before do
+      stub_request(:post, "#{Gitlab::AiGateway.url}/v1/amazon_q/events")
+        .with(body: {
+          payload: {},
+          code: '1234',
+          role_arn: '5678'
+        }.to_json).to_return(body: nil, status: 204)
+    end
+
+    it 'makes expected HTTP post request' do
+      expect(service_data).to receive_messages(
+        name: 'amazon_q_integration',
+        access_token: 'cc_token'
+      )
+      expect(::CloudConnector::AvailableServices).to receive(:find_by_name)
+        .with(:amazon_q_integration).and_return(service_data)
 
-    let(:cc_token) { 'cc_token' }
-    let(:response) { 'response' }
-    let(:role_arn) { 'role_arn' }
-    let(:secret) { 'secret' }
+      response = create_event
+      expect(response.code).to eq(204)
+      expect(response.body).to be_empty
+    end
+  end
 
+  describe '#perform_create_auth_application' do
     subject(:perform_create_auth_application) do
       described_class.new(user)
         .perform_create_auth_application(oauth_app, secret, role_arn)
diff --git a/ee/spec/services/ai/amazon_q/amazon_q_trigger_service_spec.rb b/ee/spec/services/ai/amazon_q/amazon_q_trigger_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e6a2541f3338af554bd6547b856f109390a5c20
--- /dev/null
+++ b/ee/spec/services/ai/amazon_q/amazon_q_trigger_service_spec.rb
@@ -0,0 +1,542 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ai::AmazonQ::AmazonQTriggerService, feature_category: :ai_agents do
+  let_it_be(:default_organization) { create(:organization, :default) }
+  let_it_be_with_reload(:service_account) { create(:user, :service_account, composite_identity_enforced: true) }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project, :repository) }
+  let_it_be(:issue) { create(:issue, project: project) }
+  let_it_be(:merge_request) { create(:merge_request_with_diffs, source_project: project) }
+  let_it_be(:oauth_app) { create(:doorkeeper_application) }
+  let(:response) { instance_double(HTTParty::Response, success?: true, parsed_response: nil) }
+  let(:source) { issue }
+
+  before do
+    Ai::Setting.instance.update!(
+      amazon_q_service_account_user_id: service_account.id,
+      amazon_q_oauth_application_id: oauth_app.id
+    )
+  end
+
+  describe '#execute' do
+    let(:command) { 'dev' }
+    let(:source) { 'issue' }
+    let!(:note) { create(:note_on_issue, noteable: issue, project: project) }
+    let(:service) { described_class.new(user: user, command: command, source: source, note: note) }
+    let(:service_name) { :amazon_q_integration }
+    let(:service_data) do
+      instance_double(CloudConnector::BaseAvailableServiceData,
+        name: service_name,
+        access_token: SecureRandom.hex)
+    end
+
+    let(:expected_payload) { anything }
+    let(:client) { instance_double(Gitlab::Llm::QAi::Client, create_event: response) }
+
+    subject(:execution) { service.execute }
+
+    before do
+      allow(Gitlab::Llm::QAi::Client).to receive(:new).with(user).and_return(client)
+      allow(::CloudConnector::AvailableServices).to receive(:find_by_name).with(service_name).and_return(service_data)
+      allow(SystemNoteService).to receive(:amazon_q_called)
+    end
+
+    context 'with dev command' do
+      let(:command) { 'dev' }
+
+      shared_examples 'successful dev execution' do
+        it 'creates an auth grant with the correct scopes', :aggregate_failures do
+          expect { execution }.to change { OauthAccessGrant.count }.by(1)
+          grant = OauthAccessGrant.find_by(resource_owner: service_account, application: oauth_app)
+          expect(grant.scopes.to_s).to eq("api read_repository write_repository user:#{user.id}")
+        end
+
+        it 'executes successfully' do
+          expect { execution }.not_to change { Note.count }
+          expect(execution.parsed_response).to be_nil
+          expect(SystemNoteService).to have_received(:amazon_q_called).with(source, user, command)
+        end
+      end
+
+      context 'with issue' do
+        let(:source) { issue }
+
+        it_behaves_like 'successful dev execution'
+
+        context 'when server returns a 500 error' do
+          let(:response) { instance_double(HTTParty::Response, success?: false, parsed_response: nil) }
+          let(:client) { instance_double(Gitlab::Llm::QAi::Client, create_event: response) }
+
+          before do
+            allow(Gitlab::Llm::QAi::Client).to receive(:new).with(user).and_return(client)
+          end
+
+          it 'updates a new note with an error' do
+            expect { execution }.to change { Note.count }.by(1)
+            expect(Note.last.note).to include(
+              "Sorry, I'm not able to complete the request at this moment. Please try again later")
+            expect(Note.last.note).to include("Request ID:")
+          end
+        end
+      end
+
+      context 'with merge request' do
+        let(:source) { merge_request }
+
+        it_behaves_like 'successful dev execution'
+      end
+    end
+
+    context 'with fix command' do
+      let_it_be(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+      let(:command) { 'fix' }
+      let(:service) do
+        described_class.new(user: user, command: command, source: source, note: diff_note,
+          discussion_id: diff_note.discussion_id)
+      end
+
+      let(:source) { merge_request }
+
+      context 'when executes fix command after validation' do
+        before do
+          allow(service).to receive(:validate_command!)
+        end
+
+        it 'creates an auth grant' do
+          expect { execution }.to change { OauthAccessGrant.count }.by(1)
+        end
+
+        it 'executes successfully' do
+          expect { execution }.not_to change { Note.count }
+          expect(execution.parsed_response).to be_nil
+          expect(SystemNoteService).to have_received(:amazon_q_called).with(source, user, command)
+        end
+      end
+
+      context 'when executes fix command with review findings' do
+        let(:command) { 'fix' }
+        let!(:diff_note) do
+          create(:diff_note_on_merge_request, noteable: merge_request, project: project, author: service_account,
+            note: 'We recommend to fix this security finding')
+        end
+
+        let!(:service) do
+          described_class.new(user: user, command: command, source: source, note: diff_note,
+            discussion_id: diff_note.discussion_id)
+        end
+
+        it 'executes successfully' do
+          expect { service.execute }.not_to raise_error
+        end
+      end
+    end
+
+    context 'with test command' do
+      let_it_be(:diff_note) { build(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+      let(:command) { 'test' }
+      let(:service) do
+        described_class.new(user: user, command: command, source: source, note: diff_note,
+          discussion_id: diff_note.discussion_id)
+      end
+
+      let(:source) { merge_request }
+
+      let(:expected_payload) do
+        hash_including(
+          'command' => 'test',
+          'source' => 'merge_request',
+          'merge_request_id' => merge_request.id.to_s,
+          'merge_request_iid' => merge_request.iid.to_s,
+          'note_id' => anything,
+          'discussion_id' => diff_note.discussion_id,
+          'source_branch' => merge_request.source_branch,
+          'target_branch' => merge_request.target_branch,
+          'last_commit_id' => merge_request.recent_commits.first.id,
+          'comment_start_line' => diff_note.position.new_line.to_s,
+          'comment_end_line' => diff_note.position.new_line.to_s,
+          'start_sha' => diff_note.position.start_sha,
+          'head_sha' => diff_note.position.head_sha,
+          'file_path' => diff_note.position.new_path,
+          'user_message' => anything
+        )
+      end
+
+      it 'creates an auth grant' do
+        expect { execution }.to change { OauthAccessGrant.count }.by(1)
+      end
+
+      it 'executes successfully with the right payload' do
+        expect { execution }.to change { Note.count }.by(1)
+        expect(execution.parsed_response).to be_nil
+      end
+    end
+
+    context 'when handle error' do
+      let(:command) { 'unsupported' }
+      let(:source) { issue }
+      let!(:service) do
+        described_class.new(user: user, command: command, source: source, note: note,
+          discussion_id: note.discussion_id)
+      end
+
+      before do
+        allow(service).to receive(:handle_note_error)
+        allow(Gitlab::AppLogger).to receive(:error)
+      end
+
+      context 'when UnsupportedCommandError is raised' do
+        let(:error_message) { "Unsupported issue command: #{command}" }
+        let(:error) do
+          Ai::AmazonQValidateCommandSourceService::UnsupportedCommandError.new("Unsupported issue command: #{command}")
+        end
+
+        before do
+          allow(service).to receive(:validate_source!).and_raise(error)
+        end
+
+        it 'logs the error and handles the note error' do
+          expect(Gitlab::AppLogger).to receive(:error).with(
+            message: "[amazon_q] Command #{command} encountered #{error.class.name}",
+            error: error_message
+          )
+          expect(service).to receive(:handle_note_error).with(error_message)
+
+          expect { execution }.not_to raise_error
+        end
+      end
+    end
+
+    context 'when unable to generate a cloud connector access token' do
+      let(:service_data) do
+        instance_double(CloudConnector::BaseAvailableServiceData,
+          name: service_name,
+          access_token: nil)
+      end
+
+      it 'raises CloudConnectorTokenError wth expected message' do
+        expect do
+          service.send(:validate_cloud_connector_token!)
+        end.to raise_error(described_class::CloudConnectorTokenError).with_message(
+          'Unable to generate valid cloud connector token for amazon_q_integration'
+        )
+      end
+    end
+
+    describe '#payload' do
+      let(:note) { create(:note_on_issue, noteable: issue, project: project) }
+      let(:service) { described_class.new(user: user, command: 'dev', source: issue, note: note) }
+
+      it 'generates the correct payload for an issue' do
+        service.execute
+
+        payload = service.send(:payload)
+
+        expect(payload.keys).to match_array(
+          %i[command source project_path project_id issue_id issue_iid discussion_id note_id role_arn])
+
+        expect(payload[:command]).to eq('dev')
+        expect(payload[:source]).to eq('issue')
+        expect(payload[:project_path]).to eq(project.full_path)
+        expect(payload[:project_id]).to eq(project.id.to_s)
+        expect(payload[:issue_id]).to eq(issue.id.to_s)
+        expect(payload[:issue_iid]).to eq(issue.iid.to_s)
+      end
+    end
+  end
+
+  describe '#amazon_q_service_account' do
+    let(:service) { described_class.new(user: user, command: 'dev', source: issue) }
+
+    context 'when service account exists' do
+      before do
+        Ai::Setting.instance.update!(amazon_q_service_account_user_id: service_account.id)
+      end
+
+      it 'returns service account user object' do
+        service_account_user = service.send(:amazon_q_service_account)
+
+        expect(service_account_user).to eq(service_account)
+      end
+    end
+  end
+
+  describe '#validate_service_account!' do
+    let(:service) { described_class.new(user: user, command: 'dev', source: issue) }
+
+    context 'when service account is properly configured' do
+      before do
+        Ai::Setting.instance.update!(amazon_q_service_account_user_id: service_account.id)
+      end
+
+      it 'returns true' do
+        expect(service.send(:validate_service_account!)).to be true
+      end
+    end
+
+    context 'when service account does not exist' do
+      before do
+        Ai::Setting.instance.update!(amazon_q_service_account_user_id: nil)
+      end
+
+      it 'raises ServiceAccountError' do
+        expect { service.send(:validate_service_account!) }
+          .to raise_error(
+            Ai::AmazonQ::AmazonQTriggerService::ServiceAccountError,
+            'dev failed due to Amazon Q service account ID is not configured'
+          )
+      end
+    end
+
+    context 'when service account does not have composite identity enabled' do
+      before do
+        service_account.update!(composite_identity_enforced: false)
+      end
+
+      it 'raises CompositeIdentityEnforcedError' do
+        expect { service.send(:validate_service_account!) }.to raise_error(
+          Ai::AmazonQ::AmazonQTriggerService::CompositeIdentityEnforcedError,
+          "Cannot find the service account with composite identity enabled."
+        )
+      end
+    end
+  end
+
+  describe '#validate_command!' do
+    let(:note) { create(:note_on_issue, noteable: issue, project: project) }
+    let(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project).discussion }
+    let(:service) { described_class.new(user: user, command: 'dev', source: issue, note: discussion.notes.first) }
+
+    context 'when not using an existing thread' do
+      it 'returns true' do
+        expect(service.send(:validate_command!)).to be_truthy
+      end
+    end
+
+    context 'when using an existing thread' do
+      let!(:service) { described_class.new(user: user, command: 'fix', source: issue, note: discussion.notes.first) }
+
+      context 'when there are no comments from the service account' do
+        before do
+          allow(service).to receive(:search_comments_by_service_account).and_return(nil)
+        end
+
+        it 'raises a MissingPrerequisiteError' do
+          expect { service.send(:validate_command!) }.to raise_error(
+            Ai::AmazonQ::AmazonQTriggerService::MissingPrerequisiteError,
+            "fix can only be executed as a response to the review command"
+          )
+        end
+      end
+
+      context 'when there are comments from the service account' do
+        before do
+          allow(service).to receive(:search_comments_by_service_account).and_return(note)
+        end
+
+        it 'returns true' do
+          expect(service.send(:validate_command!)).to be_truthy
+        end
+      end
+    end
+  end
+
+  describe '#search_comments_by_service_account' do
+    let(:discussion) do
+      create(:discussion_note_on_issue, noteable: issue, project: issue.project, note: "test note",
+        author: service_account).discussion
+    end
+
+    let(:service) do
+      described_class.new(user: user, command: 'dev', source: issue, note: discussion.notes.first,
+        discussion_id: discussion.notes.first.discussion_id)
+    end
+
+    it 'filters notes by author_id' do
+      expect(service.send(:search_comments_by_service_account).count).to eq(1)
+    end
+
+    context 'when keyword is blank' do
+      it 'returns notes filtered by author_id' do
+        expect(service.send(:search_comments_by_service_account, []).count).to eq(1)
+      end
+    end
+
+    context 'when keyword is present' do
+      it 'filters notes by keyword' do
+        expect(service.send(:search_comments_by_service_account, ['test']).count).to eq(1)
+      end
+
+      it 'returns empty array when not found keyword' do
+        expect(service.send(:search_comments_by_service_account, ['unknown'])).to be_empty
+      end
+    end
+  end
+
+  describe '#handle_note_error' do
+    let!(:note) { create(:note_on_issue, noteable: issue, project: project) }
+    let(:error_message) { "Test error message" }
+
+    context 'when note is already defined' do
+      let(:service) { described_class.new(user: user, command: 'dev', source: issue, note: note) }
+
+      before do
+        allow(service).to receive(:note).and_return(note)
+      end
+
+      it 'adds error to the existing note' do
+        error = service.send(:handle_note_error, error_message)
+        expect(error.type).to eq("command /q: #{error_message}")
+      end
+    end
+  end
+
+  describe '#create_note' do
+    let(:original_note) { create(:note_on_issue, noteable: issue, project: project) }
+    let(:command) { 'dev' }
+    let(:generated_message) do
+      "I'm generating code for this issue. I'll update this comment and open a merge request when I'm done."
+    end
+
+    let(:service) { described_class.new(user: user, command: command, source: source, note: original_note) }
+    let(:update_service) { instance_double(Notes::UpdateService) }
+
+    before do
+      allow(service).to receive_messages(
+        amazon_q_service_account: service_account
+      )
+    end
+
+    context 'when note exists' do
+      before do
+        allow(Notes::UpdateService).to receive(:new)
+          .with(project, service_account, { note: generated_message, author: service_account })
+          .and_return(update_service)
+        allow(update_service).to receive(:execute).and_return(original_note)
+      end
+
+      it 'creates a new note with correct parameters' do
+        service.send(:create_note)
+
+        expect(Notes::UpdateService).to have_received(:new)
+          .with(project, service_account, { note: generated_message, author: service_account })
+        expect(update_service).to have_received(:execute).with(kind_of(Note))
+      end
+
+      it 'sets the progress note' do
+        service.send(:create_note)
+
+        expect(service.instance_variable_get(:@progress_note)).to eq(original_note)
+      end
+    end
+
+    context 'when note does not exist' do
+      before do
+        allow(service).to receive(:note).and_return(nil)
+      end
+
+      it 'returns nil without creating a note' do
+        expect(Notes::UpdateService).not_to receive(:new)
+
+        expect(service.send(:create_note)).to be_nil
+      end
+    end
+  end
+
+  describe '#update_failure_note' do
+    let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project) }
+
+    let(:service) { described_class.new(user: user, command: 'dev', source: issue, note: note) }
+    let(:failure_message) { 'Error occurred' }
+
+    before do
+      allow(service).to receive(:failure_message).and_return(failure_message)
+    end
+
+    context 'when progress note does not exist' do
+      let(:create_service) { instance_double(Notes::CreateService) }
+      let(:created_note) { create(:note) }
+
+      before do
+        service.instance_variable_set(:@progress_note, nil)
+
+        allow(Notes::CreateService).to receive(:new)
+          .with(
+            issue.project,
+            service_account,
+            {
+              author: service_account,
+              noteable: issue,
+              note: failure_message,
+              discussion_id: note.discussion_id
+            }
+          ).and_return(create_service)
+        allow(create_service).to receive(:execute).and_return(created_note)
+      end
+
+      it 'creates a new note with failure message' do
+        service.send(:update_failure_note)
+
+        expect(Notes::CreateService).to have_received(:new)
+          .with(
+            issue.project,
+            service_account,
+            {
+              author: service_account,
+              noteable: issue,
+              note: failure_message,
+              discussion_id: note.discussion_id
+            }
+          )
+        expect(create_service).to have_received(:execute)
+      end
+
+      it 'sets the progress note to the newly created note' do
+        service.send(:update_failure_note)
+
+        expect(service.instance_variable_get(:@progress_note)).to eq(created_note)
+      end
+    end
+
+    context 'when note is nil' do
+      let(:service) { described_class.new(user: user, command: 'dev', source: issue, note: nil) }
+      let(:create_service) { instance_double(Notes::CreateService) }
+      let(:created_note) { create(:note) }
+
+      before do
+        service.instance_variable_set(:@progress_note, nil)
+
+        allow(Notes::CreateService).to receive(:new)
+          .with(
+            issue.project,
+            service_account,
+            {
+              author: service_account,
+              noteable: issue,
+              note: failure_message,
+              discussion_id: nil
+            }
+          ).and_return(create_service)
+        allow(create_service).to receive(:execute).and_return(created_note)
+      end
+
+      it 'creates a new note without discussion_id' do
+        service.send(:update_failure_note)
+
+        expect(Notes::CreateService).to have_received(:new)
+          .with(
+            issue.project,
+            service_account,
+            {
+              author: service_account,
+              noteable: issue,
+              note: failure_message,
+              discussion_id: nil
+            }
+          )
+        expect(create_service).to have_received(:execute)
+      end
+    end
+  end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b4c72a11a78b5167aa0705ceac8d7e9ec6c1f61b..de5f3ac1f143c62b01a278284c49787863a8a47d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5920,6 +5920,9 @@ msgstr ""
 msgid "Amazon Q"
 msgstr ""
 
+msgid "AmazonQ|:hourglass_flowing_sand: I'm creating unit tests for the selected lines of code. I'll update this comment when I'm done."
+msgstr ""
+
 msgid "AmazonQ|Active cloud connector token not found."
 msgstr ""
 
@@ -6001,6 +6004,21 @@ msgstr ""
 msgid "AmazonQ|I understand that by selecting Save changes, GitLab creates a service account for Amazon Q and sends its credentials to AWS. Use of the Amazon Q Developer capabilities as part of GitLab Duo with Amazon Q is governed by the %{helpStart}AWS Customer Agreement%{helpEnd} or other written agreement between you and AWS governing your use of AWS services."
 msgstr ""
 
+msgid "AmazonQ|I'm generating a fix for this review finding. I'll update this comment when I'm done."
+msgstr ""
+
+msgid "AmazonQ|I'm generating code for this issue. I'll update this comment and open a merge request when I'm done."
+msgstr ""
+
+msgid "AmazonQ|I'm reviewing this merge request for security vulnerabilities, quality issues, and deficiencies. I'll provide an update when I'm done."
+msgstr ""
+
+msgid "AmazonQ|I'm revising this merge request based on your feedback. I'll update this comment and this merge request when I'm done."
+msgstr ""
+
+msgid "AmazonQ|I'm upgrading your code to Java 17. I'll update this comment and open a merge request when I'm done."
+msgstr ""
+
 msgid "AmazonQ|IAM role's ARN"
 msgstr ""
 
@@ -6034,6 +6052,9 @@ msgstr ""
 msgid "AmazonQ|Something went wrong saving Amazon Q settings."
 msgstr ""
 
+msgid "AmazonQ|Sorry, I'm not able to complete the request at this moment. Please try again later."
+msgstr ""
+
 msgid "AmazonQ|Status"
 msgstr ""