Build a Robot Arm URDF from Scratch

Write a URDF file describing a 3-DOF robot arm with joints and geometry.

Introduction

30 Second Summary

Every robot arm you have ever seen, from factory floor welders to surgical assistants, started as a description file that tells software exactly where each piece sits and how each joint moves.

In this project, you will write a URDF file from scratch that describes a 3-DOF robot arm. You will build it piece by piece inside VS Code with a live 3D preview, watching your robot come to life as you add each link and joint.

What You'll Build

A complete URDF (Unified Robot Description Format) file describing a 3-DOF articulated robot arm with visual geometry, movable joints, and simulation-ready physics properties.

By the end of this project, you'll have:

  • A complete URDF file describing a robot arm with a base, shoulder, elbow, and gripper.
  • A live 3D preview of your robot rendering inside VS Code with interactive joint controls you can drag.
  • A simulation-ready robot with collision geometry and inertial properties defined for every link.
  • Secret Mission: Add a prismatic gripper mechanism that opens and closes.

Are there any prerequisites?

This project assumes a basic understanding of XML syntax (tags, attributes, nesting). You do not need any robotics experience or ROS 2 installed. The URDF file you create is fully compatible with ROS 2, Gazebo, and MoveIt whenever you are ready to use them.

Before We Start

Before you write any robot description code, you need a workspace that understands URDF files and can show you a live 3D preview of your robot as you build it.

In this step, you'll install VS Code with a specialized URDF extension that provides syntax highlighting, auto-completion, and a 3D viewer right inside your editor.

In this step, get ready to:
  • Install VS Code.
  • Install the Robot Developer Extensions for URDF extension.
  • Create your project folder and URDF file.
Install VS Code

VS Code is the code editor you'll use for this entire project. It runs the URDF extension that gives you a 3D preview panel.

  • Check if you already have VS Code installed on your computer.

✔️ I already have VS Code

You're all set. Move on to the next substep.

ⓧ I don't have VS Code yet

  • Open your web browser and go to https://code.visualstudio.com/.
  • Click the download button for your operating system (macOS or Windows).
  • Run the installer and follow the prompts to complete the installation.

Once the installation finishes, VS Code is ready to use.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
Install the Robot Developer Extensions for URDF extension

This extension gives VS Code the ability to understand URDF files. It provides syntax highlighting, IntelliSense auto-completion, XML schema validation, and a built-in 3D preview panel.

  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • In the search bar at the top of the Extensions panel, type Robot Developer Extensions for URDF.
  • Find the result published by Ranch Hand Robotics and click Install.

What does this extension do?

The Robot Developer Extensions for URDF adds several tools to VS Code. You get IntelliSense suggestions as you type URDF elements, inline documentation when you hover over tags, and Go to Definition (F12) for navigating between links and joints.

Most importantly, it includes a 3D preview panel powered by BabylonJS. You'll use this to visualize your robot arm as you build it, without needing to install ROS 2 or any other robotics framework.

Don't see the extension?

Make sure you search for the full name Robot Developer Extensions for URDF and look for the publisher Ranch Hand Robotics LLC. There are several URDF-related extensions available. The correct one has over 100,000 installs.

help me find and install the Robot Developer Extensions for URDF in VS Code

Create your project folder and URDF file

Every URDF project starts with a folder to hold your robot description files. You'll create a dedicated folder and an empty .urdf file inside it.

  • Open a terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or Command Prompt (Windows), and pressing Enter.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder by running this command:
mkdir my_robot_arm
  • Back in VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the my_robot_arm folder, and click Open.

VS Code now shows the my_robot_arm folder in the Explorer sidebar on the left.

  • In the Explorer sidebar, hover over the MY_ROBOT_ARM header area and click the New File icon (a page with a plus sign).
  • Type robot_arm.urdf and press Enter.

The file opens in the editor. It's empty for now.

  • Verify the extension is working by typing <robot in the empty file.

You should see IntelliSense suggestions appear as a dropdown menu, offering to auto-complete URDF elements for you.

What should I see?

When you type <robot, a dropdown should appear with suggestions like robot and its attributes. This confirms the URDF extension is active and providing IntelliSense for your file.

  • Delete what you just typed so the file is empty again.
  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).

No IntelliSense suggestions appearing?

Make sure the file is named robot_arm.urdf with the .urdf extension. The URDF extension activates based on the file type. Also check that the extension finished installing by looking for it in the Extensions panel (it should say Disable instead of Install).

If you still don't see suggestions, try reloading VS Code by pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) and typing Reload Window.

help me get URDF IntelliSense working in VS Code

Your workspace is ready. Next up, you'll write your first URDF link and see it rendered as a 3D shape in the preview panel.

Set Up Your Workspace

Your VS Code environment is ready with the URDF extension installed and your empty robot_arm.urdf file waiting. Before you start building robot parts, you need to scaffold the file with the correct XML structure and define a color palette.

Every URDF file follows a strict XML tree structure with a single <robot> root element. In this step, you will write the boilerplate and pre-define materials so you can reference colors by name as you add links later.

