Skip to main content
An RL Task is a workspace-scoped, optionally environment-linked entity that packages a runnable reinforcement-learning plugin. Cyberwave generates the SceneCfg from a linked environment — the same MuJoCo scene used everywhere else in the platform — and you bring the rest of the MDP and RL code as an arbitrary tree of source files. Exporting an RL task produces a ZIP that drops directly into cyberwave-rl/tasks/<task_id>/, where the existing registry discovers it and make run_cfg runs it like any other task.
Cyberwave currently supports basic mjlab: exported tasks are mjlab-compatible (scene config, action and observation terms, skrl training). We’re planning to release a richer Cyberwave mjlab extension with useful helpers in the future.

Why a separate entity?

RL tasks are deliberately kept independent of Environments. Deleting an environment detaches any linked RL tasks (sets environment to none) instead of cascading the delete, so a task keeps its source tree even after the world it was originally trained against is gone. This matches how researchers actually iterate: the SceneCfg is a moving target, while the reward, action, and observation specs are the long-lived artefacts.

Lifecycle

1

Create

Create an RL task from /rl-tasks, or from File → Export → Create RL task from this environment in the environment editor.
2

Import or author source

Import an existing task ZIP (for example an in-tree task from cyberwave-rl/tasks/) to seed the source tree, or write files directly in the Files tab.
3

Configure entrypoints

Set the entrypoint paths — scene_cfg_path, env_cfg_path / env_cfg_factory, rl_cfg_path / rl_cfg_factory, and an optional registry_path. Cyberwave never forces a particular from … import … statement; the import wiring is yours.
4

Regenerate SceneCfg

Regenerate the Cyberwave-generated SceneCfg module from the linked environment. This also rewrites the spec-derived files (scene_bindings.py, actions.py, observations.py, task_export/, and the strict task spec).
5

Export and run

Download the ZIP and drop the folder into cyberwave-rl/tasks/. Run it with make run_cfg arg_overrides="--task=<id>" exactly like the in-tree examples.
6

Round-trip

Edit locally, then re-import the same folder back into Cyberwave to keep the editor in sync. The editor stays authoritative for Cyberwave-generated files.

Editor tabs

The RL task editor is organized into tabs that map onto the export contract:

Author from Python (SDK)

Everything the editor tabs produce can also be authored from the Python SDK — useful for reproducible setup scripts. The RLTaskClient covers every tab via its set_*_spec methods, so a script and the dashboard produce the same specs. The Actions, Observations, and RL config tabs also ship make_* builder helpers; the command tabs are authored as plain dicts passed to set_training_command_spec / set_inference_command_spec (see step 4b below). The example below is the OpenArm pick-up task: a fixed-base arm, a table, a free-floating cube, and a wrist depth camera. It is the same scene used in the full RL task walkthrough, trimmed to the essentials.
rl.get_orchestration_hints(task_uuid) returns the scene-derived dropdown data (articulated entities, actuator groups, actuated vs passive joint names, available sensors) the editor uses to populate its pickers — call it first to discover the exact entity and joint names before authoring specs.
For the complete version — scaffolding the workspace, environment, and twins, wiring a wrist depth camera and contact sensors, training, and deploying the policy — follow the RL task walkthrough.

Actions

