diff --git a/doc/news/DM-41081.feature.rst b/doc/news/DM-41081.feature.rst
new file mode 100644
index 00000000..01573dff
--- /dev/null
+++ b/doc/news/DM-41081.feature.rst
@@ -0,0 +1 @@
+* Add new ``maintel/mtrotator/move_rotator.py`` SAL Script.
\ No newline at end of file
diff --git a/python/lsst/ts/standardscripts/data/scripts/maintel/mtrotator/move_rotator.py b/python/lsst/ts/standardscripts/data/scripts/maintel/mtrotator/move_rotator.py
new file mode 100755
index 00000000..bbe388ee
--- /dev/null
+++ b/python/lsst/ts/standardscripts/data/scripts/maintel/mtrotator/move_rotator.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# This file is part of ts_standardscripts
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import asyncio
+
+from lsst.ts.standardscripts.maintel.mtrotator import MoveRotator
+
+asyncio.run(MoveRotator.amain())
diff --git a/python/lsst/ts/standardscripts/maintel/mtrotator/__init__.py b/python/lsst/ts/standardscripts/maintel/mtrotator/__init__.py
new file mode 100644
index 00000000..b18a2f0b
--- /dev/null
+++ b/python/lsst/ts/standardscripts/maintel/mtrotator/__init__.py
@@ -0,0 +1,22 @@
+# This file is part of ts_standardscripts
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from .move_rotator import MoveRotator
diff --git a/python/lsst/ts/standardscripts/maintel/mtrotator/move_rotator.py b/python/lsst/ts/standardscripts/maintel/mtrotator/move_rotator.py
new file mode 100644
index 00000000..0c10b4ad
--- /dev/null
+++ b/python/lsst/ts/standardscripts/maintel/mtrotator/move_rotator.py
@@ -0,0 +1,141 @@
+# This file is part of ts_standardscripts
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+__all__ = ["MoveRotator"]
+
+import yaml
+from lsst.ts.observatory.control.maintel.mtcs import MTCS
+
+from ...base_block_script import BaseBlockScript
+
+
+class MoveRotator(BaseBlockScript):
+ """Move the rotator to a given angle. It has the option of completing the
+ script before the rotator reaches the desired angle.
+
+ Parameters
+ ----------
+ index : `int`
+ Index of Script SAL component.
+
+ Notes
+ -----
+ **Checkpoints**
+
+ - "Start moving rotator to {angle} degrees.": Start moving rotator.
+ - "Stop script and keep rotator moving.": Stop script.
+ - "Rotator reached {angle} degrees.": Rotator reached angle.
+
+ """
+
+ def __init__(self, index: int) -> None:
+ super().__init__(index=index, descr="Move Rotator")
+
+ self.mtcs = None
+
+ self.rotator_velocity = 3.5 # degrees per second
+ self.short_timeout = 10 # seconds
+ self.long_timeout = 120 # seconds
+
+ @classmethod
+ def get_schema(cls):
+ url = "https://github.com/lsst-ts/"
+ path = (
+ "ts_standardscripts/blob/main/python/lsst/ts/standardscripts/"
+ "maintel/mtrotator/move_rotator.py"
+ )
+ schema_yaml = f"""
+ $schema: http://json-schema.org/draft-07/schema#
+ $id: {url}{path}
+ title: MoveRotator v1
+ description: Configuration for Maintel move rotator SAL Script.
+ type: object
+ properties:
+ angle:
+ description: final angle of the rotator.
+ type: number
+ minimum: -90
+ maximum: 90
+ wait_for_complete:
+ description: >-
+ whether wait for the rotator to reach the desired angle or
+ complete the script before the rotator reaches the desired
+ angle.
+ type: boolean
+ default: true
+ required:
+ - angle
+ additionalProperties: false
+ """
+ schema_dict = yaml.safe_load(schema_yaml)
+
+ base_schema_dict = super().get_schema()
+
+ for properties in base_schema_dict["properties"]:
+ schema_dict["properties"][properties] = base_schema_dict["properties"][
+ properties
+ ]
+
+ return schema_dict
+
+ async def configure(self, config):
+ """
+ Configure the script.
+
+ Parameters
+ ----------
+ config : `dict`
+ Dictionary containing the configuration parameters.
+ """
+ await self.configure_tcs()
+
+ self.target_angle = config.angle
+ self.wait_for_complete = config.wait_for_complete
+
+ await super().configure(config=config)
+
+ async def configure_tcs(self) -> None:
+ """
+ Handle creating MTCS object and waiting for remote to start.
+ """
+ if self.mtcs is None:
+ self.log.debug("Creating MTCS.")
+ self.mtcs = MTCS(
+ domain=self.domain,
+ log=self.log,
+ )
+ await self.mtcs.start_task
+ else:
+ self.log.debug("MTCS already defined, skipping.")
+
+ def set_metadata(self, metadata):
+ """Set the metadata for the script."""
+ metadata.duration = self.long_timeout
+
+ async def run_block(self):
+ """Run the script."""
+ await self.checkpoint(f"Start moving rotator to {self.target_angle} degrees.")
+ await self.mtcs.move_rotator(
+ angle=self.target_angle, wait_for_complete=self.wait_for_complete
+ )
+ await self.checkpoint(
+ f"Move rotator returned. Wait for complete: {self.wait_for_complete}."
+ )
diff --git a/tests/test_maintel_mtrotator_move_rotator.py b/tests/test_maintel_mtrotator_move_rotator.py
new file mode 100644
index 00000000..7c267695
--- /dev/null
+++ b/tests/test_maintel_mtrotator_move_rotator.py
@@ -0,0 +1,118 @@
+# This file is part of ts_standardscripts
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+
+from lsst.ts import standardscripts
+from lsst.ts.observatory.control.maintel.mtcs import MTCS, MTCSUsages
+from lsst.ts.standardscripts.maintel.mtrotator import MoveRotator
+
+
+class TestMoveRotator(
+ standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase
+):
+ async def basic_make_script(self, index):
+ self.script = MoveRotator(index=index)
+
+ self.script.mtcs = MTCS(
+ domain=self.script.domain,
+ intended_usage=MTCSUsages.DryTest,
+ log=self.script.log,
+ )
+
+ self.start_angle = 0.0 # degrees
+ self.very_short_sleep = 0.1 # seconds
+ self.script.mtcs.move_rotator = unittest.mock.AsyncMock()
+
+ return (self.script,)
+
+ async def test_configure_default(self):
+ """Test the default configuration"""
+
+ async with self.make_script():
+ target_angle = 45.0
+
+ await self.configure_script(angle=target_angle)
+
+ assert self.script.target_angle == target_angle
+ assert self.script.wait_for_complete is True
+ assert self.script.program is None
+ assert self.script.reason is None
+ assert self.script.checkpoint_message is None
+
+ async def test_configure_dont_wait_for_complete(self):
+ """Test with the configuration where ``wait_for_complete`` is False"""
+
+ async with self.make_script():
+ target_angle = 45.0
+ wait_for_complete = False
+
+ await self.configure_script(angle=target_angle, wait_for_complete=False)
+
+ assert self.script.target_angle == target_angle
+ assert self.script.wait_for_complete is wait_for_complete
+ assert self.script.program is None
+ assert self.script.reason is None
+ assert self.script.checkpoint_message is None
+
+ async def test_configure_with_program_reason(self):
+ """Testing a valid configuration: with program and reason"""
+
+ # Try configure with a list of valid actuators ids
+ async with self.make_script():
+ self.script.get_obs_id = unittest.mock.AsyncMock(
+ side_effect=["202306060001"]
+ )
+ await self.configure_script(
+ angle=10.0,
+ wait_for_complete=True,
+ program="BLOCK-123",
+ reason="SITCOM-321",
+ )
+
+ assert self.script.program == "BLOCK-123"
+ assert self.script.reason == "SITCOM-321"
+ assert (
+ self.script.checkpoint_message
+ == "MoveRotator BLOCK-123 202306060001 SITCOM-321"
+ )
+
+ async def test_run_with_default_config(self):
+ async with self.make_script():
+ target_angle = 45.0
+
+ await self.configure_script(angle=target_angle)
+
+ await self.run_script()
+
+ self.script.mtcs.move_rotator.assert_called_once_with(
+ angle=target_angle, wait_for_complete=True
+ )
+
+ async def test_executable(self):
+ scripts_dir = standardscripts.get_scripts_dir()
+ script_path = scripts_dir / "maintel" / "mtrotator" / "move_rotator.py"
+ print(script_path)
+ await self.check_executable(script_path)
+
+
+if __name__ == "__main__":
+ unittest.main()