Environment Definition#

An Arena environment is three parts — scene, embodiment, and task (see the section overview). This page covers the two ways to define that composition, and where they diverge.

Both produce the same object: an IsaacLabArenaEnvironment. Environment Builder turns that into a runnable Isaac Lab environment:

Python factory and Graph YAML both produce an IsaacLabArenaEnvironment, which ArenaEnvBuilder compiles into a ManagerBasedRLEnv.
  • Python — a registered ArenaEnvironmentCfg. build() constructs the scene, embodiment, and task in code.

  • YAML — an ArenaEnvGraphSpec: a declarative environment graph. Nodes are assets; edges are spatial relations and task parameters. from_yaml() validates on load; to_arena_env() returns the same IsaacLabArenaEnvironment.

Fixed poses in YAML#

For assets whose constructors accept initial_pose, set it through params.initial_pose on embodiment, background or object nodes:

params:
  initial_pose:
    position_xyz: [-1.0, 0.0, 0.0]
    rotation_xyzw: [0.0, 0.0, 0.0, 1.0]

Positions are environment-local, in metres; quaternions use xyzw order. position_xyz is required; omitted rotation_xyzw defaults to identity, as in Pose.from_dict. The pose is applied at construction. Rigid objects and articulations also restore it on reset when pose resets are enabled; static background geometry remains at its construction pose. Do not combine it with relation placement for the same movable asset.

The same environment, side by side#

Same pick-and-place setup in both columns: a DROID arm picks a Rubik’s cube off a table and places it in a bowl. Full examples, same order, matching # markers — read across to see the correspondence.

YAML

Python

# Environment name
env_name: pick_and_place_maple_table

# Embodiment
embodiment:
  id: droid_abs_joint_pos
  registry_name: droid_abs_joint_pos

# Scene (lighting injected automatically)
background:
  id: maple_table
  registry_name: maple_table_robolab
objects:
- id: cube
  registry_name: rubiks_cube_hot3d_robolab
- id: bowl
  registry_name: bowl_ycb_robolab

# Sub-asset prims
object_references:
- id: table
  parent_id: maple_table
  prim_path: table
  object_type: rigid

# Layout
relations:
- kind: is_anchor
  subject: table
- kind: 'on'
  subject: cube
  reference: table
- kind: 'on'
  subject: bowl
  reference: table

# Task
task:
  composition: atomic
  description: Pick up the Rubik's cube and place it in the bowl.
  subtasks:
  - kind: PickAndPlaceTask
    params:
      pick_up_object: cube
      destination_location: bowl
      background_scene: maple_table
      episode_length_s: 20.0
@register_environment
class PickAndPlaceMapleTable(ArenaEnvironmentFactory):
    # Identity
    name = "pick_and_place_maple_table"

    def build(self, cfg):
        get = self.asset_registry.get_asset_by_name

        # Embodiment
        embodiment = get("droid_abs_joint_pos")(
            enable_cameras=cfg.enable_cameras,
        )

        # Scene (light must be added explicitly)
        maple_table = get("maple_table_robolab")()
        cube = get("rubiks_cube_hot3d_robolab")()
        bowl = get("bowl_ycb_robolab")()
        light = get("light")()

        # Sub-asset prims
        table = ObjectReference(
            name="table",
            prim_path="{ENV_REGEX_NS}"
                      "/maple_table_robolab/table",
            parent_asset=maple_table,
            object_type=ObjectType.RIGID,
        )

        # Layout
        table.add_relation(IsAnchor())
        table.set_initial_pose(Pose.identity())
        cube.add_relation(On(table))
        bowl.add_relation(On(table))

        # Task
        task = PickAndPlaceTask(
            pick_up_object=cube,
            destination_location=bowl,
            background_scene=maple_table,
            episode_length_s=20.0,
        )

        return IsaacLabArenaEnvironment(
            name=self.name,
            embodiment=embodiment,
            scene=Scene(assets=[
                maple_table, light, cube, bowl, table,
            ]),
            task=task,
        )

Where the two formats differ#

Three buckets:

  • Only Python

  • Only YAML

  • Same key, different behavior

Only in Python#

IsaacLabArenaEnvironment takes ten constructor arguments. build_arena_env_from_graph_spec() fills the graph-owned scene, task, placement, physics-backend, and compiled-config callback fields. The remaining runtime integrations have no YAML key.

Teleoperation device. YAML never sets env_cfg.teleop_devices. Python can pass a device that drives the embodiment:

teleop_device = self.device_registry.get_device_by_name(cfg.teleop_device)()
return IsaacLabArenaEnvironment(..., teleop_device=teleop_device)

RL interoperability. Gym kwargs for RL frameworks are Python-only. You cannot define an RL-training environment in YAML:

return IsaacLabArenaEnvironment(
    ...,
    rl_framework_entry_point="rsl_rl_cfg_entry_point",
    rl_policy_cfg="my_module:RLPolicyCfg",
)

Patching the compiled config. env_cfg_callback runs after Arena builds the ManagerBasedRLEnvCfg. Use it for viewport, decimation, physics, or anything else on that config:

def set_viewport(env_cfg):
    env_cfg.viewer.eye = (1.5, 1.5, 1.0)
    env_cfg.viewer.lookat = (0.0, 0.0, 0.5)

return IsaacLabArenaEnvironment(..., env_cfg_callback=set_viewport)

Extra episode recorder terms. Per-episode signals beyond the built-ins:

return IsaacLabArenaEnvironment(
    ...,
    episode_recorder_terms={"cube_pose": EpisodeRecorderTermCfg(func=record_cube_pose)},
)

