Skip to main content

Add a Custom Task in LeIsaac

This tutorial walks you through adding a custom task and environment in LeIsaac so you can build a variety of tasks based on it.

1. Prepare the USD Scene​

Every task environment in LeIsaac is tied to a USD scene. We assume you already have a USD file for your environment. If not, you can reuse the example scene that contains a table, a red cube, and a box. The example scene can be downloaded here.

The scene USD only needs to describe the scene itself; no robot assets are required. Once downloaded, place the file under assets/scenes in the project root.

custom_scene_usd Example scene layout in Isaac Sim.

2. Add Asset Configuration​

Once the scene file is ready, add the asset configuration in code. The root of the LeIsaac source is source/leisaac/leisaac.

In source/leisaac/leisaac, create assets/scenes/custom_scene.py with:

from pathlib import Path

import isaaclab.sim as sim_utils
from isaaclab.assets import AssetBaseCfg
from leisaac.utils.constant import ASSETS_ROOT

"""Configuration for the Custom Scene"""
SCENES_ROOT = Path(ASSETS_ROOT) / "scenes"

CUSTOM_SCENE_USD_PATH = str(SCENES_ROOT / "custom_scene" / "scene.usd")

CUSTOM_SCENE_CFG = AssetBaseCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=CUSTOM_SCENE_USD_PATH,
)
)

CUSTOM_SCENE_USD_PATH points to the USD entry file for the scene. You can rename the file or variables as needed, just update the references accordingly.

3. Implement the Task Code​

Next, implement the task logic. LeIsaac ships task templates for different robots (see the templates for more details). In this example we use the SO101 follower in a single-arm task: pick up the red cube and place it into the box.

Create tasks/custom_task/custom_task_env_cfg.py:

import torch

from isaaclab.assets import AssetBaseCfg, RigidObject
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.utils import configclass

from leisaac.assets.scenes.custom_scene import CUSTOM_SCENE_CFG, CUSTOM_SCENE_USD_PATH
from leisaac.utils.general_assets import parse_usd_and_create_subassets
from leisaac.utils.domain_randomization import domain_randomization, randomize_object_uniform

from ..template import (
SingleArmObservationsCfg,
SingleArmTaskEnvCfg,
SingleArmTaskSceneCfg,
SingleArmTerminationsCfg,
)


@configclass
class CustomTaskSceneCfg(SingleArmTaskSceneCfg):
"""Scene configuration for the custom task."""

scene: AssetBaseCfg = CUSTOM_SCENE_CFG.replace(prim_path="{ENV_REGEX_NS}/Scene")


def cube_in_box(env, cube_cfg: SceneEntityCfg, box_cfg: SceneEntityCfg, x_range: tuple[float, float], y_range: tuple[float, float], height_threshold: float):
"""Termination condition for the object in the box."""
done = torch.ones(env.num_envs, dtype=torch.bool, device=env.device)

box: RigidObject = env.scene[box_cfg.name]
box_x = box.data.root_pos_w[:, 0] - env.scene.env_origins[:, 0]
box_y = box.data.root_pos_w[:, 1] - env.scene.env_origins[:, 1]

cube: RigidObject = env.scene[cube_cfg.name]
cube_x = cube.data.root_pos_w[:, 0] - env.scene.env_origins[:, 0]
cube_y = cube.data.root_pos_w[:, 1] - env.scene.env_origins[:, 1]
cube_z = cube.data.root_pos_w[:, 2] - env.scene.env_origins[:, 2]

done = torch.logical_and(done, cube_x < box_x + x_range[1])
done = torch.logical_and(done, cube_x > box_x + x_range[0])
done = torch.logical_and(done, cube_y < box_y + y_range[1])
done = torch.logical_and(done, cube_y > box_y + y_range[0])
done = torch.logical_and(done, cube_z < height_threshold)

return done


@configclass
class TerminationsCfg(SingleArmTerminationsCfg):
"""Termination configuration for the custom task."""
success = DoneTerm(
func=cube_in_box,
params={
"cube_cfg": SceneEntityCfg("cube"),
"box_cfg": SceneEntityCfg("box"),
"x_range": (-0.05, 0.05),
"y_range": (-0.05, 0.05),
"height_threshold": 0.10,
},
)


@configclass
class CustomTaskEnvCfg(SingleArmTaskEnvCfg):
"""Configuration for the custom task environment."""

scene: CustomTaskSceneCfg = CustomTaskSceneCfg(env_spacing=8.0)

observations: SingleArmObservationsCfg = SingleArmObservationsCfg()

terminations: TerminationsCfg = TerminationsCfg()

task_description: str = "pick up the red cube and place it into the box."

def __post_init__(self) -> None:
super().__post_init__()

self.viewer.eye = (-0.2, -1.0, 0.5)
self.viewer.lookat = (0.6, 0.0, -0.2)

self.scene.robot.init_state.pos = (0.35, -0.64, 0.01)

parse_usd_and_create_subassets(CUSTOM_SCENE_USD_PATH, self)

domain_randomization(
self,
random_options=[
randomize_object_uniform(
"cube",
pose_range={
"x": (-0.05, 0.05),
"y": (-0.05, 0.05),
"z": (0.0, 0.0),
},
),
randomize_object_uniform(
"box",
pose_range={
"x": (-0.05, 0.05),
"y": (-0.05, 0.05),
"z": (0.0, 0.0),
},
),
],
)

Here are some notes on the code:

  • CustomTaskSceneCfg inherits SingleArmTaskSceneCfg and sets the scene field to CUSTOM_SCENE_CFG.
  • TerminationsCfg inherits SingleArmTerminationsCfg. It keeps the default timeout and adds cube_in_box, which checks cube and box positions to decide success of task.
  • CustomTaskEnvCfg inherits SingleArmTaskEnvCfg and supplies scene, observations, and terminations. The default observations include joint positions/velocities, actions, and more. You can also add any custom observations you need.
  • In __post_init__, you can further adjust the environment configuration:
    • viewer.eye / viewer.lookat define the IsaacSim UI viewport when you launch this task.
    • scene.robot.init_state.pos sets the robot spawn pose.
    • parse_usd_and_create_subassets extracts sub-assets from the USD into the interactive scene.
    • domain_randomization adds randomness; for example, randomize_object_uniform jitters object poses within a range at every reset.

4. Register the Environment​

Finally, register the task environment by adding tasks/custom_task/__init__.py:

import gymnasium as gym

gym.register(
id="LeIsaac-SO101-CustomTask-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.custom_task_env_cfg:CustomTaskEnvCfg",
},
)

5. Run Your Task​

With the task registered, launch it with the standard scripts. For example, start it via the teleoperation script:

python scripts/environments/teleoperation/teleop_se3_agent.py \
--task=LeIsaac-SO101-CustomTask-v0 \
--teleop_device=so101leader \
--num_envs=1 \
--device=cuda \
--enable_cameras

custom_task Custom task running with teleoperation.