diff --git a/examples/02_plan_and_execute.py b/examples/02_plan_and_execute.py
index 0fad11f3..998d8d34 100644
--- a/examples/02_plan_and_execute.py
+++ b/examples/02_plan_and_execute.py
@@ -1,6 +1,6 @@
 import asyncio
 
-from nova import Nova
+from nova import Nova, MotionSettings
 from nova.actions import jnt, ptp
 from nova.types import Pose
 
@@ -40,6 +40,10 @@ async def main():
             jnt(home_joints),
         ]
 
+        # you can update the settings of the action
+        for action in actions:
+            action.settings = MotionSettings(tcp_velocity_limit=200)
+
         joint_trajectory = await motion_group.plan(actions, tcp)
         await motion_group.execute(joint_trajectory, tcp, actions=actions)
 
diff --git a/examples/03_move_and_set_ios.py b/examples/03_move_and_set_ios.py
index 573dffd9..f98734a9 100644
--- a/examples/03_move_and_set_ios.py
+++ b/examples/03_move_and_set_ios.py
@@ -32,7 +32,7 @@ async def main():
         target_pose = current_pose @ Pose((100, 0, 0, 0, 0, 0))
         actions = [
             jnt(home_joints),
-            WriteAction(device_id="ur", key="digital_out[0]", value=False),
+            WriteAction(key="digital_out[0]", value=False),
             ptp(target_pose),
             jnt(home_joints),
         ]
diff --git a/nova/__init__.py b/nova/__init__.py
index 64275a69..a2c79655 100644
--- a/nova/__init__.py
+++ b/nova/__init__.py
@@ -2,6 +2,7 @@
 from nova.core.controller import Controller
 from nova.core.motion_group import MotionGroup
 from nova.core.movement_controller import speed_up as speed_up_movement_controller
+from nova.motion_settings import MotionSettings
 from nova.core.nova import Cell, Nova
 
 __all__ = [
@@ -13,4 +14,5 @@
     "api",
     "types",
     "actions",
+    "MotionSettings",
 ]
diff --git a/nova/actions.py b/nova/actions.py
index 27927aee..aea57955 100644
--- a/nova/actions.py
+++ b/nova/actions.py
@@ -3,7 +3,7 @@
 
 import pydantic
 import wandelbots_api_client as wb
-
+from nova.motion_settings import MotionSettings
 from nova.types.collision_scene import CollisionScene
 from nova.types.pose import Pose
 
@@ -17,7 +17,6 @@ def serialize_model(self):
 
 class WriteAction(Action):
     type: Literal["Write"] = "Write"
-    device_id: str
     key: str
     value: Any
 
@@ -50,36 +49,6 @@ class CallAction(Action):
     arguments: list
 
 