Action terms are authored from five control-mode presets: position, velocity, and effort mirror the Isaac Lab / mjlab JointPositionActionCfg / JointVelocityActionCfg / JointEffortActionCfg math one-to-one — exactly the target = action × scale + offset that mjlab implements in BaseAction.process_actions. position_delta is Cyberwave’s own preset: baseline_delta is an explicit per-step bias baked into the target, so an action of zero can drift the joints toward a goal pose. smoothed_position_delta is the same delta, but spread evenly across the policy step’s physics substeps for a smoother velocity profile. A single Use default joint offset checkbox mirrors mjlab’s use_default_offset: when on, mjlab replaces offset with each joint’s default_joint_pos (or default_joint_vel) at runtime so action zero returns the joint to its home pose. Every numeric field (scale, offset, baseline_delta) has a Per joint toggle that switches it between a uniform scalar and a {joint_name: value} mapping. The backend rejects per-joint keys that aren’t in the action’s selected joints, so typos surface at save time. Legacy symbolic offsets ("default", "zero", "none") keep round-tripping unchanged. The Scene tab is where you enable simulation actuators for an entity (which Xml*ActuatorCfg wrapper drives each joint); the Actions tab is where you pick the policy-controlled subset of those actuators. Each action row renders a schema-backed joint table (Joint, Control type, Position min/max, Max velocity, Max torque/force, KP, KD, Selected) with Select all / Clear. Passive joints, spring-only actuators, and joints driven by an incompatible wrapper appear greyed out with a hint. Incompatible presets are hidden rather than shown disabled. Concrete joint names are stored in target_names_expr; a collapsed Advanced — custom regex disclosure lets imported or legacy specs keep regex patterns.
One action per joint. Every (entity, joint) pair can be assigned to at most one action term — the policy emits a single value per actuated DOF. The Actions tab enforces this in three places: joints already controlled by another row render disabled with an “owned by …” pill; Select all only picks unclaimed joints; and PUT /api/v1/rl-tasks/{uuid}/actions rejects overlapping payloads with an error naming both action names and a sample of overlapping joints. To split a robot across two terms (for example arm + gripper), give each term a disjoint subset of joints.

Observations

Observation terms are organized into named groups (policy, critic, …) and drawn from a catalog: Link terms pick bodies from a scene-derived checkbox list and expose Components toggles so you only export the axes the policy needs. Joint observation terms reuse the shared Joint target picker and — unlike actions — may include both actuated and passive joints, with “All actuated”, “All joints”, “Passive joints”, and “Pick specific joints” exposed directly; “Custom regex” sits behind a small Advanced disclosure.
One group is authored, and the runtime aliases the missing critic group onto policy so both heads see the same tensor.
Observation rows show the term type as a compact chip next to the editable name; the type is fixed after creation (remove and re-add to change it).

RL config

The RL config tab persists the skrl backend, the algorithm (PPO or SAC), a structured graph-based Network builder, the trainer parameters (timesteps, intervals, seed), and the algorithm-config knobs (learning rate, discount factor, ratio clip, batch size, …). Switching algorithm seeds matching defaults. Experiment paths (experiment.name / experiment.directory) default to values derived from the RL task slug and are shown read-only; toggle Override experiment paths to write custom values.

Network builder

The network builder is graph-first: it renders the network as a DAG of typed nodes on an inline canvas, with one canvas per role for the active topology.
  • shared uses the same architecture for both heads. For PPO this yields a true shared-trunk model (SharedRLModelCfg); for SAC it instantiates two independent modules (policy + Q) with the same block spec, because SAC heads can’t share weights.
  • separated lets you author one graph per role (policy / value for PPO, policy / q for SAC).
