Actions

An Action is the typed command the controller sends to the robot each tick. Actions flow from the controller through the relay server to the robot, and they are the same Python object you record alongside observations, replay, and emit from a learned policy.

Defined in solarian/core/action.py.

What is an Action?

An Action is a top-level container that carries:

  • An action type (action.action_type), an integer that tells the robot how to dispatch the action (see Action types).
  • For MODULE_ACTION, a dictionary of ModuleAction instances keyed by module name.
  • For other action types, an arbitrary data dictionary that's forwarded as-is.
Action(action_type=MODULE_ACTION)
 ├── arm: ArmAction
 │    ├── shoulder_pan: PosData
 │    └── shoulder_lift: PosData
 └── gripper: GripperAction
      └── pos: PosData

The structure mirrors your Robot: every ModuleAction attribute matches a Module attribute on the robot.

ModuleAction

Subclass ModuleAction and bind each field to a Data reference on your Robot:

from solarian.core.action import ModuleAction, ActuatorField
from myrobot.robot import MyRobot

class ArmAction(ModuleAction):
    shoulder_pan = ActuatorField(MyRobot.arm.shoulder_pan.pos)
    shoulder_lift = ActuatorField(MyRobot.arm.shoulder_lift.pos)
    elbow_flex = ActuatorField(MyRobot.arm.elbow_flex.pos)
    wrist_flex = ActuatorField(MyRobot.arm.wrist_flex.pos)
    wrist_roll = ActuatorField(MyRobot.arm.wrist_roll.pos)

Each ActuatorField carries the dtype/shape of its bound Data so that values are validated and shaped correctly at runtime. The field name (shoulder_pan) must match the actuator name on the module.

Use the introspection helpers if you need to walk the schema:

ArmAction.get_actuator_fields_spec()
# {'shoulder_pan': ActuatorField(...), 'shoulder_lift': ActuatorField(...), ...}

Action

Subclass Action and attach ModuleAction instances. The attribute name on Action must match the module name on Robot:

from solarian.core.action import Action

class MyAction(Action):
    arm = ArmAction()
    gripper = GripperAction()

MyAction knows how to serialize to a dict, validate incoming data, and produce a zero-initialized example via MyAction.zeros().

Building an action

Start from MyAction.zeros() and overwrite the fields you care about:

from solarian.core.action import ActionType
from myrobot.action import MyAction

action = MyAction.zeros()
action.action_type = ActionType.MODULE_ACTION
action.arm.shoulder_pan.value = 0.42
action.arm.shoulder_lift.value = -0.1
action.gripper.pos.value = 0.0

MyAction.zeros() walks every declared ModuleAction and ActuatorField, allocates the right Data subtype for each, and returns a fully populated container with action_type=STOP.

A skill does not use the typed schema. It builds the envelope itself, keyed by module name, with the motion type per module; see Motion actions.

Sending actions from a controller

A controller sends whatever its control_step() returns. By default that is the merge of what its running skills emitted; override it to return your own:

class MyController(Controller):
    def control_step(self) -> MyAction:
        action = MyAction.zeros()
        action.action_type = ActionType.MODULE_ACTION
        action.arm.shoulder_pan.value = self.target_pan
        return action

The controller's control loop runs at cfg.fps. Each return value goes out over the action socket as a single msgpack-encoded message.

Action types

ActionType is a lightweight integer-style class (not a true IntEnum, so it supports subclass extension without enum-validation conflicts):

