Cheat sheet

URDF and MJCF cheat sheet

URDF and MuJoCo MJCF elements side by side for links, joints, inertia, geometry, limits, actuators and frames, one small arm written in both formats, and the conversion traps between them.

ROS 2MuJoCoLast verified 14 Nov 2026· URDF as specified on the ROS wiki, MuJoCo 3.16 (checked against the 4.0 XML reference)

This sheet is for anyone who has a robot in one format and needs it in the other, or who is writing a model by hand. It maps the elements of URDF, the ROS robot description format, to MJCF, MuJoCo’s native format. It then gives one small arm in both formats and lists what changes in conversion. Element names were checked against the ROS URDF specification and the MuJoCo XML reference on 14 November 2026. For a guided first model, see simulate an arm in MuJoCo.

Element map

Concept URDF MJCF Watch for
Root <robot name> <mujoco model>, bodies inside <worldbody> MJCF also holds options, defaults, assets, actuators and sensors
Rigid body <link>, a flat list <body>, nested URDF builds the tree from joints. MJCF builds it from nesting
Joint <joint> between a parent and a child link <joint> inside the child body A body with no joint is welded to its parent
Joint types revolute, continuous, prismatic, fixed, floating, planar hinge, slide, ball, free revolute and continuous become hinge with or without range. fixed becomes no joint. floating becomes free
Joint axis <axis xyz>, default (1, 0, 0), in the joint frame axis, default (0, 0, 1), in the body frame The defaults differ. Always write the axis explicitly
Joint placement <origin xyz rpy> on the joint: parent link frame to child link frame pos and quat (or euler, axisangle) on the child <body> URDF rpy is fixed-axis x, y, z. In MJCF that is eulerseq="XYZ"
Position limits <limit lower upper>, required for revolute and prismatic range on the joint. With autolimits="true" (the default) a range turns the limit on MJCF reads hinge range in degrees unless <compiler angle="radian"/>
Effort and velocity limits <limit effort velocity>, both required Effort: actuatorfrcrange on the joint or forcerange on the actuator. No joint velocity limit MuJoCo turns URDF effort into actuatorfrcrange when it loads a URDF
Mass and inertia <inertial>: <origin>, <mass value>, <inertia ixx ixy ixz iyy iyz izz> <inertial pos mass diaginertia> or fullinertia (Ixx, Iyy, Izz, Ixy, Ixz, Iyz) The six numbers are in a different order. If <inertial> is absent, MuJoCo computes it from the geoms
Geometry <box size> full lengths, <cylinder radius length>, <sphere radius>, <mesh> <geom type size>: box sizes are half-lengths, cylinder and capsule take a radius and half-length, or fromto Halve every box and cylinder length on the way to MJCF
Visual and collision Separate <visual> and <collision> A geom collides and renders by default. Set contype="0" conaffinity="0" for visual-only geoms MuJoCo discards URDF visuals by default (discardvisual is true for URDF)
Joint dynamics <dynamics damping friction> damping, frictionloss, armature on the joint armature (reflected rotor inertia) has no URDF equivalent
Actuators <transmission> with <actuator> and <mechanicalReduction>. ros2_control uses a <ros2_control> tag <actuator> section: motor, position, velocity, general and more, with gear, ctrlrange, forcerange URDF has no actuator dynamics. Decide the actuator model yourself
Tool and sensor frames A massless link on a fixed joint <site> inside a body MuJoCo fuses fixed-joint links into their parent by default when it loads a URDF (fusestatic)
Sensors <sensor> (camera, ray) is specified on the ROS wiki <sensor> section: jointpos, gyro, accelerometer, force, touch, rangefinder and more
Contacts None, every collision pair is checked by the consumer Parent and child bodies are excluded, unless the parent is welded to the world. <contact><exclude> for other pairs See the exclude in the MJCF below
Units SI: metres, kilograms, radians No fixed units. Angles in degrees by default Set angle="radian" and keep SI to match URDF

One arm, two formats

Two revolute joints (shoulder and elbow) on a fixed base, both turning about y. The upper arm is 0.30 m long and 1.0 kg. The forearm is 0.25 m long and 0.6 kg. Inertias are those of solid cylinders about their centres. The tool frame sits at the forearm tip.

URDF