Top-level presets are graph templates (for example PPO – shared MLP, SAC – separated MLP) that materialise the equivalent observation → trunk → head topology directly on the canvas. The palette covers inputs (observation_input), dense layers (mlp, residual_mlp), transforms (activation, layer_norm), recurrent layers (rnn, gru, lstm), vision (cnn_backbone with NatureCNN / IMPALA presets), merge nodes (concat, residual_add), and a role/algorithm-aware Heads section that surfaces actor_head + value_head for PPO and actor_head + q_head for SAC. Drag the canvas background to pan, drag a node to reposition it (positions persist in graphs[role].layout), and click an edge to delete it. Clicking a node opens an inline inspector below the canvas with the outputs picker (features_node_id / head_node_id) and the per-node editor. An Open graph builder button pops the same graph into a fullscreen dialog for serious authoring. observation_input nodes pick a group and a subset of term names from the Observations tab. Each term shows read-only tensor metadata — an image-like / flat badge and a short kind label — so the builder can route image-like terms into a cnn_backbone and reject flat terms feeding one. Gaussian log-std controls (initial_log_std, min_log_std, max_log_std) live in the actor head inspector and apply only to the stochastic Gaussian policy.
Legacy {kind, hidden_units, activation} and architecture: "mlp" / "residual_mlp" / "custom" specs are transparently promoted to a graph on load, so older tasks just render. At runtime, from runners.skrl.network_builder import build_skrl_model_cfg maps the persisted spec onto rlmodule.MlpCfg / ResidualMlpCfg / SharedRLModelCfg / SeparatedRLModelCfg for the skrl runner.
Verify before saving. The Verify button next to Save RL config runs the same validation the save path uses — without persisting — and reports the first problem (for example “mlp needs exactly 1 input; got 2 — merge multiple branches with a Concat node first”). The inline canvas also flags structural issues live as red error badges. A passing Verify guarantees Save will succeed.

Runtime: target, accelerator, and bundle

Open the Runtime tab to choose how the exported plugin runs:
  1. Pick a runtime_targetcyberwave-rl-experimental (plain local Python, no Docker) or cyberwave-rl (the dockerized stack).
  2. Pick a runtime_acceleratorgpu is the default; cpu is the smoke-friendly opt-in.
  3. Pin a single runtime_bundle version (default latest). The bundle ships mjlab + the chosen runtime + skrl together, so callers only ever pin one version.
  4. Configure the entrypoints (scene_cfg_path, env_cfg_path / env_cfg_factory, rl_cfg_path / rl_cfg_factory, optional registry_path) inline.
  5. Copy the matching Train locally snippet for the selected target and accelerator.
Both runtimes consume the same exported plugin contract, and the runtime spec (target + accelerator + versions + policy interface) is persisted on the RLTask and forwarded to checkpoints and inference dispatch with a task → checkpoint → launch merge order.
Real training expects a CUDA GPU.

Policies and inference

The Policies tab is the post-training counterpart to the Runtime tab’s “Train locally” snippets: upload a trained checkpoint, publish it as a controller, and run inference. Inference deliberately does not run MuJoCo — the worker loops Cyberwave joint state through the policy and publishes commands over MQTT. See the dedicated Uploading Policies page for the full checkpoint upload, publish, and inference flow, and Online Controllers for serving a published policy.

What’s in the exported ZIP?

The export is a minimal Python package that drops into cyberwave-rl/tasks/<task_id>/:
  • __init__.py — generated header that imports your registry (or the default one).
  • Your source tree — exactly as you authored it, with arbitrary nesting and free file structure.
  • scene_cfg.py (at the configured scene_cfg_path) — the Cyberwave-generated SceneCfg, regenerated from the linked environment.
  • scene_bindings.py — semantic role / twin / sensor aliases plus ACTION_SPEC, OBSERVATION_SPEC, and RL_CONFIG_SPEC.
  • actions.py (make_actions()) and observations.py (make_observations()) — the named wrappers.
  • task_export/ — the runtime adapters those wrappers import.
  • cyberwave_task_spec.py — the strict, canonical Python interchange for scene entities.
  • cyberwave_export.json — the export manifest (records runtime target, accelerator, and version pins).
  • MuJoCo assets — one merged models/mujoco_scene.xml, one per-entity models/<name>.xml carved out of it at build time (so each EntityCfg.spec_fn can MjSpec.from_file its own slice), and the shared models/assets/ meshes.
  • registry.py — a fallback registry wiring env_cfg / rl_cfg factories into register_cyberwave_task, used only when you don’t ship your own registry_path.

Cyberwave-generated vs user-written