-class MotionSettings(pydantic.BaseModel):
-    """Settings to customize motions.
-
-    Attributes:
-        velocity: the cartesian velocity of the TCP on the segment in mm/s
-        acceleration: the cartesian acceleration of the TCP on the segment in mm/s
-        orientation_velocity: the (tcp) orientation velocity on the segment in rad/s
-        orientation_acceleration: the tcp orientation acceleration on the segment in rad/s
-        joint_velocities: the joint velocities on the segment in rad/s
-        joint_accelerations: the joint accelerations on the segment in rad/s
-        blending: the blending radius to connect the previous segment to this one. If blending == 0, blending is
-            disabled. If blending == math.inf, blending is enabled and the radius is automatically determined
-            based on the robot velocity.
-        optimize_approach: define approach axis as DoF and optimize for executable path
-    """
-
-    velocity: float | None = pydantic.Field(default=None, ge=0)
-    acceleration: float | None = pydantic.Field(default=None, ge=0)
-    orientation_velocity: float | None = pydantic.Field(default=None, ge=0)
-    orientation_acceleration: float | None = pydantic.Field(default=None, ge=0)
-    joint_velocities: tuple[float, ...] | None = None
-    joint_accelerations: tuple[float, ...] | None = None
-    blending: float = pydantic.Field(default=0, ge=0)
-    optimize_approach: bool = False
-
-    @classmethod
-    def field_to_varname(cls, field):
-        return f"__ms_{field}"
-
-
 MS = MotionSettings
 PoseOrVectorTuple = Union[
     Pose, tuple[float, float, float, float, float, float], tuple[float, float, float]
@@ -131,8 +100,9 @@ class Linear(Motion):
     """A linear motion
 
     Examples:
-    >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=10))
-    Linear(type='linear', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(velocity=10.0, acceleration=None, orientation_velocity=None, orientation_acceleration=None, joint_velocities=None, joint_accelerations=None, blending=0, optimize_approach=False))
+    >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=10))
+    Linear(type='linear', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(min_blending_velocity=None, position_zone_radius=None, joint_velocity_limits=None, joint_acceleration_limits=None, tcp_velocity_limit=10.0, tcp_acceleration_limit=None, tcp_orientation_velocity_limit=None, tcp_orientation_acceleration_limit=None))
+
     """
 
     type: Literal["linear"] = "linear"
@@ -142,7 +112,7 @@ def _to_api_model(self) -> wb.models.PathLine:
         """Serialize the model to the API model
 
         Examples:
-        >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=10))._to_api_model()
+        >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=10))._to_api_model()
         PathLine(target_pose=Pose2(position=[1, 2, 3], orientation=[4, 5, 6]), path_definition_name='PathLine')
         """
         return wb.models.PathLine(
@@ -164,7 +134,7 @@ def lin(target: PoseOrVectorTuple, settings: MotionSettings = MotionSettings())
     Returns: the linear motion
 
     Examples:
-    >>> ms = MotionSettings(velocity=10)
+    >>> ms = MotionSettings(tcp_velocity_limit=10)
     >>> assert lin((1, 2, 3, 4, 5, 6), settings=ms) == Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=ms)
     >>> assert lin((1, 2, 3)) == lin((1, 2, 3, 0, 0, 0))
 
@@ -177,8 +147,9 @@ class PTP(Motion):
     """A point-to-point motion
 
     Examples:
-    >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=30))
-    PTP(type='ptp', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(velocity=30.0, acceleration=None, orientation_velocity=None, orientation_acceleration=None, joint_velocities=None, joint_accelerations=None, blending=0, optimize_approach=False))
+    >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=30))
+    PTP(type='ptp', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(min_blending_velocity=None, position_zone_radius=None, joint_velocity_limits=None, joint_acceleration_limits=None, tcp_velocity_limit=30.0, tcp_acceleration_limit=None, tcp_orientation_velocity_limit=None, tcp_orientation_acceleration_limit=None))
+
     """
 
     type: Literal["ptp"] = "ptp"
@@ -187,7 +158,7 @@ def _to_api_model(self) -> wb.models.PathCartesianPTP:
         """Serialize the model to the API model
 
         Examples:
-        >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=30))._to_api_model()
+        >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=30))._to_api_model()
         PathCartesianPTP(target_pose=Pose2(position=[1, 2, 3], orientation=[4, 5, 6]), path_definition_name='PathCartesianPTP')
         """
         if not isinstance(self.target, Pose):
@@ -212,7 +183,7 @@ def ptp(target: PoseOrVectorTuple, settings: MotionSettings = MotionSettings())
     Returns: the point-to-point motion
 
     Examples:
-    >>> ms = MotionSettings(acceleration=10)
+    >>> ms = MotionSettings(tcp_acceleration_limit=10)
     >>> assert ptp((1, 2, 3, 4, 5, 6), settings=ms) == PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=ms)
     >>> assert ptp((1, 2, 3)) == ptp((1, 2, 3, 0, 0, 0))
 
@@ -239,7 +210,7 @@ def _to_api_model(self) -> wb.models.PathCircle:
         """Serialize the model to a dictionary
 
         Examples:
-        >>> Circular(target=Pose((1, 2, 3, 4, 5, 6)), intermediate=Pose((10, 20, 30, 40, 50, 60)), settings=MotionSettings(velocity=30))._to_api_model()
+        >>> Circular(target=Pose((1, 2, 3, 4, 5, 6)), intermediate=Pose((10, 20, 30, 40, 50, 60)), settings=MotionSettings(tcp_velocity_limit=30))._to_api_model()
         PathCircle(via_pose=Pose2(position=[10, 20, 30], orientation=[40, 50, 60]), target_pose=Pose2(position=[1, 2, 3], orientation=[4, 5, 6]), path_definition_name='PathCircle')
         """
         if not isinstance(self.target, Pose):