In this step, get ready to:
  • Write the URDF XML boilerplate with a named robot element.
  • Define reusable material colors for the robot arm.
  • Open the 3D preview panel to confirm your file is valid.
Write the URDF boilerplate and define your materials

A URDF file starts with an XML declaration and a single <robot> element that wraps everything. Materials are defined at the top level so every link in the robot can reference them by name.

  • In VS Code, click on the robot_arm.urdf tab to make sure it is the active file.
  • Replace the empty file contents with the following code:
<?xml version="1.0"?>
<robot name="my_robot_arm">

  <!-- Materials -->
  <material name="dark_gray">
    <color rgba="0.3 0.3 0.3 1"/>
  </material>
  <material name="blue">
    <color rgba="0.2 0.2 0.8 1"/>
  </material>
  <material name="orange">
    <color rgba="0.9 0.5 0.1 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

</robot>

What does this code do?

  • <?xml version="1.0"?> declares this as an XML file so parsers know how to read it.
  • <robot name="my_robot_arm"> is the single root element every URDF requires. All links and joints go inside it.
  • Each <material> block defines a named color using RGBA values in the range 0 to 1. The fourth value (alpha) controls transparency.
  • Defining materials here means you can write <material name="blue"/> on any link later instead of repeating the color values every time.
  • Save robot_arm.urdf by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check the bottom-left status bar in VS Code. You should see no error indicators for the file.

Seeing a red squiggly underline?

Make sure your opening <robot> tag and closing </robot> tag both exist and match. XML is strict about this.

Check that every <material> element has a closing </material> tag and that the self-closing <color ... /> tags end with /> (not just >).

Still stuck? Help me fix an XML validation error in my URDF file.

Open the 3D preview panel

The URDF extension includes a built-in 3D viewer powered by BabylonJS. You can open it any time to see your robot rendered live as you edit.

  • Right-click anywhere inside the robot_arm.urdf editor and select Preview.
  • Alternatively, press Ctrl+Shift+P (macOS: Cmd+Shift+P) to open the Command Palette, then type URDF: Preview and press Enter.

A new panel opens to the side. It will be an empty 3D viewport for now because you have not added any geometry yet.

Why is the preview empty?

The 3D preview renders <visual> geometry (cylinders, boxes, spheres) from your links. Your file currently only defines materials with no links, so there is nothing to draw. You will see shapes appear in the next step when you add your first link.

Preview panel not opening?

Make sure you saved the file first. The extension needs a valid saved .urdf file to launch the preview.

Confirm the Robot Developer Extensions for URDF extension is installed by opening the Extensions sidebar (Ctrl+Shift+X) and searching for Ranch Hand Robotics. It should show as enabled.

Need more help? The URDF Preview panel is not opening in VS Code. Help me troubleshoot.

✔️ Awesome, I've got everything!

Great. Double check you have saved robot_arm.urdf and that the preview panel is open (even if empty).

ⓧ I'd like to double check the full code

Here is exactly what robot_arm.urdf should contain at this point:

<?xml version="1.0"?>
<robot name="my_robot_arm">

  <!-- Materials -->
  <material name="dark_gray">
    <color rgba="0.3 0.3 0.3 1"/>
  </material>
  <material name="blue">
    <color rgba="0.2 0.2 0.8 1"/>
  </material>
  <material name="orange">
    <color rgba="0.9 0.5 0.1 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

</robot>

Your URDF skeleton is in place with a color palette ready to go. Next up, you will add your first physical link and see it appear in the 3D preview.

Create Your First Link

Your workspace is ready with VS Code, the URDF extension, and an empty robot file waiting for its first shape.

Every robot in URDF starts with links. A link is a rigid body with a visual shape, like a single bone in a skeleton. In this step, you'll create the flat cylindrical base that your robot arm sits on, then stack a pillar on top of it.

In this step, get ready to:
  • Define the base_link with a cylinder shape.
  • Preview your robot in the 3D viewer.
  • Add a base_pillar connected with a fixed joint.
Define the base_link as a cylinder

The base_link is the foundation of your robot arm. Think of it as the heavy, flat plate bolted to a table that keeps everything stable.

  • In robot_arm.urdf, find the closing </material> tag for the white material, then add the following code below it (but above the closing </robot> tag):
  <!-- Base Link -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
  </link>

What does this code do?

This defines your first physical piece of robot:

  • <link name="base_link"> creates a rigid body named base_link. Every URDF needs at least one link.
  • <visual> tells the renderer what to draw on screen.
  • <cylinder radius="0.1" length="0.05"/> creates a short, wide disc (10 cm radius, 5 cm tall). URDF uses meters for all dimensions.
  • <material name="dark_gray"/> references the color you defined earlier.
  • Save robot_arm.urdf.

Why is there no visible change yet?

You need to open the 3D preview to see your robot. The next substep walks you through that.

Preview your URDF in 3D

The URDF extension includes a built-in 3D viewer that renders your robot right inside VS Code. This is how you'll check your work after every change.

  • Right-click anywhere inside the robot_arm.urdf editor tab.
  • Select Preview from the context menu.

Alternative preview shortcut

