diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 31500b919f3b66f7573b029a9a14226fa621ff64..d84a9a4a4b57cad5c77002c62bc2963a13e94e5c 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -8,17 +8,22 @@ import {
   GlFormGroup,
   GlFormInput,
   GlFormTextarea,
+  GlLoadingIcon,
 } from '@gitlab/ui';
 import { __, s__ } from '~/locale';
 import { createAlert } from '~/alert';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { visitUrl, queryToObject } from '~/lib/utils/url_utility';
 import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
 import RefSelector from '~/ref/components/ref_selector.vue';
 import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
 import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
 import createPipelineScheduleMutation from '../graphql/mutations/create_pipeline_schedule.mutation.graphql';
+import updatePipelineScheduleMutation from '../graphql/mutations/update_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
 import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
 
+const scheduleId = queryToObject(window.location.search).id;
+
 export default {
   components: {
     GlButton,
@@ -29,20 +34,12 @@ export default {
     GlFormGroup,
     GlFormInput,
     GlFormTextarea,
+    GlLoadingIcon,
     RefSelector,
     TimezoneDropdown,
     IntervalPatternInput,
   },
-  inject: [
-    'fullPath',
-    'projectId',
-    'defaultBranch',
-    'cron',
-    'cronTimezone',
-    'dailyLimit',
-    'settingsLink',
-    'schedulesPath',
-  ],
+  inject: ['fullPath', 'projectId', 'defaultBranch', 'dailyLimit', 'settingsLink', 'schedulesPath'],
   props: {
     timezoneData: {
       type: Array,
@@ -58,24 +55,74 @@ export default {
       required: true,
     },
   },
+  apollo: {
+    schedule: {
+      query: getPipelineSchedulesQuery,
+      variables() {
+        return {
+          projectPath: this.fullPath,
+          ids: scheduleId,
+        };
+      },
+      update(data) {
+        return data.project?.pipelineSchedules?.nodes[0] || {};
+      },
+      result({ data }) {
+        if (data) {
+          const {
+            project: {
+              pipelineSchedules: { nodes },
+            },
+          } = data;
+
+          const schedule = nodes[0];
+          const variables = schedule.variables?.nodes || [];
+
+          this.description = schedule.description;
+          this.cron = schedule.cron;
+          this.cronTimezone = schedule.cronTimezone;
+          this.scheduleRef = schedule.ref;
+          this.variables = variables.map((variable) => {
+            return {
+              id: variable.id,
+              variableType: variable.variableType,
+              key: variable.key,
+              value: variable.value,
+              destroy: false,
+            };
+          });
+          this.addEmptyVariable();
+          this.activated = schedule.active;
+        }
+      },
+      skip() {
+        return !this.editing;
+      },
+      error() {
+        createAlert({ message: this.$options.i18n.scheduleFetchError });
+      },
+    },
+  },
   data() {
     return {
-      cronValue: this.cron,
+      cron: '',
       description: '',
       scheduleRef: this.defaultBranch,
       activated: true,
-      timezone: this.cronTimezone,
+      cronTimezone: '',
       variables: [],
+      schedule: {},
     };
   },
   i18n: {
     activated: __('Activated'),
-    cronTimezone: s__('PipelineSchedules|Cron timezone'),
+    cronTimezoneText: s__('PipelineSchedules|Cron timezone'),
     description: s__('PipelineSchedules|Description'),
     shortDescriptionPipeline: s__(
       'PipelineSchedules|Provide a short description for this pipeline',
     ),
-    savePipelineSchedule: s__('PipelineSchedules|Save pipeline schedule'),
+    editScheduleBtnText: s__('PipelineSchedules|Edit pipeline schedule'),
+    createScheduleBtnText: s__('PipelineSchedules|Create pipeline schedule'),
     cancel: __('Cancel'),
     targetBranchTag: __('Select target branch or tag'),
     intervalPattern: s__('PipelineSchedules|Interval Pattern'),
@@ -87,6 +134,12 @@ export default {
     scheduleCreateError: s__(
       'PipelineSchedules|An error occurred while creating the pipeline schedule.',
     ),
+    scheduleUpdateError: s__(
+      'PipelineSchedules|An error occurred while updating the pipeline schedule.',
+    ),
+    scheduleFetchError: s__(
+      'PipelineSchedules|An error occurred while trying to fetch the pipeline schedule.',
+    ),
   },
   typeOptions: {
     [VARIABLE_TYPE]: __('Variable'),
@@ -114,9 +167,26 @@ export default {
     getEnabledRefTypes() {
       return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
     },
-    preparedVariables() {
+    preparedVariablesUpdate() {
       return this.variables.filter((variable) => variable.key !== '');
     },
+    preparedVariablesCreate() {
+      return this.preparedVariablesUpdate.map((variable) => {
+        return {
+          key: variable.key,
+          value: variable.value,
+          variableType: variable.variableType,
+        };
+      });
+    },
+    loading() {
+      return this.$apollo.queries.schedule.loading;
+    },
+    buttonText() {
+      return this.editing
+        ? this.$options.i18n.editScheduleBtnText
+        : this.$options.i18n.createScheduleBtnText;
+    },
   },
   created() {
     this.addEmptyVariable();
@@ -133,6 +203,7 @@ export default {
         variableType: VARIABLE_TYPE,
         key: '',
         value: '',
+        destroy: false,
       });
     },
     setVariableAttribute(key, attribute, value) {
@@ -140,16 +211,11 @@ export default {
       variable[attribute] = value;
     },
     removeVariable(index) {
-      this.variables.splice(index, 1);
+      this.variables[index].destroy = true;
     },
     canRemove(index) {
       return index < this.variables.length - 1;
     },
-    scheduleHandler() {
-      if (!this.editing) {
-        this.createPipelineSchedule();
-      }
-    },
     async createPipelineSchedule() {
       try {
         const {
@@ -161,10 +227,10 @@ export default {
           variables: {
             input: {
               description: this.description,
-              cron: this.cronValue,
-              cronTimezone: this.timezone,
+              cron: this.cron,
+              cronTimezone: this.cronTimezone,
               ref: this.scheduleRef,
-              variables: this.preparedVariables,
+              variables: this.preparedVariablesCreate,
               active: this.activated,
               projectPath: this.fullPath,
             },
@@ -180,11 +246,48 @@ export default {
         createAlert({ message: this.$options.i18n.scheduleCreateError });
       }
     },
+    async updatePipelineSchedule() {
+      try {
+        const {
+          data: {
+            pipelineScheduleUpdate: { errors },
+          },
+        } = await this.$apollo.mutate({
+          mutation: updatePipelineScheduleMutation,
+          variables: {
+            input: {
+              id: this.schedule.id,
+              description: this.description,
+              cron: this.cron,
+              cronTimezone: this.cronTimezone,
+              ref: this.scheduleRef,
+              variables: this.preparedVariablesUpdate,
+              active: this.activated,
+            },
+          },
+        });
+
+        if (errors.length > 0) {
+          createAlert({ message: errors[0] });
+        } else {
+          visitUrl(this.schedulesPath);
+        }
+      } catch {
+        createAlert({ message: this.$options.i18n.scheduleUpdateError });
+      }
+    },
+    scheduleHandler() {
+      if (this.editing) {
+        this.updatePipelineSchedule();
+      } else {
+        this.createPipelineSchedule();
+      }
+    },
     setCronValue(cron) {
-      this.cronValue = cron;
+      this.cron = cron;
     },
     setTimezone(timezone) {
-      this.timezone = timezone.identifier || '';
+      this.cronTimezone = timezone.identifier || '';
     },
   },
 };
@@ -192,7 +295,8 @@ export default {
 
 <template>
   <div class="col-lg-8 gl-pl-0">
-    <gl-form>
+    <gl-loading-icon v-if="loading && editing" size="lg" />
+    <gl-form v-else>
       <!--Description-->
       <gl-form-group :label="$options.i18n.description" label-for="schedule-description">
         <gl-form-input
@@ -215,10 +319,10 @@ export default {
         />
       </gl-form-group>
       <!--Timezone-->
-      <gl-form-group :label="$options.i18n.cronTimezone" label-for="schedule-timezone">
+      <gl-form-group :label="$options.i18n.cronTimezoneText" label-for="schedule-timezone">
         <timezone-dropdown
           id="schedule-timezone"
-          :value="timezone"
+          :value="cronTimezone"
           :timezone-data="timezoneData"
           name="schedule-timezone"
           @input="setTimezone"
@@ -242,12 +346,12 @@ export default {
         <div
           v-for="(variable, index) in variables"
           :key="`var-${index}`"
-          class="gl-mb-3 gl-pb-2"
-          data-testid="ci-variable-row"
           data-qa-selector="ci_variable_row_container"
         >
           <div
-            class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
+            v-if="!variable.destroy"
+            class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
+            data-testid="ci-variable-row"
           >
             <gl-dropdown
               :text="$options.typeOptions[variable.variableType]"
@@ -308,7 +412,7 @@ export default {
       </gl-form-checkbox>
 
       <gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler">
-        {{ $options.i18n.savePipelineSchedule }}
+        {{ buttonText }}
       </gl-button>
       <gl-button :href="schedulesPath" data-testid="schedule-cancel-button">
         {{ $options.i18n.cancel }}
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..a6a937af74af70b706b5464c1bcd0bc3143b09ff
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql
@@ -0,0 +1,6 @@
+mutation updatePipelineSchedule($input: PipelineScheduleUpdateInput!) {
+  pipelineScheduleUpdate(input: $input) {
+    clientMutationId
+    errors
+  }
+}
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
index 0e091afb9d71cefe7bb7bd529c24b1291bf09ace..29a26be034473aa374457ea44dd10297b3895351 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -1,15 +1,22 @@
-query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStatus) {
+query getPipelineSchedulesQuery(
+  $projectPath: ID!
+  $status: PipelineScheduleStatus
+  $ids: [ID!] = null
+) {
   currentUser {
     id
     username
   }
   project(fullPath: $projectPath) {
     id
-    pipelineSchedules(status: $status) {
+    pipelineSchedules(status: $status, ids: $ids) {
       count
       nodes {
         id
         description
+        cron
+        cronTimezone
+        ref
         forTag
         editPath
         refPath
@@ -35,6 +42,14 @@ query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStat
           name
           webPath
         }
+        variables {
+          nodes {
+            id
+            variableType
+            key
+            value
+          }
+        }
         userPermissions {
           playPipelineSchedule
           updatePipelineSchedule
diff --git a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
index 749e3d0a69fc20a48c2a90460dab88f95816c435..6bf121d39b6940236c390472efe0dfca0050306b 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
@@ -18,10 +18,8 @@ export default (selector, editing = false) => {
 
   const {
     fullPath,
-    cron,
     dailyLimit,
     timezoneData,
-    cronTimezone,
     projectId,
     defaultBranch,
     settingsLink,
@@ -37,8 +35,6 @@ export default (selector, editing = false) => {
       projectId,
       defaultBranch,
       dailyLimit: dailyLimit ?? '',
-      cronTimezone: cronTimezone ?? '',
-      cron: cron ?? '',
       settingsLink,
       schedulesPath,
     },
diff --git a/app/graphql/mutations/ci/pipeline_schedule/update.rb b/app/graphql/mutations/ci/pipeline_schedule/update.rb
index a0b5e793ecbf73e5f82d250e7371572986c26ef8..aff0a5494e722bdc8b33c5abfdcb2b542e942a87 100644
--- a/app/graphql/mutations/ci/pipeline_schedule/update.rb
+++ b/app/graphql/mutations/ci/pipeline_schedule/update.rb
@@ -43,7 +43,7 @@ class Update < Base
         def resolve(id:, variables: [], **pipeline_schedule_attrs)
           schedule = authorized_find!(id: id)
 
-          params = pipeline_schedule_attrs.merge(variables_attributes: variables.map(&:to_h))
+          params = pipeline_schedule_attrs.merge(variables_attributes: variable_attributes_for(variables))
 
           service_response = ::Ci::PipelineSchedules::UpdateService
             .new(schedule, current_user, params)
@@ -54,6 +54,18 @@ def resolve(id:, variables: [], **pipeline_schedule_attrs)
             errors: service_response.errors
           }
         end
+
+        private
+
+        def variable_attributes_for(variables)
+          variables.map do |variable|
+            variable.to_h.tap do |hash|
+              hash[:id] = GlobalID::Locator.locate(hash[:id]).id if hash[:id]
+
+              hash[:_destroy] = hash.delete(:destroy)
+            end
+          end
+        end
       end
     end
   end
diff --git a/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb
index 54a6ad92448dd49377228306c674abfb0d5dc8af..eb6a78eb67a131fb4ae7ff5213210f33b1f9d6e6 100644
--- a/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb
+++ b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb
@@ -8,11 +8,18 @@ class VariableInputType < Types::BaseInputObject
 
         description 'Attributes for the pipeline schedule variable.'
 
+        PipelineScheduleVariableID = ::Types::GlobalIDType[::Ci::PipelineScheduleVariable]
+
+        argument :id, PipelineScheduleVariableID, required: false, description: 'ID of the variable to mutate.'
+
         argument :key, GraphQL::Types::String, required: true, description: 'Name of the variable.'
 
         argument :value, GraphQL::Types::String, required: true, description: 'Value of the variable.'
 
         argument :variable_type, Types::Ci::VariableTypeEnum, required: true, description: 'Type of the variable.'
+
+        argument :destroy, GraphQL::Types::Boolean, required: false,
+          description: 'Boolean option to destroy the variable.'
       end
     end
   end
diff --git a/app/helpers/ci/pipeline_schedules_helper.rb b/app/helpers/ci/pipeline_schedules_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e5125353b993906c5e7775d8124828263ec37c7e
--- /dev/null
+++ b/app/helpers/ci/pipeline_schedules_helper.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Ci
+  module PipelineSchedulesHelper
+    def js_pipeline_schedules_form_data(project, schedule)
+      {
+        full_path: project.full_path,
+        daily_limit: schedule.daily_limit,
+        timezone_data: timezone_data.to_json,
+        project_id: project.id,
+        default_branch: project.default_branch,
+        settings_link: project_settings_ci_cd_path(project),
+        schedules_path: pipeline_schedules_path(project)
+      }
+    end
+  end
+end
+
+Ci::PipelineSchedulesHelper.prepend_mod_with('Ci::PipelineSchedulesHelper')
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index 3f843ce6aec21e6a856d86ba3b9aa1f792482bcc..4e1ae53a101a6d5c86371a86e22deb0a86832bc3 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -5,9 +5,8 @@
 
 %h1.page-title.gl-font-size-h-display
   = _("Edit Pipeline Schedule")
-%hr
 
 - if Feature.enabled?(:pipeline_schedules_vue, @project)
-  #pipeline-schedules-form-edit{ data: { full_path: @project.full_path } }
+  #pipeline-schedules-form-edit{ data: js_pipeline_schedules_form_data(@project, @schedule) }
 - else
   = render "form"
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index 89836e6d09121f312727bb031a46a327215d9637..ef99a79b06fe22b541df3432c703271940cb69e2 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -9,6 +9,6 @@
   = _("Schedule a new pipeline")
 
 - if Feature.enabled?(:pipeline_schedules_vue, @project)
-  #pipeline-schedules-form-new{ data: { full_path: @project.full_path, cron: @schedule.cron, daily_limit: @schedule.daily_limit, timezone_data: timezone_data.to_json, cron_timezone: @schedule.cron_timezone, project_id: @project.id, default_branch: @project.default_branch, settings_link: project_settings_ci_cd_path(@project), schedules_path: pipeline_schedules_path(@project) } }
+  #pipeline-schedules-form-new{ data: js_pipeline_schedules_form_data(@project, @schedule) }
 - else
   = render "form"
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d4ada9791da572e0ee8b92663b8ac0dc424c71e3..0979185def3a6b062c31c4215db1db620cc7cc0d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -27110,6 +27110,12 @@ A `CiPipelineScheduleID` is a global ID. It is encoded as a string.
 
 An example `CiPipelineScheduleID` is: `"gid://gitlab/Ci::PipelineSchedule/1"`.
 
+### `CiPipelineScheduleVariableID`
+
+A `CiPipelineScheduleVariableID` is a global ID. It is encoded as a string.
+
+An example `CiPipelineScheduleVariableID` is: `"gid://gitlab/Ci::PipelineScheduleVariable/1"`.
+
 ### `CiRunnerID`
 
 A `CiRunnerID` is a global ID. It is encoded as a string.
@@ -28961,6 +28967,8 @@ Attributes for the pipeline schedule variable.
 
 | Name | Type | Description |
 | ---- | ---- | ----------- |
+| <a id="pipelineschedulevariableinputdestroy"></a>`destroy` | [`Boolean`](#boolean) | Boolean option to destroy the variable. |
+| <a id="pipelineschedulevariableinputid"></a>`id` | [`CiPipelineScheduleVariableID`](#cipipelineschedulevariableid) | ID of the variable to mutate. |
 | <a id="pipelineschedulevariableinputkey"></a>`key` | [`String!`](#string) | Name of the variable. |
 | <a id="pipelineschedulevariableinputvalue"></a>`value` | [`String!`](#string) | Value of the variable. |
 | <a id="pipelineschedulevariableinputvariabletype"></a>`variableType` | [`CiVariableType!`](#civariabletype) | Type of the variable. |
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d4f3c649df6d41bd7915d5973c643ff2f663071f..0367a77421aaf24a521932a43bc687d10817b561 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -33336,6 +33336,12 @@ msgstr ""
 msgid "PipelineSchedules|An error occurred while creating the pipeline schedule."
 msgstr ""
 
+msgid "PipelineSchedules|An error occurred while trying to fetch the pipeline schedule."
+msgstr ""
+
+msgid "PipelineSchedules|An error occurred while updating the pipeline schedule."
+msgstr ""
+
 msgid "PipelineSchedules|Are you sure you want to delete this pipeline schedule?"
 msgstr ""
 
@@ -33345,6 +33351,9 @@ msgstr ""
 msgid "PipelineSchedules|Create a new pipeline schedule"
 msgstr ""
 
+msgid "PipelineSchedules|Create pipeline schedule"
+msgstr ""
+
 msgid "PipelineSchedules|Cron timezone"
 msgstr ""
 
@@ -33402,9 +33411,6 @@ msgstr ""
 msgid "PipelineSchedules|Runs with the same project permissions as the schedule owner."
 msgstr ""
 
-msgid "PipelineSchedules|Save pipeline schedule"
-msgstr ""
-
 msgid "PipelineSchedules|Successfully scheduled a pipeline to run. Go to the %{linkStart}Pipelines page%{linkEnd} for details. "
 msgstr ""
 
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index 1697533803aa088dd091f0facf63db121389a691..bb48d4dc38dcc18f6e72af6c896ab4a6cb0f4507 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -1,5 +1,5 @@
 import MockAdapter from 'axios-mock-adapter';
-import { GlForm } from '@gitlab/ui';
+import { GlForm, GlLoadingIcon } from '@gitlab/ui';
 import Vue, { nextTick } from 'vue';
 import VueApollo from 'vue-apollo';
 import createMockApollo from 'helpers/mock_apollo_helper';
@@ -14,8 +14,14 @@ import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
 import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
 import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
 import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql';
+import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
 import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers';
-import { createScheduleMutationResponse } from '../mock_data';
+import {
+  createScheduleMutationResponse,
+  updateScheduleMutationResponse,
+  mockSinglePipelineScheduleNode,
+} from '../mock_data';
 
 Vue.use(VueApollo);
 
@@ -23,8 +29,20 @@ jest.mock('~/alert');
 jest.mock('~/lib/utils/url_utility', () => ({
   visitUrl: jest.fn(),
   joinPaths: jest.fn().mockReturnValue(''),
+  queryToObject: jest.fn().mockReturnValue({ id: '1' }),
 }));
 
+const {
+  data: {
+    project: {
+      pipelineSchedules: { nodes },
+    },
+  },
+} = mockSinglePipelineScheduleNode;
+
+const schedule = nodes[0];
+const variables = schedule.variables.nodes;
+
 describe('Pipeline schedules form', () => {
   let wrapper;
   const defaultBranch = 'main';
@@ -32,8 +50,13 @@ describe('Pipeline schedules form', () => {
   const cron = '';
   const dailyLimit = '';
 
+  const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode);
+  const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
   const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse);
   const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+  const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse);
+  const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
 
   const createMockApolloProvider = (
     requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]],
@@ -52,8 +75,6 @@ describe('Pipeline schedules form', () => {
         fullPath: 'gitlab-org/gitlab',
         projectId,
         defaultBranch,
-        cron,
-        cronTimezone: '',
         dailyLimit,
         settingsLink: '',
         schedulesPath: '/root/ci-project/-/pipeline_schedules',
@@ -69,6 +90,7 @@ describe('Pipeline schedules form', () => {
   const findRefSelector = () => wrapper.findComponent(RefSelector);
   const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button');
   const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button');
+  const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
   // Variables
   const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
   const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
@@ -187,7 +209,38 @@ describe('Pipeline schedules form', () => {
     });
   });
 
-  describe('schedule creation', () => {
+  describe('Button text', () => {
+    it.each`
+      editing  | expectedText
+      ${true}  | ${'Edit pipeline schedule'}
+      ${false} | ${'Create pipeline schedule'}
+    `(
+      'button text is $expectedText when editing is $editing',
+      async ({ editing, expectedText }) => {
+        createComponent(shallowMountExtended, editing, [
+          [getPipelineSchedulesQuery, querySuccessHandler],
+        ]);
+
+        await waitForPromises();
+
+        expect(findSubmitButton().text()).toBe(expectedText);
+      },
+    );
+  });
+
+  describe('Schedule creation', () => {
+    it('when creating a schedule the query is not called', () => {
+      createComponent();
+
+      expect(querySuccessHandler).not.toHaveBeenCalled();
+    });
+
+    it('does not show loading state when creating new schedule', () => {
+      createComponent();
+
+      expect(findLoadingIcon().exists()).toBe(false);
+    });
+
     describe('schedule creation success', () => {
       let mock;
 
@@ -259,4 +312,125 @@ describe('Pipeline schedules form', () => {
       });
     });
   });
+
+  describe('Schedule editing', () => {
+    let mock;
+
+    beforeEach(() => {
+      mock = new MockAdapter(axios);
+    });
+
+    afterEach(() => {
+      mock.restore();
+    });
+
+    it('shows loading state when editing', async () => {
+      createComponent(shallowMountExtended, true, [
+        [getPipelineSchedulesQuery, querySuccessHandler],
+      ]);
+
+      expect(findLoadingIcon().exists()).toBe(true);
+
+      await waitForPromises();
+
+      expect(findLoadingIcon().exists()).toBe(false);
+    });
+
+    describe('schedule fetch success', () => {
+      it('fetches schedule and sets form data correctly', async () => {
+        createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]);
+
+        expect(querySuccessHandler).toHaveBeenCalled();
+
+        await waitForPromises();
+
+        expect(findDescription().element.value).toBe(schedule.description);
+        expect(findIntervalComponent().props('initialCronInterval')).toBe(schedule.cron);
+        expect(findTimezoneDropdown().props('value')).toBe(schedule.cronTimezone);
+        expect(findRefSelector().props('value')).toBe(schedule.ref);
+        expect(findVariableRows()).toHaveLength(3);
+        expect(findKeyInputs().at(0).element.value).toBe(variables[0].key);
+        expect(findKeyInputs().at(1).element.value).toBe(variables[1].key);
+        expect(findValueInputs().at(0).element.value).toBe(variables[0].value);
+        expect(findValueInputs().at(1).element.value).toBe(variables[1].value);
+      });
+    });
+
+    it('schedule fetch failure', async () => {
+      createComponent(shallowMountExtended, true, [
+        [getPipelineSchedulesQuery, queryFailedHandler],
+      ]);
+
+      await waitForPromises();
+
+      expect(createAlert).toHaveBeenCalledWith({
+        message: 'An error occurred while trying to fetch the pipeline schedule.',
+      });
+    });
+
+    it('edit schedule success', async () => {
+      createComponent(mountExtended, true, [
+        [getPipelineSchedulesQuery, querySuccessHandler],
+        [updatePipelineScheduleMutation, updateMutationHandlerSuccess],
+      ]);
+
+      await waitForPromises();
+
+      findDescription().element.value = 'Updated schedule';
+      findDescription().trigger('change');
+
+      findIntervalComponent().vm.$emit('cronValue', '0 22 16 * *');
+
+      // Ensures variable is sent with destroy property set true
+      findRemoveIcons().at(0).vm.$emit('click');
+
+      findSubmitButton().vm.$emit('click');
+
+      await waitForPromises();
+
+      expect(updateMutationHandlerSuccess).toHaveBeenCalledWith({
+        input: {
+          active: schedule.active,
+          cron: '0 22 16 * *',
+          cronTimezone: schedule.cronTimezone,
+          id: schedule.id,
+          ref: schedule.ref,
+          description: 'Updated schedule',
+          variables: [
+            {
+              destroy: true,
+              id: variables[0].id,
+              key: variables[0].key,
+              value: variables[0].value,
+              variableType: variables[0].variableType,
+            },
+            {
+              destroy: false,
+              id: variables[1].id,
+              key: variables[1].key,
+              value: variables[1].value,
+              variableType: variables[1].variableType,
+            },
+          ],
+        },
+      });
+    });
+
+    it('edit schedule failure', async () => {
+      createComponent(shallowMountExtended, true, [
+        [getPipelineSchedulesQuery, querySuccessHandler],
+        [updatePipelineScheduleMutation, updateMutationHandlerFailed],
+      ]);
+
+      await waitForPromises();
+
+      findSubmitButton().vm.$emit('click');
+
+      await waitForPromises();
+
+      expect(createAlert).toHaveBeenCalledWith({
+        message: 'An error occurred while updating the pipeline schedule.',
+      });
+    });
+  });
 });
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 58fc4a616e44c284be720544174fba766073d835..81283a7170b4b43648299d2bf16bba22b73db914 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -2,6 +2,7 @@
 import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
 import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
 import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json';
+import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json';
 
 const {
   data: {
@@ -30,10 +31,10 @@ const {
 
 export const mockPipelineScheduleNodes = nodes;
 export const mockPipelineScheduleCurrentUser = currentUser;
-
 export const mockPipelineScheduleAsGuestNodes = guestNodes;
-
 export const mockTakeOwnershipNodes = takeOwnershipNodes;
+export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse;
+
 export const emptyPipelineSchedulesResponse = {
   data: {
     project: {
@@ -89,4 +90,14 @@ export const createScheduleMutationResponse = {
   },
 };
 
+export const updateScheduleMutationResponse = {
+  data: {
+    pipelineScheduleUpdate: {
+      clientMutationId: null,
+      errors: [],
+      __typename: 'PipelineScheduleUpdatePayload',
+    },
+  },
+};
+
 export { mockGetPipelineSchedulesGraphQLResponse };
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 3bfe9113e834885db689d22003c068c066a8b60f..7bba7910b873854210d569f4beffd88875ee504c 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -63,6 +63,12 @@
       expect_graphql_errors_to_be_empty
     end
 
+    it "#{fixtures_path}#{get_pipeline_schedules_query}.single.json" do
+      post_graphql(query, current_user: user, variables: { projectPath: project.full_path, ids: pipeline_schedule_populated.id })
+
+      expect_graphql_errors_to_be_empty
+    end
+
     it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do
       guest = create(:user)
       project.add_guest(user)
diff --git a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
index 564bc95b352cd7f4917f4e4754cd88a95cad714a..a932002d61498320199da8d0572721ae69967544 100644
--- a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
+++ b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
@@ -5,5 +5,5 @@
 RSpec.describe Mutations::Ci::PipelineSchedule::VariableInputType, feature_category: :continuous_integration do
   specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariableInput') }
 
-  it { expect(described_class.arguments.keys).to match_array(%w[key value variableType]) }
+  it { expect(described_class.arguments.keys).to match_array(%w[id key value variableType destroy]) }
 end
diff --git a/spec/helpers/ci/pipeline_schedules_helper_spec.rb b/spec/helpers/ci/pipeline_schedules_helper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1ba24a08b5876a1e46bd8d19709b53ce108121d4
--- /dev/null
+++ b/spec/helpers/ci/pipeline_schedules_helper_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedulesHelper, feature_category: :continuous_integration do
+  let_it_be(:project) { build_stubbed(:project) }
+  let_it_be(:user) { build_stubbed(:user) }
+  let_it_be(:pipeline_schedule) { build_stubbed(:ci_pipeline_schedule, project: project, owner: user) }
+  let_it_be(:timezones) { [{ identifier: "Pacific/Honolulu", name: "Hawaii" }] }
+
+  let_it_be(:pipeline_schedule_variable) do
+    build_stubbed(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
+  end
+
+  describe '#js_pipeline_schedules_form_data' do
+    before do
+      allow(helper).to receive(:timezone_data).and_return(timezones)
+    end
+
+    it 'returns pipeline schedule form data' do
+      expect(helper.js_pipeline_schedules_form_data(project, pipeline_schedule)).to include({
+        full_path: project.full_path,
+        daily_limit: nil,
+        project_id: project.id,
+        schedules_path: pipeline_schedules_path(project),
+        settings_link: project_settings_ci_cd_path(project),
+        timezone_data: timezones.to_json
+      })
+    end
+  end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb
index c1da231a4a6c632cc262ee986e1c1bffb544ec55..3c3dcfc0a2dac94144274a28bf96f6e0614e9137 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb
@@ -9,6 +9,14 @@
   let_it_be(:project) { create(:project, :public, :repository) }
   let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
 
+  let_it_be(:variable_one) do
+    create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
+  end
+
+  let_it_be(:variable_two) do
+    create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule)
+  end
+
   let(:mutation) do
     variables = {
       id: pipeline_schedule.to_global_id.to_s,
@@ -30,6 +38,7 @@
             nodes {
               key
               value
+              variableType
             }
           }
         }
@@ -88,8 +97,37 @@
 
         expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref])
 
-        expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA')
-        expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123')
+        expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['key']).to eq('AAA')
+        expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['value']).to eq('AAA123')
+      end
+    end
+
+    context 'when updating and removing variables' do
+      let(:pipeline_schedule_parameters) do
+        {
+          variables: [
+            { key: 'ABC', value: "ABC123", variableType: 'ENV_VAR', destroy: false },
+            { id: variable_one.to_global_id.to_s,
+              key: 'foo', value: "foovalue",
+              variableType: 'ENV_VAR',
+              destroy: true },
+            { id: variable_two.to_global_id.to_s, key: 'newbar', value: "newbarvalue", variableType: 'ENV_VAR' }
+          ]
+        }
+      end
+
+      it 'processes variables correctly' do
+        post_graphql_mutation(mutation, current_user: user)
+
+        expect(response).to have_gitlab_http_status(:success)
+
+        expect(mutation_response['pipelineSchedule']['variables']['nodes'])
+          .to match_array(
+            [
+              { "key" => 'newbar', "value" => 'newbarvalue', "variableType" => 'ENV_VAR' },
+              { "key" => 'ABC', "value" => "ABC123", "variableType" => 'ENV_VAR' }
+            ]
+          )
       end
     end
 
diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb
index 6899f6c7d63121c232974260168b8afbdf7c15a4..c31a652ed937af6d335b8d956d6416928c8ab960 100644
--- a/spec/services/ci/pipeline_schedules/update_service_spec.rb
+++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb
@@ -8,9 +8,16 @@
   let_it_be(:project) { create(:project, :public, :repository) }
   let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
 
+  let_it_be(:pipeline_schedule_variable) do
+    create(:ci_pipeline_schedule_variable,
+      key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
+  end
+
   before_all do
     project.add_maintainer(user)
     project.add_reporter(reporter)
+
+    pipeline_schedule.reload
   end
 
   describe "execute" do
@@ -35,7 +42,10 @@
           description: 'updated_desc',
           ref: 'patch-x',
           active: false,
-          cron: '*/1 * * * *'
+          cron: '*/1 * * * *',
+          variables_attributes: [
+            { id: pipeline_schedule_variable.id, key: 'bar', secret_value: 'barvalue' }
+          ]
         }
       end
 
@@ -47,6 +57,42 @@
           .and change { pipeline_schedule.ref }.from('master').to('patch-x')
           .and change { pipeline_schedule.active }.from(true).to(false)
           .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
+          .and change { pipeline_schedule.variables.last.key }.from('foo').to('bar')
+          .and change { pipeline_schedule.variables.last.value }.from('foovalue').to('barvalue')
+      end
+
+      context 'when creating a variable' do
+        let(:params) do
+          {
+            variables_attributes: [
+              { key: 'ABC', secret_value: 'ABC123' }
+            ]
+          }
+        end
+
+        it 'creates the new variable' do
+          expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(1)
+
+          expect(pipeline_schedule.variables.last.key).to eq('ABC')
+          expect(pipeline_schedule.variables.last.value).to eq('ABC123')
+        end
+      end
+
+      context 'when deleting a variable' do
+        let(:params) do
+          {
+            variables_attributes: [
+              {
+                id: pipeline_schedule_variable.id,
+                _destroy: true
+              }
+            ]
+          }
+        end
+
+        it 'deletes the existing variable' do
+          expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(-1)
+        end
       end
 
       it 'returns ServiceResponse.success' do