@@ -273,7 +244,7 @@ def cir(
     Returns: the circular motion
 
     Examples:
-    >>> ms = MotionSettings(acceleration=10)
+    >>> ms = MotionSettings(tcp_acceleration_limit=10)
     >>> assert cir((1, 2, 3, 4, 5, 6), (7, 8, 9, 10, 11, 12), settings=ms) == Circular(target=Pose((1, 2, 3, 4, 5, 6)), intermediate=Pose((7, 8, 9, 10, 11, 12)), settings=ms)
     >>> assert cir((1, 2, 3), (4, 5, 6)) == cir((1, 2, 3, 0, 0, 0), (4, 5, 6, 0, 0, 0))
 
@@ -287,8 +258,8 @@ class JointPTP(Motion):
     """A joint PTP motion
 
     Examples:
-    >>> JointPTP(target=(1, 2, 3, 4, 5, 6), settings=MotionSettings(velocity=30))
-    JointPTP(type='joint_ptp', target=(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), settings=MotionSettings(velocity=30.0, acceleration=None, orientation_velocity=None, orientation_acceleration=None, joint_velocities=None, joint_accelerations=None, blending=0, optimize_approach=False))
+    >>> JointPTP(target=(1, 2, 3, 4, 5, 6), settings=MotionSettings(tcp_velocity_limit=30))
+    JointPTP(type='joint_ptp', target=(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), settings=MotionSettings(min_blending_velocity=None, position_zone_radius=None, joint_velocity_limits=None, joint_acceleration_limits=None, tcp_velocity_limit=30.0, tcp_acceleration_limit=None, tcp_orientation_velocity_limit=None, tcp_orientation_acceleration_limit=None))
 
     """
 
@@ -298,7 +269,7 @@ def _to_api_model(self) -> wb.models.PathJointPTP:
         """Serialize the model to the API model
 
         Examples:
-        >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(velocity=30))._to_api_model()
+        >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(tcp_velocity_limit=30))._to_api_model()
         PathJointPTP(target_joint_position=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], path_definition_name='PathJointPTP')
         """
         if not isinstance(self.target, tuple):
@@ -322,7 +293,7 @@ def jnt(target: tuple[float, ...], settings: MotionSettings = MotionSettings())
     Returns: the joint PTP motion
 
     Examples:
-    >>> ms = MotionSettings(acceleration=10)
+    >>> ms = MotionSettings(tcp_acceleration_limit=10)
     >>> assert jnt((1, 2, 3, 4, 5, 6), settings=ms) == JointPTP(target=(1, 2, 3, 4, 5, 6), settings=ms)
 
     """
@@ -347,7 +318,7 @@ def serialize_model(self):
         """Serialize the model to a dictionary
 
         Examples:
-        >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(velocity=30)).model_dump()
+        >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(tcp_velocity_limit=30)).model_dump()
         {'target_joint_position': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], 'path_definition_name': 'PathJointPTP'}
         """
         raise NotImplementedError("Spline motion is not yet implemented")
@@ -370,7 +341,7 @@ def spl(
     Returns: the spline motion
 
     Examples:
-    >>> ms = MotionSettings(acceleration=10)
+    >>> ms = MotionSettings(tcp_acceleration_limit=10)
     >>> assert spl((1, 2, 3, 4, 5, 6), settings=ms) == Spline(target=Pose((1, 2, 3, 4, 5, 6)), settings=ms)
     >>> assert spl((1, 2, 3)) == spl((1, 2, 3, 0, 0, 0))
 
@@ -496,10 +467,25 @@ def __add__(self, other: "CombinedActions") -> "CombinedActions":
         return CombinedActions(items=self.items + other.items)
 
     def to_motion_command(self) -> list[wb.models.MotionCommand]:
-        motions = [
-            wb.models.MotionCommandPath.from_dict(motion.model_dump()) for motion in self.motions
-        ]
-        return [wb.models.MotionCommand(path=motion) for motion in motions]
+        motion_commands = []
+        for motion in self.motions:
+            path = wb.models.MotionCommandPath.from_dict(motion.model_dump())
+            blending = (
+                motion.settings.as_blending_setting()
+                if motion.settings.has_blending_settings()
+                else None
+            )
+            limits_override = (
+                motion.settings.as_limits_settings()
+                if motion.settings.has_limits_override()
+                else None
+            )
+            motion_commands.append(
+                wb.models.MotionCommand(
+                    path=path, blending=blending, limits_override=limits_override
+                )
+            )
+        return motion_commands
 
     def to_set_io(self) -> list[wb.models.SetIO]:
         return [
diff --git a/nova/motion_settings.py b/nova/motion_settings.py
new file mode 100644
index 00000000..4b60a5df
--- /dev/null
+++ b/nova/motion_settings.py
@@ -0,0 +1,96 @@
+import pydantic
+import wandelbots_api_client as wb
+
+
+class MotionSettings(pydantic.BaseModel):
+    """
+    Settings for an action. This is closely related to the `MotionCommand` in the API.
+    See planTrajectory.motion_commands for more information.
+
+    Motion settings are immutable; if you need to change a setting, create a copy and update the new object.
+
+    Attributes:
+        min_blending_velocity:
+            A minimum velocity for blending, in percent. Cannot be used if `blending` is set.
+
+        position_zone_radius:
+            Defines the position zone radius.
+
+        joint_velocity_limits:
+            Maximum joint velocities
+
+        joint_acceleration_limits:
+            Maximum joint accelerations
+
+        tcp_velocity_limit:
+            Maximum TCP velocity
+
+        tcp_acceleration_limit:
+            Maximum TCP acceleration
+
+        tcp_orientation_velocity_limit:
+            Maximum TCP orientation velocity
+
+        tcp_orientation_acceleration_limit:
+            Maximum TCP orientation acceleration
+    """
+
+    min_blending_velocity: int | None = pydantic.Field(default=None)
+    position_zone_radius: float | None = pydantic.Field(default=None)
+    joint_velocity_limits: tuple[float, ...] | None = pydantic.Field(default=None)
+    joint_acceleration_limits: tuple[float, ...] | None = pydantic.Field(default=None)
+    tcp_velocity_limit: float | None = pydantic.Field(default=None)
+    tcp_acceleration_limit: float | None = pydantic.Field(default=None)
+    tcp_orientation_velocity_limit: float | None = pydantic.Field(default=None)
+    tcp_orientation_acceleration_limit: float | None = pydantic.Field(default=None)
+
+    class Config:
+        frozen = True
+
+    @pydantic.model_validator(mode="after")
+    def validate_blending_settings(self) -> "MotionSettings":
+        if self.min_blending_velocity and self.position_zone_radius:
+            raise ValueError("Can't set both min_blending_velocity and blending")
+        return self
+
+    def has_blending_settings(self) -> bool:
+        return any([self.min_blending_velocity, self.position_zone_radius])
+
+    def has_limits_override(self) -> bool:
+        return any(
+            [
+                self.tcp_velocity_limit,
+                self.tcp_acceleration_limit,
+                self.tcp_orientation_velocity_limit,
+                self.tcp_orientation_acceleration_limit,
+                self.joint_velocity_limits,
+                self.joint_acceleration_limits,
+            ]
+        )
+
+    def as_limits_settings(self) -> wb.models.LimitsOverride:
+        return wb.models.LimitsOverride(
+            joint_velocity_limits=wb.models.Joints(joints=self.joint_velocity_limits)  # type: ignore
+            if self.joint_velocity_limits
+            else None,
+            joint_acceleration_limits=wb.models.Joints(joints=self.joint_acceleration_limits)  # type: ignore
+            if self.joint_acceleration_limits
+            else None,
+            tcp_velocity_limit=self.tcp_velocity_limit,
+            tcp_acceleration_limit=self.tcp_acceleration_limit,
+            tcp_orientation_velocity_limit=self.tcp_orientation_velocity_limit,
+            tcp_orientation_acceleration_limit=self.tcp_orientation_acceleration_limit,
+        )
+
+    def as_blending_setting(self) -> wb.models.MotionCommandBlending:
+        if self.position_zone_radius:
+            return wb.models.MotionCommandBlending(
+                wb.models.BlendingPosition(
+                    position_zone_radius=self.position_zone_radius, blending_name="BlendingPosition"
+                )
+            )
+        return wb.models.MotionCommandBlending(
+            wb.models.BlendingAuto(
+                min_velocity_in_percent=self.min_blending_velocity, blending_name="BlendingAuto"
+            )
+        )