You can also press Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS) to open the Command Palette, then type URDF: Preview and press Enter.

A new panel opens beside your editor. You should see a single dark gray disc (your base_link cylinder) rendered in the 3D viewport.

Don't see anything in the preview?

  • Make sure you saved the file before opening the preview.
  • Check that the <robot> opening and </robot> closing tags are both present. Missing tags cause the parser to fail silently.
  • Verify your <cylinder tag uses a self-closing /> at the end, not a separate closing tag.

Help me troubleshoot why my URDF preview is not rendering anything.

Add a base_pillar with a fixed joint

Your base is flat on the ground. Now you need a vertical pillar rising from the center of it, giving the arm some height before the first movable joint.

  • In robot_arm.urdf, add the following code below the closing </link> tag of base_link (still above </robot>):
  <!-- Base Pillar -->
  <link name="base_pillar">
    <visual>
      <geometry>
        <cylinder radius="0.04" length="0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <material name="dark_gray"/>
    </visual>
  </link>

  <joint name="base_to_pillar" type="fixed">
    <parent link="base_link"/>
    <child link="base_pillar"/>
    <origin xyz="0 0 0.025" rpy="0 0 0"/>
  </joint>

What does this code do?

This adds a second piece and bolts it to the base:

  • base_pillar is a thinner, taller cylinder (4 cm radius, 20 cm tall).
  • <origin xyz="0 0 0.1"> inside the visual shifts the drawn geometry upward by 10 cm. Without this, the cylinder would be centered at the link's origin with half poking below.
  • <joint name="base_to_pillar" type="fixed"> creates an immovable connection between the two links. Fixed joints lock parts together permanently.
  • <origin xyz="0 0 0.025"> on the joint places the pillar's origin 2.5 cm above the base link's center (half the base cylinder's height), so the pillar sits right on top.

What is a joint in URDF?

A joint connects two links and defines how they relate in space. Every joint has a parent link and a child link. The child's position is described relative to the parent using the joint's <origin> tag.

A fixed joint means the child never moves relative to the parent. You'll use revolute joints in the next step to add rotation.

  • Save robot_arm.urdf.
  • Check the 3D preview panel. You should now see two dark gray cylinders stacked together: a flat disc on the bottom (base) with a thin pillar standing on top of it.

Only seeing one cylinder?

  • Make sure the joint's <parent link="base_link"/> and <child link="base_pillar"/> names match the name attributes on your <link> elements exactly. A single typo breaks the connection.
  • Confirm the pillar code is placed before the </robot> closing tag. XML outside the root element is ignored.
  • Try closing and re-opening the preview if it does not update automatically.

Help me figure out why my base_pillar isn't showing in the URDF preview.

✔️ Awesome, I've got everything!

Great. Make sure you've saved robot_arm.urdf and both cylinders appear in the 3D preview before moving on.

ⓧ I'd like to double check the full code

Here is the complete robot_arm.urdf file at this point. Compare it against yours:

<?xml version="1.0"?>
<robot name="my_robot_arm">

  <!-- Materials -->
  <material name="dark_gray">
    <color rgba="0.3 0.3 0.3 1"/>
  </material>
  <material name="blue">
    <color rgba="0.2 0.2 0.8 1"/>
  </material>
  <material name="orange">
    <color rgba="0.9 0.5 0.1 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

  <!-- Base Link -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
  </link>

  <!-- Base Pillar -->
  <link name="base_pillar">
    <visual>
      <geometry>
        <cylinder radius="0.04" length="0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <material name="dark_gray"/>
    </visual>
  </link>

  <joint name="base_to_pillar" type="fixed">
    <parent link="base_link"/>
    <child link="base_pillar"/>
    <origin xyz="0 0 0.025" rpy="0 0 0"/>
  </joint>

</robot>

You've got a solid foundation. Next up, you'll attach arm segments with revolute joints that actually rotate.

Build the Arm with Revolute Joints

Your robot has a solid foundation, but it can't move yet. The base and pillar are locked in place with a fixed joint, which means they behave like a single rigid piece.

To give your arm actual motion, you need a different joint type. Revolute joints rotate around a single axis, just like a human shoulder or elbow. In this step, you'll add two arm segments connected by revolute joints that let the arm bend.

In this step, get ready to:
  • Add a lower arm link with a revolute shoulder joint.
  • Add an upper arm link with a revolute elbow joint.
  • Preview and drag both joints to verify the arm bends correctly.
Add the lower arm and shoulder joint

The lower arm is the first moving segment of your robot. It connects to the top of the base pillar with a revolute joint that rotates around the Y-axis, giving it a forward-and-back swinging motion.

  • In robot_arm.urdf, find the closing </robot> tag at the bottom of the file.
  • Add the following code just above </robot>:
  <!-- Lower Arm -->
  <link name="lower_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.3"/>
      </geometry>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <material name="blue"/>
    </visual>
  </link>

  <joint name="shoulder_joint" type="revolute">
    <parent link="base_pillar"/>
    <child link="lower_arm"/>
    <origin xyz="0 0 0.2" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-1.57" upper="1.57" effort="100" velocity="1.0"/>
  </joint>

What does this code do?

The <link name="lower_arm"> defines a blue box (0.04m x 0.04m x 0.3m) that represents the first arm segment. The origin xyz="0 0 0.15" shifts the box's visual center upward by half its length, so the bottom of the box sits at the joint connection point.

The <joint name="shoulder_joint" type="revolute"> creates a rotating hinge between the base pillar and the lower arm. The axis xyz="0 1 0" means it rotates around the Y-axis (left-right swinging). The limit restricts rotation to -1.57 to 1.57 radians (about -90 to +90 degrees).

Why does the visual origin matter?

In URDF, a link's geometry is drawn centered at its own origin by default. If you leave the visual origin at 0 0 0, half of the box would poke below the joint. Shifting it upward by half the box height (0.15m for a 0.3m box) makes the arm extend upward from the joint, which is the natural position.

  • Save robot_arm.urdf.
  • Check the 3D preview panel. You should see a blue rectangular arm segment extending upward from the top of the dark gray pillar.

Don't see the blue arm?

  • Check that the <parent link="base_pillar"/> in your joint matches the link name exactly. Link names are case-sensitive.
  • Make sure your new code is placed before the closing </robot> tag. Any content outside the robot element is ignored.
  • Look for red squiggly underlines in the editor. These indicate XML syntax errors like unclosed tags or mismatched quotes.

Why isn't my lower arm link appearing in the 3D preview?

Add the upper arm and elbow joint

The upper arm connects to the end of the lower arm with a second revolute joint. This gives the robot an elbow that bends independently from the shoulder.

  • In robot_arm.urdf, add the following code below the shoulder_joint closing tag (still above </robot>):
  <!-- Upper Arm -->
  <link name="upper_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.25"/>
      </geometry>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <material name="orange"/>
    </visual>
  </link>

  <joint name="elbow_joint" type="revolute">
    <parent link="lower_arm"/>
    <child link="upper_arm"/>
    <origin xyz="0 0 0.3" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-2.0" upper="2.0" effort="100" velocity="1.0"/>
  </joint>

What does this code do?

The upper_arm link is a slightly shorter orange box (0.25m tall). Its visual origin is shifted up by half its height (0.125m), same pattern as the lower arm.

The elbow_joint origin is set to xyz="0 0 0.3", which positions it at the top end of the lower arm (the lower arm is 0.3m tall). This makes the upper arm connect at the tip of the lower arm. The limits of -2.0 to 2.0 radians give the elbow more range of motion than the shoulder.

  • Save robot_arm.urdf.
  • Check the 3D preview. You should now see a blue lower arm and an orange upper arm stacked above the pillar.

Arms overlapping or floating?

  • If the upper arm overlaps the lower arm, check that the elbow joint origin is xyz="0 0 0.3" (matching the lower arm's full height).
  • If there's a visible gap between segments, verify the visual origin of each link is exactly half its height (0.15 for the 0.3m lower arm, 0.125 for the 0.25m upper arm).

Help me fix my arm link positioning in URDF.

Preview and test the joints

Your arm now has two movable joints. The 3D preview lets you drag each joint to verify the arm bends in the correct direction.

  • In the 3D preview panel, look for joint sliders or draggable controls on the arm segments.
  • Drag the shoulder_joint control. The lower arm and everything attached to it should swing forward and back.
  • Drag the elbow_joint control. Only the upper arm should rotate, while the lower arm stays put.

What is a kinematic chain?

When you move the shoulder, the elbow and upper arm follow along. This happens because URDF describes a tree structure where each child link inherits its parent's position. Moving a joint higher in the chain moves everything below it.

This is the same reason your hand moves when you rotate your shoulder. Each segment in the chain carries the segments after it.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete robot_arm.urdf file at this point:

<?xml version="1.0"?>
<robot name="my_robot_arm">

  <!-- Materials -->
  <material name="dark_gray">
    <color rgba="0.3 0.3 0.3 1"/>
  </material>
  <material name="blue">
    <color rgba="0.2 0.2 0.8 1"/>
  </material>
  <material name="orange">
    <color rgba="0.9 0.5 0.1 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

  <!-- Base Link -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
  </link>

  <!-- Base Pillar -->
  <link name="base_pillar">
    <visual>
      <geometry>
        <cylinder radius="0.04" length="0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <material name="dark_gray"/>
    </visual>
  </link>

  <joint name="base_to_pillar" type="fixed">
    <parent link="base_link"/>
    <child link="base_pillar"/>
    <origin xyz="0 0 0.025" rpy="0 0 0"/>
  </joint>

  <!-- Lower Arm -->
  <link name="lower_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.3"/>
      </geometry>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <material name="blue"/>
    </visual>
  </link>

  <joint name="shoulder_joint" type="revolute">
    <parent link="base_pillar"/>
    <child link="lower_arm"/>
    <origin xyz="0 0 0.2" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-1.57" upper="1.57" effort="100" velocity="1.0"/>
  </joint>

  <!-- Upper Arm -->
  <link name="upper_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.25"/>
      </geometry>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <material name="orange"/>
    </visual>
  </link>

  <joint name="elbow_joint" type="revolute">
    <parent link="lower_arm"/>
    <child link="upper_arm"/>
    <origin xyz="0 0 0.3" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-2.0" upper="2.0" effort="100" velocity="1.0"/>
  </joint>

</robot>

Your arm has two working joints and can bend at the shoulder and elbow. Next up, you'll add a rotating wrist and gripper fingers to complete the end effector.

Add a Wrist and End Effector

Your arm can bend at the shoulder and elbow. Now it needs a way to interact with objects at its tip.

In this step, you'll add a rotating wrist and a simple two-finger gripper to the end of your robot arm. The wrist introduces a new joint type: the continuous joint, which spins without any angular limits.

In this step, get ready to:
  • Add a wrist link with unlimited continuous rotation.
  • Build a gripper with a base and two symmetrical finger links.
  • Verify all three movable joints work in the 3D preview.
Add a wrist with continuous rotation

The wrist sits at the tip of the upper arm and needs to spin freely around the Z-axis. A continuous joint is perfect here because it has no angular limits.

  • In robot_arm.urdf, find the closing </joint> tag of the elbow_joint.
  • Add the wrist link and its continuous joint below it by pasting this code:
  <!-- Wrist -->
  <link name="wrist_link">
    <visual>
      <geometry>
        <cylinder radius="0.02" length="0.04"/>
      </geometry>
      <material name="white"/>
    </visual>
  </link>

  <joint name="wrist_joint" type="continuous">
    <parent link="upper_arm"/>
    <child link="wrist_link"/>
    <origin xyz="0 0 0.25" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

What does this code do?

The wrist_link is a small white cylinder (radius 0.02m, length 0.04m) that acts as the rotating connector between the arm and the gripper.

The wrist_joint uses type="continuous" instead of revolute. A continuous joint rotates infinitely without any stop limits. The axis 0 0 1 means it spins around the Z-axis (like turning a screwdriver).

What is a continuous joint?

A continuous joint works just like a revolute joint but has no upper or lower angle limits. Think of a wheel spinning freely on an axle. Because there are no limits, you don't need a <limit> tag.

  • Save robot_arm.urdf.
  • Check the 3D preview. You should see a small white cylinder appear at the top of the orange upper arm.

Don't see the wrist cylinder?

Make sure your new code is placed after the closing </joint> of elbow_joint and before the closing </robot> tag. Also check that <parent link="upper_arm"/> matches the exact link name from earlier.

Help me debug my wrist_link placement in URDF

Build the gripper assembly

The gripper attaches to the wrist and gives your robot arm something to grab with. You'll build it from three pieces: a base plate and two parallel fingers.

  • In robot_arm.urdf, find the closing </joint> tag of the wrist_joint you just added.
  • Add the gripper base link and its fixed joint below it by pasting this code:
  <!-- Gripper Base -->
  <link name="gripper_base">
    <visual>
      <geometry>
        <box size="0.06 0.02 0.04"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
  </link>

  <joint name="gripper_base_joint" type="fixed">
    <parent link="wrist_link"/>
    <child link="gripper_base"/>
    <origin xyz="0 0 0.02" rpy="0 0 0"/>
  </joint>

What does this code do?

The gripper_base is a flat dark gray box (0.06m wide, 0.02m deep, 0.04m tall) that serves as the mounting plate for the fingers.

The gripper_base_joint is fixed because the gripper base should move rigidly with the wrist. The origin xyz="0 0 0.02" offsets it 2cm above the wrist center so it sits on top.

  • Now add both finger links below the gripper_base_joint by pasting this code:
  <!-- Left Finger -->
  <link name="left_finger">
    <visual>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
      <material name="white"/>
    </visual>
  </link>

  <joint name="left_finger_joint" type="fixed">
    <parent link="gripper_base"/>
    <child link="left_finger"/>
    <origin xyz="0.025 0 0.02" rpy="0 0 0"/>
  </joint>

  <!-- Right Finger -->
  <link name="right_finger">
    <visual>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
      <material name="white"/>
    </visual>
  </link>

  <joint name="right_finger_joint" type="fixed">
    <parent link="gripper_base"/>
    <child link="right_finger"/>
    <origin xyz="-0.025 0 0.02" rpy="0 0 0"/>
  </joint>

What does this code do?

Each finger is a thin white box (0.01m x 0.02m x 0.06m). The visual origin xyz="0 0 0.03" shifts each finger's geometry upward so it extends from the joint rather than being centered on it.

The left finger joint has origin xyz="0.025 0 0.02" and the right finger joint has origin xyz="-0.025 0 0.02". The opposite X values place them symmetrically on either side of the gripper base, forming a parallel gripper shape.

  • Save robot_arm.urdf.
  • Check the 3D preview. You should see a dark gray plate with two white finger-shaped boxes extending from it.

Fingers look misaligned or overlapping?

Double check the X values in your joint origins. The left finger should use 0.025 (positive) and the right finger should use -0.025 (negative). If both use the same sign, they'll stack on the same side.

Help me fix my gripper finger positioning in URDF

Preview the full robot arm

Your robot arm is now fully assembled with all its parts. Time to verify that every movable joint works correctly.

  • In the 3D preview panel, look for three joint sliders: shoulder_joint, elbow_joint, and wrist_joint.
  • Drag the shoulder_joint slider. The entire arm (lower arm, upper arm, wrist, and gripper) should tilt together.
  • Drag the elbow_joint slider. The upper arm, wrist, and gripper should bend independently from the lower arm.
  • Drag the wrist_joint slider. Only the gripper assembly (base and fingers) should spin around the Z-axis.

You should see a complete arm: base, pillar, lower arm, upper arm, rotating wrist, and gripper fingers.

Why don't the fingers have their own joints?

The finger joints are set to type="fixed" right now, so they can't move independently. In the Secret Mission, you'll upgrade them to prismatic joints that slide open and closed.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete robot_arm.urdf file as it should look at this point:

<?xml version="1.0"?>
<robot name="my_robot_arm">

  <!-- Materials -->
  <material name="dark_gray">
    <color rgba="0.3 0.3 0.3 1"/>
  </material>
  <material name="blue">
    <color rgba="0.2 0.2 0.8 1"/>
  </material>
  <material name="orange">
    <color rgba="0.9 0.5 0.1 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

  <!-- Base Link -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
  </link>

  <!-- Base Pillar -->
  <link name="base_pillar">
    <visual>
      <geometry>
        <cylinder radius="0.04" length="0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <material name="dark_gray"/>
    </visual>
  </link>

  <joint name="base_to_pillar" type="fixed">
    <parent link="base_link"/>
    <child link="base_pillar"/>
    <origin xyz="0 0 0.025" rpy="0 0 0"/>
  </joint>

  <!-- Lower Arm -->
  <link name="lower_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.3"/>
      </geometry>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <material name="blue"/>
    </visual>
  </link>

  <joint name="shoulder_joint" type="revolute">
    <parent link="base_pillar"/>
    <child link="lower_arm"/>
    <origin xyz="0 0 0.2" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-1.57" upper="1.57" effort="100" velocity="1.0"/>
  </joint>

  <!-- Upper Arm -->
  <link name="upper_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.25"/>
      </geometry>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <material name="orange"/>
    </visual>
  </link>

  <joint name="elbow_joint" type="revolute">
    <parent link="lower_arm"/>
    <child link="upper_arm"/>
    <origin xyz="0 0 0.3" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-2.0" upper="2.0" effort="100" velocity="1.0"/>
  </joint>

  <!-- Wrist -->
  <link name="wrist_link">
    <visual>
      <geometry>
        <cylinder radius="0.02" length="0.04"/>
      </geometry>
      <material name="white"/>
    </visual>
  </link>

  <joint name="wrist_joint" type="continuous">
    <parent link="upper_arm"/>
    <child link="wrist_link"/>
    <origin xyz="0 0 0.25" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- Gripper Base -->
  <link name="gripper_base">
    <visual>
      <geometry>
        <box size="0.06 0.02 0.04"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
  </link>

  <joint name="gripper_base_joint" type="fixed">
    <parent link="wrist_link"/>
    <child link="gripper_base"/>
    <origin xyz="0 0 0.02" rpy="0 0 0"/>
  </joint>

  <!-- Left Finger -->
  <link name="left_finger">
    <visual>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
      <material name="white"/>
    </visual>
  </link>

  <joint name="left_finger_joint" type="fixed">
    <parent link="gripper_base"/>
    <child link="left_finger"/>
    <origin xyz="0.025 0 0.02" rpy="0 0 0"/>
  </joint>

  <!-- Right Finger -->
  <link name="right_finger">
    <visual>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
      <material name="white"/>
    </visual>
  </link>

  <joint name="right_finger_joint" type="fixed">
    <parent link="gripper_base"/>
    <child link="right_finger"/>
    <origin xyz="-0.025 0 0.02" rpy="0 0 0"/>
  </joint>

</robot>

Your robot arm is fully assembled with a base, two arm segments, a rotating wrist, and a gripper. Next up, you'll add collision and inertial properties to make the whole model simulation-ready.

Add Collision and Inertial Properties

Your robot arm looks great in the 3D preview, but a physics simulator like Gazebo needs more than visual geometry to work with. It needs to know where solid surfaces are (for contact detection) and how heavy each part is (for realistic motion).

In this step, you will add collision geometry and inertial properties to every link. This makes your URDF simulation-ready.

In this step, get ready to:
  • Add collision elements to every link.
  • Add inertial elements (mass and inertia tensor) to every link.
  • Validate that your URDF is error-free and simulation-ready.
Add collision elements to every link

A <collision> element tells the physics engine the shape of each link for contact detection. For simple robots, the collision geometry is identical to the visual geometry.

  • In robot_arm.urdf, find the base_link definition. Directly after the closing </visual> tag, add this collision block:
    <collision>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
    </collision>

What does this code do?

The <collision> element mirrors the <visual> block's <geometry> and <origin> (if present) exactly. The simulator uses this shape to calculate when parts collide with the environment or other objects.

For complex robots, you might use a simpler shape for collision to speed up physics calculations. For our arm, the visual and collision shapes are the same.

Some links also have an <origin> inside their visual block. When that is the case, you need to include the same origin in the collision block.

  • Find the base_pillar link definition. After its closing </visual> tag, add this collision block (note the <origin> tag matching the visual):
    <collision>
      <geometry>
        <cylinder radius="0.04" length="0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
    </collision>
  • Now add a <collision> block to each of the remaining 6 links: lower_arm, upper_arm, wrist_link, gripper_base, left_finger, and right_finger.
  • For each link, copy the <geometry> tag and the <origin> tag (if present) from inside <visual> into a new <collision> block placed directly after the closing </visual> tag.

Quick reference for origins

Links that need an <origin> in their collision block: base_pillar, lower_arm, upper_arm, left_finger, and right_finger.

Links with NO origin in their collision block: base_link, wrist_link, and gripper_base.

  • Save robot_arm.urdf.
  • Check the 3D preview. It should still render your full arm with no errors. The visual appearance does not change because collision geometry is invisible by default.

Seeing errors in the preview?

Make sure each <collision> block is inside the <link> tags, at the same level as <visual>. Placing it outside the link or inside the visual will cause parse errors.

Double-check that you copied the geometry values exactly. A mismatched closing tag like </box> instead of a self-closing /> will break the XML.

help me fix collision element errors in my URDF

Add inertial elements to every link

Physics simulators also need to know how heavy each link is and how its mass is distributed. The <inertial> element provides a mass value (in kilograms) and a 3x3 inertia tensor.

  • In robot_arm.urdf, find the base_link definition. After the closing </collision> tag you just added, paste this inertial block:
    <inertial>
      <mass value="2.0"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>

What does this code do?

The <mass value="2.0"/> tag sets this link's weight to 2 kilograms.

The <inertia> tag describes how that mass is distributed in 3D space using 6 values. The diagonal values (ixx, iyy, izz) represent resistance to rotation around each axis. The off-diagonal values are zero because our shapes are symmetrical.

  • Add an <inertial> block to each of the remaining 7 links. The format is identical for every link. Only the mass value changes. Use these masses:
  • base_pillar: 1.0 kg.
  • lower_arm: 0.5 kg.
  • upper_arm: 0.5 kg.
  • wrist_link: 0.1 kg.
  • gripper_base: 0.1 kg.
  • left_finger: 0.05 kg.
  • right_finger: 0.05 kg.
  • For all links, use the same inertia values: ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3".
  • Place each <inertial> block after the </collision> closing tag inside each link.

Why are the inertia values approximate?

For a production robot, you would calculate exact inertia tensors from CAD models or measure them experimentally. For learning URDF structure, approximate values (1e-3 on diagonals, 0 off-diagonal) are standard placeholders that keep simulators stable.

  • Save robot_arm.urdf.
  • Check the 3D preview to confirm it still renders without errors.

Preview showing a blank model?

If the entire robot disappears, you likely have an unclosed tag or a typo in one of the inertial blocks. Check that every <inertial> block has a closing </inertial> tag.

Also confirm that <inertial> is inside the <link> tags, not inside <visual> or <collision>.

help me debug my inertial elements

Validate your complete URDF

Every link in your URDF should now have all three sub-elements: <visual>, <collision>, and <inertial>. Your file is now simulation-ready.

  • Check the **Problems** panel in VS Code (press Ctrl+Shift+M on Windows or Cmd+Shift+M on macOS) to see if the extension reports any diagnostics.
  • Confirm there are no errors listed for robot_arm.urdf.
  • Open the 3D preview one more time. Drag the shoulder, elbow, and wrist joints to confirm the full arm still moves correctly.

What makes a URDF simulation-ready?

Simulators like Gazebo require every link to have collision geometry (so objects can bump into each other) and inertial properties (so gravity and forces affect the robot realistically). Without these, the simulator either crashes or treats links as weightless ghosts.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is the complete robot_arm.urdf file with all collision and inertial properties added:

<?xml version="1.0"?>
<robot name="my_robot_arm">

  <!-- Materials -->
  <material name="dark_gray">
    <color rgba="0.3 0.3 0.3 1"/>
  </material>
  <material name="blue">
    <color rgba="0.2 0.2 0.8 1"/>
  </material>
  <material name="orange">
    <color rgba="0.9 0.5 0.1 1"/>
  </material>
  <material name="white">
    <color rgba="1 1 1 1"/>
  </material>

  <!-- Base Link -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="2.0"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <!-- Base Pillar -->
  <link name="base_pillar">
    <visual>
      <geometry>
        <cylinder radius="0.04" length="0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
      <material name="dark_gray"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.04" length="0.2"/>
      </geometry>
      <origin xyz="0 0 0.1" rpy="0 0 0"/>
    </collision>
    <inertial>
      <mass value="1.0"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <joint name="base_to_pillar" type="fixed">
    <parent link="base_link"/>
    <child link="base_pillar"/>
    <origin xyz="0 0 0.025" rpy="0 0 0"/>
  </joint>

  <!-- Lower Arm -->
  <link name="lower_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.3"/>
      </geometry>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <material name="blue"/>
    </visual>
    <collision>
      <geometry>
        <box size="0.04 0.04 0.3"/>
      </geometry>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
    </collision>
    <inertial>
      <mass value="0.5"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <joint name="shoulder_joint" type="revolute">
    <parent link="base_pillar"/>
    <child link="lower_arm"/>
    <origin xyz="0 0 0.2" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-1.57" upper="1.57" effort="100" velocity="1.0"/>
  </joint>

  <!-- Upper Arm -->
  <link name="upper_arm">
    <visual>
      <geometry>
        <box size="0.04 0.04 0.25"/>
      </geometry>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <material name="orange"/>
    </visual>
    <collision>
      <geometry>
        <box size="0.04 0.04 0.25"/>
      </geometry>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
    </collision>
    <inertial>
      <mass value="0.5"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <joint name="elbow_joint" type="revolute">
    <parent link="lower_arm"/>
    <child link="upper_arm"/>
    <origin xyz="0 0 0.3" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="-2.0" upper="2.0" effort="100" velocity="1.0"/>
  </joint>

  <!-- Wrist -->
  <link name="wrist_link">
    <visual>
      <geometry>
        <cylinder radius="0.02" length="0.04"/>
      </geometry>
      <material name="white"/>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.02" length="0.04"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.1"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <joint name="wrist_joint" type="continuous">
    <parent link="upper_arm"/>
    <child link="wrist_link"/>
    <origin xyz="0 0 0.25" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- Gripper Base -->
  <link name="gripper_base">
    <visual>
      <geometry>
        <box size="0.06 0.02 0.04"/>
      </geometry>
      <material name="dark_gray"/>
    </visual>
    <collision>
      <geometry>
        <box size="0.06 0.02 0.04"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.1"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <joint name="gripper_base_joint" type="fixed">
    <parent link="wrist_link"/>
    <child link="gripper_base"/>
    <origin xyz="0 0 0.02" rpy="0 0 0"/>
  </joint>

  <!-- Left Finger -->
  <link name="left_finger">
    <visual>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
      <material name="white"/>
    </visual>
    <collision>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
    </collision>
    <inertial>
      <mass value="0.05"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <joint name="left_finger_joint" type="fixed">
    <parent link="gripper_base"/>
    <child link="left_finger"/>
    <origin xyz="0.025 0 0.02" rpy="0 0 0"/>
  </joint>

  <!-- Right Finger -->
  <link name="right_finger">
    <visual>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
      <material name="white"/>
    </visual>
    <collision>
      <geometry>
        <box size="0.01 0.02 0.06"/>
      </geometry>
      <origin xyz="0 0 0.03" rpy="0 0 0"/>
    </collision>
    <inertial>
      <mass value="0.05"/>
      <inertia ixx="1e-3" ixy="0.0" ixz="0.0" iyy="1e-3" iyz="0.0" izz="1e-3"/>
    </inertial>
  </link>

  <joint name="right_finger_joint" type="fixed">
    <parent link="gripper_base"/>
    <child link="right_finger"/>
    <origin xyz="-0.025 0 0.02" rpy="0 0 0"/>
  </joint>

</robot>

Secret mission

Add a Prismatic Gripper

Your robot arm has a gripper, but it can't actually grip anything. The fingers are frozen in place with fixed joints. Can you bring them to life with prismatic motion so the gripper opens and closes on command?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The my_robot_arm project folder on your Desktop.
  • The Robot Developer Extensions for URDF extension in VS Code.

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend your URDF with more links and joints.

  • Your my_robot_arm folder stays on your Desktop, ready to open any time.
  • The URDF extension stays installed in VS Code for future robotics projects.

Pause - I'll come back to this later

Since this project is entirely local files, there's nothing to pause. Your files sit quietly on disk without using any resources.

  • Close the VS Code window to free up memory.
  • Your my_robot_arm folder and the URDF extension will be right where you left them when you come back.

Delete - I don't want to use this again

Remove all project files and the extension to start completely fresh.

  • Delete the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/my_robot_arm

Windows

Remove-Item -Recurse -Force ~\Desktop\my_robot_arm
  • Uninstall the URDF extension by opening VS Code, pressing Ctrl+Shift+X to open the Extensions panel, finding Robot Developer Extensions for URDF, and clicking Uninstall.

Nice Work!

Nice Work!

You just built a robot arm from pure XML. Your robot_arm.urdf file fully describes a 3-DOF articulated arm that any major robotics framework can read and simulate.

You learned how to:

  • Wrote a complete URDF file from scratch, using links, joints, visual geometry, and materials to model a 3-DOF robot arm.
  • Used three joint types (fixed, revolute, and continuous) to create movable connections and positioned links in 3D space with origin offsets.
  • Added simulation-ready collision and inertial properties to every link, making your robot compatible with Gazebo, MoveIt, and other physics engines.
  • Built a working prismatic gripper with mimic joints that opens and closes from a single control.

Ready to quiz yourself?