Skip to the lesson
FixTheRobot Home / VLA / Phase 9 / 9.2

VLA · Phase 9 · VLA Deployment and Robot Integration

9.2 — Action Adapters and Robot-Specific Interfaces

Translating a model’s outputs into a particular robot’s commands.

  • Roadmap phase 9
  • VLA Deployment and Robot Integration
  • About 12 minutes to read

The model and the robot do not speak the same language. This lesson builds the small, testable translator between them and shows how to keep per-robot differences in one reviewable table.

Claims about papers, models and textbooks carry numbered references, listed with what each one is cited for in the References. Code marked “Toy model” is our own illustration, not the cited system.

Learning objectives

After this lesson, you should be able to:

  • List the steps of an action adapter.
  • Build an adapter with clipping, inverse kinematics and a limit check.
  • Keep per-robot differences in a table.
  • Write golden tests for sign and unit conventions.
  1. De-normalise the model output
  2. Convert units and clip the size
  3. Convert hand motion to joint motion
  4. Check the limits, or reject
  5. Convert the gripper and send
An action adapter.

The one-sentence idea

A small, boring translator with tests.

Signs and units are where the bugs are.

The model speaks one language, the robot another

A model outputs numbers in the action definition of its training data (lesson 4.1): perhaps small hand motions plus a gripper value, normalised (lesson 4.3). Your robot accepts something else: joint targets in radians, a gripper width in millimetres, at a particular rate. An action adapter is the piece of software in between. It is small, but it is where a large share of integration bugs live, because a wrong sign or unit moves a real arm. Mixed-robot datasets have the same problem on the training side: RDT-1B maps different robots into one named action layout with a mask so that the physical meaning of each number is preserved.[1] An adapter does the reverse mapping on the way out.

An adapter is a chain of small, testable steps

Typical steps: de-normalise, convert units, clip the motion size, convert from hand motion to joint targets (inverse kinematics), check joint limits, convert the gripper. Here is a complete adapter for the two-joint arm of earlier lessons:

import numpy as np

L1, L2 = 1.0, 1.0
joint_limits = np.array([[-2.5, 2.5], [-2.8, 2.8]])                 # radians

def forward(q): return np.array([L1*np.cos(q[0]) + L2*np.cos(q[0]+q[1]), L1*np.sin(q[0]) + L2*np.sin(q[0]+q[1])])
def jacobian(q):
    eps = 1e-6
    return np.column_stack([(forward(q + eps*np.eye(2)[i]) - forward(q)) / eps for i in range(2)])

class Adapter:
    max_step = 0.05                                                  # metres per command
    def __call__(self, q, model_action):
        move = model_action[:2] * 0.02                               # de-normalise: one unit = 2 cm
        norm = np.linalg.norm(move)
        if norm > self.max_step: move = move * self.max_step / norm  # clip the size of the motion
        dq = np.linalg.solve(jacobian(q), move)                      # hand motion -> joint motion
        target = q + dq
        if np.any(target < joint_limits[:, 0]) or np.any(target > joint_limits[:, 1]):
            return None, "rejected: joint limit"
        gripper_mm = 80.0 * (1.0 - float(np.clip(model_action[2], 0, 1)))     # model: 1 = closed, 0 = open; robot: width in mm
        return (target, gripper_mm), "ok"

adapter = Adapter()
q = np.array([0.4, 1.2])
for action in ([1.0, 0.0, 0.0], [10.0, 10.0, 1.0]):
    result, status = adapter(q, np.array(action))
    if result is None: print(action, "->", status)
    else: print(action, "-> joint targets (rad)", np.round(result[0], 4).tolist(), "| gripper width (mm)", result[1], "|", status)

Output:

[1.0, 0.0, 0.0] -> joint targets (rad) [0.3994, 1.1809] | gripper width (mm) 80.0 | ok
[10.0, 10.0, 1.0] -> joint targets (rad) [0.4368, 1.1135] | gripper width (mm) 0.0 | ok

Toy model. A planar two-joint arm and made-up unit conversions. The structure, small steps with a rejection path, is what to copy.

The second action asked for a very large motion; the adapter clipped it to 5 cm before converting. Where the resulting joint target would violate a limit, the adapter returns a rejection instead of a command; lesson 9.4 builds on that.

One model, several robots: a table, not a pile of ifs