<?xml version="1.0"?>
<robot name="two_link_arm">
  <link name="base_link">
    <visual>
      <origin xyz="0 0 0.05" rpy="0 0 0"/>
      <geometry><box size="0.2 0.2 0.1"/></geometry>
    </visual>
    <collision>
      <origin xyz="0 0 0.05" rpy="0 0 0"/>
      <geometry><box size="0.2 0.2 0.1"/></geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 0.05" rpy="0 0 0"/>
      <mass value="2.0"/>
      <inertia ixx="0.008333" ixy="0" ixz="0" iyy="0.008333" iyz="0" izz="0.013333"/>
    </inertial>
  </link>

  <joint name="shoulder" type="revolute">
    <parent link="base_link"/>
    <child link="upper_arm"/>
    <origin xyz="0 0 0.1" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-1.57" upper="1.57" effort="20" velocity="2.0"/>
    <dynamics damping="0.5" friction="0.0"/>
  </joint>

  <link name="upper_arm">
    <visual>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <geometry><cylinder radius="0.03" length="0.3"/></geometry>
    </visual>
    <collision>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <geometry><cylinder radius="0.03" length="0.3"/></geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <mass value="1.0"/>
      <inertia ixx="0.007725" ixy="0" ixz="0" iyy="0.007725" iyz="0" izz="0.00045"/>
    </inertial>
  </link>

  <joint name="elbow" type="revolute">
    <parent link="upper_arm"/>
    <child link="forearm"/>
    <origin xyz="0 0 0.3" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-2.5" upper="2.5" effort="10" velocity="3.0"/>
    <dynamics damping="0.3" friction="0.0"/>
  </joint>

  <link name="forearm">
    <visual>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <geometry><cylinder radius="0.025" length="0.25"/></geometry>
    </visual>
    <collision>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <geometry><cylinder radius="0.025" length="0.25"/></geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <mass value="0.6"/>
      <inertia ixx="0.003219" ixy="0" ixz="0" iyy="0.003219" iyz="0" izz="0.000188"/>
    </inertial>
  </link>

  <joint name="tool_joint" type="fixed">
    <parent link="forearm"/>
    <child link="tool0"/>
    <origin xyz="0 0 0.25" rpy="0 0 0"/>
  </joint>
  <link name="tool0"/>

  <transmission name="shoulder_trans">
    <type>transmission_interface/SimpleTransmission</type>
    <joint name="shoulder">
      <hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
    </joint>
    <actuator name="shoulder_motor">
      <mechanicalReduction>100</mechanicalReduction>
    </actuator>
  </transmission>

  <transmission name="elbow_trans">
    <type>transmission_interface/SimpleTransmission</type>
    <joint name="elbow">
      <hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
    </joint>
    <actuator name="elbow_motor">
      <mechanicalReduction>100</mechanicalReduction>
    </actuator>
  </transmission>
</robot>

MJCF

<mujoco model="two_link_arm">
  <compiler angle="radian" autolimits="true"/>
  <option timestep="0.002" gravity="0 0 -9.81"/>
  <default>
    <joint type="hinge" axis="0 1 0"/>
  </default>
  <worldbody>
    <geom name="floor" type="plane" size="1 1 0.1"/>
    <body name="base_link">
      <inertial pos="0 0 0.05" mass="2.0" diaginertia="0.008333 0.008333 0.013333"/>
      <geom type="box" pos="0 0 0.05" size="0.1 0.1 0.05"/>
      <body name="upper_arm" pos="0 0 0.1">
        <joint name="shoulder" range="-1.57 1.57" damping="0.5" armature="0.01"/>
        <inertial pos="0 0 0.15" mass="1.0" diaginertia="0.007725 0.007725 0.00045"/>
        <geom type="cylinder" fromto="0 0 0 0 0 0.3" size="0.03"/>
        <body name="forearm" pos="0 0 0.3">
          <joint name="elbow" range="-2.5 2.5" damping="0.3" armature="0.005"/>
          <inertial pos="0 0 0.125" mass="0.6" diaginertia="0.003219 0.003219 0.000188"/>
          <geom type="cylinder" fromto="0 0 0 0 0 0.25" size="0.025"/>
          <site name="tool0" pos="0 0 0.25"/>
        </body>
      </body>
    </body>
  </worldbody>
  <contact>
    <exclude body1="base_link" body2="upper_arm"/>
  </contact>
  <actuator>
    <position name="shoulder_pos" joint="shoulder" kp="50" kv="2" ctrlrange="-1.57 1.57" forcerange="-20 20"/>
    <position name="elbow_pos" joint="elbow" kp="30" kv="1" ctrlrange="-2.5 2.5" forcerange="-10 10"/>
  </actuator>
</mujoco>

Line by line, the differences are these. Box and cylinder sizes in MJCF are half-lengths, or given by fromto. The base box sits at pos="0 0 0.05" with half-size 0.05. The joint axes are written once, in a <default> class. Limits are radians because of <compiler angle="radian"/>. The base and upper arm are welded parent and child, so MuJoCo would let them collide. The <exclude> stops that. Without it, the shoulder stays pinned against the base and never reaches its target.

Load and step the MJCF

This block loads the model, checks the tool position against the forward kinematics written by hand, then drives both joints to a target with the position actuators.

import numpy as np
import mujoco

