diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue
index 5ed2fc7022b7fd0da90ca7f8692bd4df07b37c0e..a6b083fbb30fbdd2f27379a29bcdfc4b758c84a4 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_button.vue
@@ -7,6 +7,7 @@ import cancelJobMutation from './graphql/mutations/job_cancel.mutation.graphql';
 import playJobMutation from './graphql/mutations/job_play.mutation.graphql';
 import retryJobMutation from './graphql/mutations/job_retry.mutation.graphql';
 import unscheduleJobMutation from './graphql/mutations/job_unschedule.mutation.graphql';
+import JobActionModal from './job_action_modal.vue';
 
 export const i18n = {
   errors: {
@@ -46,6 +47,7 @@ export default {
     GlButton,
     GlIcon,
     GlLoadingIcon,
+    JobActionModal,
   },
   directives: {
     GlTooltip: GlTooltipDirective,
@@ -74,10 +76,17 @@ export default {
     actionType() {
       return this.jobAction.icon;
     },
+    hasConfirmationModal() {
+      return this.jobAction?.confirmationMessage !== null;
+    },
   },
   methods: {
     onActionButtonClick() {
-      this.executeJobAction();
+      if (this.hasConfirmationModal) {
+        this.showConfirmationModal = true;
+      } else {
+        this.executeJobAction();
+      }
     },
     async executeJobAction() {
       try {
@@ -117,5 +126,12 @@ export default {
       <gl-loading-icon v-if="isLoading" size="sm" class="gl-m-2" />
       <gl-icon v-else :name="jobAction.icon" :size="12" />
     </gl-button>
+    <job-action-modal
+      v-if="hasConfirmationModal"
+      v-model="showConfirmationModal"
+      :job-name="jobName"
+      :custom-message="jobAction.confirmationMessage"
+      @confirm="executeJobAction"
+    />
   </div>
 </template>
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/job_action_modal.vue b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_modal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4efcd91e6102017b85748db71f5cb4eb62c7fec7
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/job_action_modal.vue
@@ -0,0 +1,69 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+
+export default {
+  name: 'JobActionModal',
+  i18n: {
+    title: s__('PipelineGraph|Are you sure you want to run %{jobName}?'),
+    confirmationText: s__('PipelineGraph|Do you want to continue?'),
+    actionCancel: { text: __('Cancel') },
+  },
+  components: {
+    GlModal,
+  },
+  model: {
+    prop: 'visible',
+    event: 'change',
+  },
+  props: {
+    customMessage: {
+      type: String,
+      required: true,
+    },
+    visible: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    jobName: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    modalText() {
+      return {
+        confirmButton: {
+          text: sprintf(__('Yes, run %{jobName}'), {
+            jobName: this.jobName,
+          }),
+        },
+        message: sprintf(__('Custom confirmation message: %{message}'), {
+          message: this.customMessage,
+        }),
+        title: sprintf(this.$options.i18n.title, {
+          jobName: this.jobName,
+        }),
+      };
+    },
+  },
+};
+</script>
+
+<template>
+  <gl-modal
+    modal-id="job-action-modal"
+    :action-cancel="$options.i18n.actionCancel"
+    :action-primary="modalText.confirmButton"
+    :title="modalText.title"
+    :visible="visible"
+    @primary="$emit('confirm')"
+    @change="$emit('change', $event)"
+  >
+    <div>
+      <p>{{ modalText.message }}</p>
+      <span>{{ $options.i18n.confirmationText }}</span>
+    </div>
+  </gl-modal>
+</template>
diff --git a/spec/frontend/ci/pipeline_mini_graph/job_action_modal_spec.js b/spec/frontend/ci/pipeline_mini_graph/job_action_modal_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7464a00ea485afbff4062d9baf3e7626fe8fee3
--- /dev/null
+++ b/spec/frontend/ci/pipeline_mini_graph/job_action_modal_spec.js
@@ -0,0 +1,58 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import JobActionModal from '~/ci/pipeline_mini_graph/job_action_modal.vue';
+
+describe('JobActionModal', () => {
+  let wrapper;
+
+  const defaultProps = {
+    customMessage: 'This is a custom message.',
+    jobName: 'test_job',
+  };
+
+  const createComponent = ({ props = {} } = {}) => {
+    wrapper = shallowMount(JobActionModal, {
+      propsData: {
+        ...defaultProps,
+        ...props,
+      },
+      stubs: {
+        GlSprintf,
+      },
+    });
+  };
+
+  const findModal = () => wrapper.findComponent(GlModal);
+
+  beforeEach(() => {
+    createComponent();
+  });
+
+  it('shows modal', () => {
+    expect(findModal().props()).toMatchObject({
+      actionCancel: { text: 'Cancel' },
+      actionPrimary: { text: 'Yes, run test_job' },
+      modalId: 'job-action-modal',
+      title: 'Are you sure you want to run test_job?',
+    });
+  });
+
+  it('displays the custom message', () => {
+    expect(findModal().text()).toContain(defaultProps.customMessage);
+  });
+
+  it('emits change event when modal visibility changes', async () => {
+    await findModal().vm.$emit('change', true);
+    expect(wrapper.emitted('change')).toEqual([[true]]);
+  });
+
+  it('passes visible prop to gl-modal', () => {
+    createComponent({
+      props: {
+        visible: true,
+      },
+    });
+
+    expect(findModal().props('visible')).toBe(true);
+  });
+});