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 (setsenvironment 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. TheRLTaskClient 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.
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.
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.
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.shareduses 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.separatedlets you author one graph per role (policy/valuefor PPO,policy/qfor SAC).
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.Runtime: target, accelerator, and bundle
Open the Runtime tab to choose how the exported plugin runs:- Pick a
runtime_target—cyberwave-rl-experimental(plain local Python, no Docker) orcyberwave-rl(the dockerized stack). - Pick a
runtime_accelerator—gpuis the default;cpuis the smoke-friendly opt-in. - Pin a single
runtime_bundleversion (defaultlatest). The bundle shipsmjlab+ the chosen runtime +skrltogether, so callers only ever pin one version. - Configure the entrypoints (
scene_cfg_path,env_cfg_path/env_cfg_factory,rl_cfg_path/rl_cfg_factory, optionalregistry_path) inline. - Copy the matching Train locally snippet for the selected target and accelerator.
RLTask and forwarded to checkpoints and inference dispatch with a task → checkpoint → launch merge order.
- cyberwave-rl (Docker)
- cyberwave-rl-experimental (local Python)
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 intocyberwave-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 configuredscene_cfg_path) — the Cyberwave-generated SceneCfg, regenerated from the linked environment.scene_bindings.py— semantic role / twin / sensor aliases plusACTION_SPEC,OBSERVATION_SPEC, andRL_CONFIG_SPEC.actions.py(make_actions()) andobservations.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-entitymodels/<name>.xmlcarved out of it at build time (so eachEntityCfg.spec_fncanMjSpec.from_fileits own slice), and the sharedmodels/assets/meshes. registry.py— a fallback registry wiringenv_cfg/rl_cfgfactories intoregister_cyberwave_task, used only when you don’t ship your ownregistry_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:
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)
- Position controllers: Set a target EE pose for reach tasks
- Waypoint controllers: Set the detected object center for inspection trajectory generation
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.