MJCF = """
<mujoco model="two_link_arm">
  <compiler angle="radian" autolimits="true"/>
  <option timestep="0.002" gravity="0 0 -9.81"/>
  <default>
    <joint type="hinge" axis="0 1 0"/>
  </default>
  <worldbody>
    <geom name="floor" type="plane" size="1 1 0.1"/>
    <body name="base_link">
      <inertial pos="0 0 0.05" mass="2.0" diaginertia="0.008333 0.008333 0.013333"/>
      <geom type="box" pos="0 0 0.05" size="0.1 0.1 0.05"/>
      <body name="upper_arm" pos="0 0 0.1">
        <joint name="shoulder" range="-1.57 1.57" damping="0.5" armature="0.01"/>
        <inertial pos="0 0 0.15" mass="1.0" diaginertia="0.007725 0.007725 0.00045"/>
        <geom type="cylinder" fromto="0 0 0 0 0 0.3" size="0.03"/>
        <body name="forearm" pos="0 0 0.3">
          <joint name="elbow" range="-2.5 2.5" damping="0.3" armature="0.005"/>
          <inertial pos="0 0 0.125" mass="0.6" diaginertia="0.003219 0.003219 0.000188"/>
          <geom type="cylinder" fromto="0 0 0 0 0 0.25" size="0.025"/>
          <site name="tool0" pos="0 0 0.25"/>
        </body>
      </body>
    </body>
  </worldbody>
  <contact>
    <exclude body1="base_link" body2="upper_arm"/>
  </contact>
  <actuator>
    <position name="shoulder_pos" joint="shoulder" kp="50" kv="2" ctrlrange="-1.57 1.57" forcerange="-20 20"/>
    <position name="elbow_pos" joint="elbow" kp="30" kv="1" ctrlrange="-2.5 2.5" forcerange="-10 10"/>
  </actuator>
</mujoco>
"""

model = mujoco.MjModel.from_xml_string(MJCF)
data = mujoco.MjData(model)
data.qpos[:] = [0.5, -0.8]                       # shoulder, elbow (rad)
mujoco.mj_kinematics(model, data)
q1, q2 = data.qpos
analytic = np.array([0.3 * np.sin(q1) + 0.25 * np.sin(q1 + q2), 0.0,
                     0.1 + 0.3 * np.cos(q1) + 0.25 * np.cos(q1 + q2)])
print("tool0 from MuJoCo:", np.round(data.site("tool0").xpos, 4))
print("tool0 analytic:   ", np.round(analytic, 4))

mujoco.mj_resetData(model, data)
data.ctrl[:] = [0.8, -1.2]                       # position targets (rad)
for _ in range(int(3.0 / model.opt.timestep)):
    mujoco.mj_step(model, data)
print("joint angles after 3 s:", np.round(data.qpos, 3), "(gravity pulls them off target)")
print("total mass (kg):", round(float(model.body_mass.sum()), 3))

The P-only position servo leaves a steady error under gravity: the shoulder settles at 0.843 rad for a target of 0.8 rad. Raise kp, add kv, or add gravity compensation (actuatorgravcomp on the joint in recent MuJoCo versions) if the tracking matters.

Converting

Direction Route What to check afterwards
URDF to MuJoCo mujoco.MjModel.from_xml_path("arm.urdf") loads a URDF directly. Add a <mujoco><compiler .../></mujoco> child to the URDF for mesh paths and options. Save with mujoco.mj_saveLastXML to get MJCF Bodies fused away by fusestatic, visuals dropped, no actuators (add them), joint effort now in actuatorfrcrange, contact exclusions
MJCF to URDF The MuJoCo documentation describes loading URDF, not writing it. Write the URDF by hand or use a third-party tool Ball joints and tendons have no URDF form. Free joints become floating. Joint equality constraints map only partly, to <mimic>. Sites become fixed-joint links
xacro to URDF ros2 run xacro xacro robot.urdf.xacro > robot.urdf Run the result through check_urdf, which ships with ROS 2 Jazzy, before you load it anywhere

When we loaded the URDF above with MuJoCo 3.16, base_link was fused into the world body, the visual geoms were dropped (3 geoms remain), the damping values carried over, and the joint efforts became force ranges of plus or minus 20 N m and 10 N m.

Pitfalls

  • The URDF spec requires <limit> on revolute and prismatic joints. Not every parser enforces it: urdf_parser_py accepts a revolute joint without one, so check your files yourself.
  • Inertia must be about the centre of mass and positive definite. A link with zero mass and no inertia under a moving joint makes simulators unstable.
  • The URDF joint <origin> places the child link frame. The visual, collision and inertial <origin> tags are offsets inside the link. Mixing them up moves the mass but not the shape.
  • MJCF euler uses the compiler’s eulerseq, which defaults to intrinsic xyz, not URDF’s fixed-axis order. See rotation representations.
  • MJCF quaternions are (w, x, y, z).
  • Collide with primitives where you can, and keep detailed meshes for visuals.