class ActionType:
    STOP             = 0
    MODULE_ACTION    = 1
    RESTART_PUT      = 3
    START_SKILL      = 4
    STOP_SKILL       = 5
    SCHEDULE_L       = 8
    SCHEDULE_J       = 9
    MOVE_L           = 10
    MOVE_J           = 11
    SERVO_L          = 12
    SERVO_J          = 13
    START_SERVICE    = 17
    STOP_SERVICE     = 18
    CUSTOM_START     = 100
  • Name
    STOP
    Type
    int = 0
    Description

    Tells a module to stop its run loop. The default _dispatch_action handles it by returning False.

  • Name
    MODULE_ACTION
    Type
    int = 1
    Description

    The most common type. The action carries one or more ModuleAction sub-actions in action.data; the robot pushes each one onto the corresponding module's action_queue.

  • Name
    RESTART_PUT
    Type
    int = 3
    Description

    Resets a module's internal put timing — used when the controller resynchronizes the rate.

  • Name
    START_SKILL
    Type
    int = 4
    Description

    Start a registered skill by name, carrying {"skill": name, "params": {...}}. Module names the skill writes live in params["arm"] / params["gripper"]. One shape for two channels: on the skill_command_queue it asks the controller to start the skill, and on the wire to the robot it asks the robot to. The direction is the channel's, not the type's. Never forwarded to a module.

  • Name
    STOP_SKILL
    Type
    int = 5
    Description

    The counterpart to START_SKILL, carrying just the name.

  • Name
    START_SERVICE / STOP_SERVICE
    Type
    int = 17 / 18
    Description

    Start or stop the processor behind a calculated field, carrying {"module": name, "field": name, "params": {...}}. The relay applies it when it computes that field and forwards it to the robot otherwise; the robot hands it to the owning module. Sent by Controller.start_service / stop_service; see Services.

  • Name
    CUSTOM_START
    Type
    int = 100
    Description

    The first integer reserved for user-defined action types. Anything >= 100 is yours.

Motion actions

Inside a MODULE_ACTION, each module's entry carries its own type, naming the module primitive it calls. L is Cartesian (pose interpolator, arms only). J is configuration space: joints on an arm, a scalar on a gripper.

  • Name
    SCHEDULE_L / SCHEDULE_J
    Type
    int = 8 / 9
    Description

    Timestamped waypoint. The caller owns arrival time; the interpolator blends the point in. Policy chunks use this.

  • Name
    MOVE_L / MOVE_J
    Type
    int = 10 / 11
    Description

    Speed-limited go-to (moveL / moveJ). Duration is distance / max_speed. Optional params.speed overrides the module limit; it is not a mode switch.

  • Name
    SERVO_L / SERVO_J
    Type
    int = 12 / 13
    Description

    Streaming target (servoL / servoJ) over the module's adaptive drive horizon. Leader follow uses SERVO_J.

  • Name
    speed
    Type
    float | None
    Description

    Optional override on MOVE_*: moveJ in rad/s (or gripper units/s), moveL in m/s. None or omitted uses the module default / cfg.max_speed.

  • Name
    rot_speed
    Type
    float
    Description

    Rotational limit for MOVE_L, in rad/s.

  • Name
    frame
    Type
    str = 'world'
    Description

    On MOVE_L, "ee" composes the Cartesian payload as a delta in the current end-effector frame before moving.

A skill builds this envelope itself, keyed by module name. Modules interpret actions; they do not manufacture them for someone else, which is what lets the same skill run on the controller or on the robot — a name crosses a link, a Module object cannot.

# Streaming follow: module owns a short horizon.
Action(
    action_type=ActionType.MODULE_ACTION,
    data={
        "right_arm": {
            "action_type": ActionType.SERVO_J,
            "data": joint_fields,
        },
        "right_gripper": {
            "action_type": ActionType.SERVO_J,
            "data": grip_fields,
        },
    },
)

# Exclusive go-to: duration from max_speed.
Action(
    action_type=ActionType.MODULE_ACTION,
    data={
        "right_arm": {
            "action_type": ActionType.MOVE_J,
            "data": joint_fields,
            "params": {"frame": "world"},
        }
    },
)

See Scheduling Waypoints for how arm and gripper modules execute them.

How the robot routes actions

When the robot receives an Action, it looks at action.action_type:

Action received
   │
   ├── MODULE_ACTION          → push each module's entry onto that
   │                            module.action_queue (shared memory)
   ├── START_SKILL / STOP_SKILL → start or stop a robot-side skill by name
   │
   └── anything else           → look up @action_handler(action_type)
                                 on the Robot subclass and call it

The module's run loop drains its queue each tick and dispatches each entry by its own type: a motion type to the module's interpolator, a custom type to the module's @action_handler.

Custom action types

To add your own action types, subclass ActionType and define class attributes whose values are >= ActionType.CUSTOM_START:

from solarian.core.action import ActionType

class MyActionType(ActionType):
    OPEN_GRIPPER  = ActionType.CUSTOM_START + 0
    CLOSE_GRIPPER = ActionType.CUSTOM_START + 1
    HOMING        = ActionType.CUSTOM_START + 2

Solarian's ActionType.__init_subclass__ validates each value at import time:

  • Any value below CUSTOM_START that conflicts with a built-in raises ValueError(f"{name}={val} conflicts with reserved range").
  • Each new value is registered in ActionType._registry so ActionType.name_of(value) returns a friendly label.

You can declare as many subclasses as you like across plugins; each adds its own entries to the shared registry.

Registering handlers

Use the @action_handler decorator to bind a method to an action type. The decorated method must return bool — True to keep the loop running, False to stop the module/robot.

On a Module, for actions placed on that module's queue:

from solarian.core.module import Module
from solarian.core.action import action_handler, ModuleAction

class GripperModule(Module):
    @action_handler(MyActionType.OPEN_GRIPPER)
    def _on_open(self, action: ModuleAction, t_now: float) -> bool:
        self.write_actuators({"pos": 1.0})
        return True

    @action_handler(MyActionType.CLOSE_GRIPPER)
    def _on_close(self, action: ModuleAction, t_now: float) -> bool:
        self.write_actuators({"pos": 0.0})
        return True

On a Robot, for actions whose type isn't MODULE_ACTION. Useful for fan-out commands, one incoming action that affects several modules:

from solarian.core import Robot
from solarian.core.action import action_handler

class MyRobot(Robot):
    arm = ArmModule()
    gripper = GripperModule()

    @action_handler(MyActionType.HOMING)
    def _on_homing(self, action, t_now) -> bool:
        for module in self.modules.values():
            module.action_queue.put(MoveToHomeAction(...))
        return True

Sending a custom action

Set action.action_type and put your payload in action.data. Custom action types use the raw data dict; Action.zeros() and the per-module path don't apply:

from solarian.core.action import Action

class MyController(Controller):
    def control_step(self) -> Action:
        if self.want_to_home:
            self.want_to_home = False
            action = Action()
            action.action_type = MyActionType.HOMING
            action.data = {"speed": 0.5}
            return action
        return super().control_step()

A skill cannot send one: skills command modules, so a skill's non-MODULE_ACTION return is dropped with a warning. Starting skills and custom commands are the host's call.

Naming conventions

  • Use a subclass per logical group of actions. One per plugin or per high-level capability (GripperActionType, NavigationActionType, …) keeps namespaces clean.
  • Always offset from CUSTOM_START. The reserved range catches collisions early.
  • Keep handler return values explicit. A True keeps the loop alive; a False is interpreted as "stop this module" (or "stop this robot" at the robot level). Don't let a handler accidentally fall through to None.

Compression and serialization

Action shares its serialization stack with Observation. On the wire it's msgpack(action.to_dict()). Each ActuatorField's value is whatever the underlying Data subclass uses (e.g. a float for PosData).

You generally don't compress actions explicitly — they're small enough that JPEG-style codecs aren't needed.

See also

  • Observations — the counterpart that flows the other way, and the Data classes both are built from.
  • Modules — where actions are dispatched on the robot side.
  • Scheduling Waypoints — pose and scalar motion on arm and gripper modules.
  • Controllers — where actions are produced.
  • Skills — skills emit module actions into the controller's queues.

Was this page helpful?