Extra variations. Both formats get the defaults an asset attaches in its constructor (a light brings intensity, color, and HDR). Anything the asset class does not declare needs Python:

pick_up_object.add_variation(MyCustomVariation(pick_up_object))

Calling methods on assets. YAML params go to constructors only. Method calls on a live instance have no YAML form:

directional_light.set_dome_light(dome_light)

Only in YAML#

Load-time validation. The spec is Pydantic. Registry names, node ids, relation arity, task params, and check subsets fail when the file loads — before Isaac Sim starts. Python has no equivalent; the same mistakes show up later as an AttributeError or a broken scene:

AssertionError: Unknown asset registry_name 'rubiks_cube_hot3d'
AssertionError: Relation kind 'is_anchor' must not define relation.reference
AssertionError: composition 'atomic' requires exactly one atomic task

Automatic default lighting. No light-tagged asset and no light in the background USD? The loader adds a dome light and a directional light (off until a lighting-direction variation turns it on). In Python, no light means no light.

Reuse across task files. external_yaml reuses external definitions across tasks. It takes a single path or a list of paths, relative to the including file. For example:

external_yaml:
- ../scenes/bagel_plate_banana_bowl.yaml
- ../shared_defaults.yaml
env_name: banana_on_plate
task:
  composition: atomic
  ...

All external YAML files are merged into one spec under the following rules:

  • A value can be defined in only one file. A file can never silently override another, even if both files hold the same value.

  • Exception: lists of dicts with an id (such as objects:) are concatenated in file order. Each id may appear only once across all files. Other lists, such as relations, may be defined in only one file.

  • Nested includes are not allowed. An included file cannot declare external_yaml itself.

Declared CLI overrides. cli_override_specs turns a node into a CLI flag with no code change. Below, --object replaces that node’s registry_name:

cli_override_specs:
- arg: object
  target_node_id: rubiks_cube_hot3d_robolab

Machine authoring. Graph YAML is the output of Agentic Environment Generation: natural language in, validated ArenaEnvGraphSpec out. Pydantic checks registry names, node ids, relation arity, and task params before the simulator sees anything.

Graph specs may also declare default_physics_backend and env_cfg_override; see Graph YAML env_cfg_override and Physics backend selection.

Same key, different behavior#

Skip these when porting and you will not get the same environment.

Placement tuning. YAML placer_params is a validated nested override of ObjectPlacerParams and its data-only child configs. Field names and types come directly from those dataclasses, so nested solver settings do not need a second schema:

placer_params:
  placement_seed: 42
  random_yaw_init: true
  resolve_on_reset: false
  min_unique_layouts_per_env: 20
  allow_best_loss_fallbacks: false
  solver_params:
    clearance_m: 0.0

The same block selects enabled_checks, required_checks, and placement debug output. Runtime/code-bearing values such as reachability_config.embodiment and solver_params.strategies are rejected. Explicit builder/CLI placement_seed and resolve_on_reset values take precedence over YAML.

Duplicate instances need unique names. YAML sets name to the node id. Library object classes default it to the registry name, so in Python two objects created from the same class share one name unless you set name. Prim paths, event terms, and observation keys all key off that name:

# Two objects of one class. Give each its own name.
banana_a = get("banana_ycb_robolab")(name="banana_a")
banana_b = get("banana_ycb_robolab")(name="banana_b")

An anchor without a pose. YAML gives an is_anchor subject with no pose Pose.identity() — the layout origin for the relation solver. Python leaves the anchor with no pose until you set one:

table.add_relation(IsAnchor())
table.set_initial_pose(Pose.identity())

Relative prim paths. An object-reference prim_path that does not start with {ENV_REGEX_NS}/ expands to {ENV_REGEX_NS}/<background registry_name>/<prim_path>. Always the background registry name — even if parent_id points elsewhere. Python takes the path verbatim; it must be the full runtime path.

Cameras. YAML forwards enable_cameras into the embodiment params. A Python factory that ignores cfg.enable_cameras builds a camera-less env even when the flag is set. Policies that expect images will not be amused:

embodiment = get("droid_abs_joint_pos")(enable_cameras=cfg.enable_cameras)

Openable references. YAML picks the class from the fields: openable_joint_name in params → OpenableObjectReference (and object_type: articulation); otherwise → ObjectReference. Python: you pick. Use the plain class for a door and you get a door with nothing to open.

Object set members. YAML constructs set members with no constructor args, so they cannot carry a usd_path. SimReady assets fail at load time and cannot be set members. Python RigidObjectSet takes live instances — SimReady is fine:

RigidObjectSet(name="bottles", objects=[simready_bottle, ycb_bottle])

How to spawn an environment#

YAML

Python

python isaaclab_arena/scripts/environment_runner.py \
  --env_spec \
  isaaclab_arena_environments/robolab/tasks/banana_on_plate.yaml
python isaaclab_arena/scripts/environment_runner.py \
  pick_and_place_maple_table \
  --embodiment droid_rel_joint_pos \
  --pick_up_object banana_ycb_robolab \
  --destination_location bowl_ycb_robolab

Or load a graph YAML directly:

spec = ArenaEnvGraphSpec.from_yaml("path/to/env_graph.yaml")
environment = spec.to_arena_env()
env = ArenaEnvBuilder(environment, ArenaEnvBuilderCfg()).make_registered()

Choosing between them#

Many assets and relations? Start with YAML as it is validated, and machine-generatable so you can focus on the scene layout and task definition. Reach for Python when the YAML cannot express what you need: RL registration, teleop, ManagerBasedRLEnvCfg patches, placement parameters, and so on.

Next Steps#

Environment Builder shows how either definition becomes a runnable Isaac Lab environment.