scene_bindings.py is the stable interface between Cyberwave-generated scene metadata and user-written training code. It exposes role aliases (ROBOT_ENTITY, OBJECT_ENTITY, SUPPORT_ENTITY), twin namespace prefixes, namespaced joint-position regex keys, actuator target regexes, camera attach paths, contact sensor names, and a body_pattern(entity, suffix) helper. Consume these constants from env_cfg.py so renaming a twin or moving a camera in the composer regenerates the bindings without touching MDP code. The editor owns Scene entities, Actions, Observations, RL config, and all generated artefacts. Task-local Python — reward functions, reset / termination logic, metrics, command shaping, and custom observation functions — stays user-written under task_sources/. Three execution-only modules in cyberwave-rl consume the persisted specs so user-written files only have to provide rewards, resets, terminations, commands, and custom observation functions:
ZIP import is intentionally document-only for the generated paths: scene_cfg.py, scene_bindings.py, and cyberwave_task_spec.py are skipped on import and the response carries an explicit warning. Scene entities are not auto-restored from a bundled cyberwave_task_spec.py — re-compose them in the editor. The export ZIP always carries fresh bytes for the Cyberwave-generated files, so re-running the composer (or hitting regenerate-scene-cfg) is enough to refresh the artefact.

API surface

All endpoints are under /api/v1/rl-tasks and require authentication. The RLTask response surfaces the action, observation, and RL-config specs as action_spec, observation_spec, and rl_config_spec JSON fields. It also surfaces the two command specs split by lifecycle — training_command_spec and inference_command_spec — each paired with an explicit lifecycle switch (training_command_setup_enabled / inference_command_setup_enabled). Unlike the actions/observations/rl-config specs (which have dedicated GET/PUT endpoints), the command specs and their switches are read from the RLTask response and written through POST/PUT /api/v1/rl-tasks/{uuid} (mirrored by the SDK’s set_training_command_spec / set_inference_command_spec).
The spec GET, the regenerate endpoints, and the export ZIP all produce byte-identical output for the same scene-entity rows, so re-reading and re-exporting are always consistent. If a body can’t be resolved while splitting the MuJoCo scene, the request fails with HTTP 400 naming the failing entity and the selectors that were tried — check the twin selectors and re-export.

Inference Commands (stub)

RL tasks can declare inference commands that define a live control surface for deployed policies. Each command declares:
  • A payload schema (typed fields like target_position: vec3, object_position: vec3)
  • A source binding (transport + topic, e.g. MQTT)
When you deploy an RL task controller, the Online Controllers panel shows schema-driven inputs matching your inference command spec. Submitting values sends them to the running controller over MQTT, updating the policy’s command observations in real time. Common use cases:
  • Position controllers: Set a target EE pose for reach tasks
  • Waypoint controllers: Set the detected object center for inspection trajectory generation
Commands are authored as first-class terms, like observation terms: each term references a CommandTermCfg subclass (module.symbol) implemented in the task’s env_cfg.py. Add and edit terms in the editor’s Commands tab (term name, module.symbol, lifecycle fields, kwargs) or from the SDK (set_training_command_spec / set_inference_command_spec); the term’s Python implementation lives in the task’s source files. The Commands tab has two explicit lifecycle switches — Training command setup and Inference command setup. Turning one off clears its spec by design (a disabled lifecycle is “deliberately off”, not a forgotten upload); turning it on requires at least one code-backed term. From the SDK, pass enabled= to set_training_command_spec / set_inference_command_spec to set a switch explicitly, or omit it and the switch is derived from whether the spec has commands.

Where to go next

RL Task Walkthrough

Build the OpenArm pick-up task end-to-end in Python, from empty workspace to deployed policy.

Uploading Policies

Upload trained checkpoints, publish them as controllers, and run inference.

Online Controllers

Serve a published policy as a live controller.

RL Task Scene Sensors

Configure cameras and contact sensors on scene entities.

Simulation

Build the MuJoCo environment that backs your RL task’s SceneCfg.