When the same model drives more than one robot, keep each robot's differences in a table of explicit settings rather than scattered conditionals. The adapter code stays the same; the table is reviewed by someone who knows the robot:

robots = {
    "arm_a": {"step_scale_m": 0.02, "gripper_open_mm": 80.0, "invert_y": False, "rate_hz": 10},
    "arm_b": {"step_scale_m": 0.01, "gripper_open_mm": 110.0, "invert_y": True,  "rate_hz": 20},
}

def to_command(robot, action):
    s = robots[robot]
    dx, dy, grip = action
    dy = -dy if s["invert_y"] else dy
    return {"dx_m": dx * s["step_scale_m"], "dy_m": dy * s["step_scale_m"], "gripper_mm": s["gripper_open_mm"] * (1 - grip)}

action = (1.0, 1.0, 0.0)
for name in robots:
    print(name, "->", to_command(name, action))

Output:

arm_a -> {'dx_m': 0.02, 'dy_m': 0.02, 'gripper_mm': 80.0}
arm_b -> {'dx_m': 0.01, 'dy_m': -0.01, 'gripper_mm': 110.0}

The same model output becomes different commands, which is correct. Each entry (the scale, the sign, the gripper range) should come from the robot's documentation or a measurement, written next to the value.

Test adapters with simple known cases

Adapters are easy to test and very much worth testing. A zero action should not move the robot. A positive x action should move the hand in the positive x direction. An out-of-range action should be clipped. And a sign-convention check protects you from the most common mistake:

import numpy as np

def hand_motion(action, scale=0.02, max_step=0.05):
    move = np.array(action[:2]) * scale
    n = np.linalg.norm(move)
    return move if n <= max_step else move * max_step / n

tests = {
    "zero action does not move":             np.allclose(hand_motion([0, 0, 0]), 0),
    "positive x moves in +x":                hand_motion([1, 0, 0])[0] > 0,
    "negative y moves in -y":                hand_motion([0, -1, 0])[1] < 0,
    "huge action is clipped to 5 cm":        np.isclose(np.linalg.norm(hand_motion([100, 100, 0])), 0.05),
    "direction is kept when clipping":       np.isclose(hand_motion([100, 0, 0])[0], 0.05),
}
for name, passed in tests.items():
    print(("PASS" if passed else "FAIL"), name)

Output:

PASS zero action does not move
PASS positive x moves in +x
PASS negative y moves in -y
PASS huge action is clipped to 5 cm
PASS direction is kept when clipping

Run such tests in simulation first and with the robot powered but held, before any free motion. A good adapter is boring.

Common misconceptions

Misconception 1 “The model output can go straight to the motors.”

Correction It is in the training data’s definition, not the robot’s.

Misconception 2 “An adapter is trivial.”

Correction A wrong sign moves a real arm the wrong way.

Misconception 3 “Per-robot conditionals scattered in code are fine.”

Correction A single table can be reviewed.

Engineering takeaways

  1. A chain of small steps.
  2. Reject rather than send a bad command.
  3. Per-robot table.
  4. Golden tests.

Make the translator boring and tested.

Knowledge check

Try each question first, then reveal the explanation.

Question 1

Why clip the motion size in the adapter?

Explanation

To bound what any model output can make the robot do.

Question 2

Why use a table of per-robot settings?

Explanation

It can be reviewed in one place by someone who knows the robot.

Question 3

Name a golden test for an adapter.

Explanation

A zero action does not move; +x action moves in +x.

References

Numbered notes in the text, like [1], point here. Each entry says what it is cited for and how that was checked. Papers and books can be revised or misread, so follow the links before relying on a detail. Code marked “Toy model” is our own illustration of an idea, not the code of the cited system.

  1. Liu, S., et al. (2025). RDT-1B: a diffusion foundation model for bimanual manipulation. ICLR 2025 / arXiv. arXiv:2410.07864

    Cited for:

    • Introduces a physically interpretable unified action space that represents different robots’ actions in one format while preserving the physical meaning of each original action, to help transfer physical knowledge across robots. Checked against the paper’s abstract.

What’s next

Next: coordinate frames and camera calibration.

Next lesson

9.3 — Coordinate frames and camera calibration

Keeping image, robot and world coordinates consistent.

Back to the VLA roadmap