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

You can also describe a robot task from the dashboard or the environment editor’s Agent tab. Type @ (or choose Tag asset or scene object) to select an existing scene object or a catalog asset by name/full slug. Tagging adds context; it does not create an instance or start training. The assistant can look for a compatible policy or prepare a training proposal when the task is supported. Review and confirm training separately from binding or running its result. The same training card appears in chat and Policies, showing reported epochs, episodes or transitions as they advance. These counts are progress, not a success score. Scene built on export means the configured scene entities will generate the scene when you export; you do not need to save a generated scene file first. Scene source saved identifies a saved source, not a compilation or evaluation result. If the editor reports Scene setup needed, add scene entities or use Scene entities → Regenerate generated files, then prepare a fresh training request when its inputs change. Use Cancel training on that card to review and cancel only its active attempt. Cancellation prevents a new result from registering; worker shutdown can still be in progress. Existing policies and recordings remain unchanged. After failure or cancellation, Prepare a new attempt opens a fresh proposal using the previous settings. Review and confirm it before training starts. If confirmation reports changed inputs, or the proposal has expired, choose Prepare a fresh proposal. Review the current task and settings again; the rejected or expired proposal does not start training. In Live and Simulation, open Process from the strip below the scene to follow workflow runs and their recorded named steps in the central workspace. The right panel stays available for the agent. Process is read-only, works in Monitor mode, and does not verify physical task success. It is hidden in Edit and Replay.
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: For a task configured for CPU, scene export preserves authored mesh collisions supported by native MuJoCo. GPU exports still require primitive colliders or an appropriate simplified collision variant. Selecting CPU does not make arbitrary training code CPU-compatible; the task must also provide a compatible training implementation.

Reviewing policy evidence

Training requests shows the current training phase and reported progress in both the task and assistant conversation. When a task reports curriculum stages, the same card shows its current stage and recent training-stage success rate. That rate is not the final held-out evaluation; missing results remain unreported. In Policies, select a checkpoint and open Evaluation evidence to inspect episode outcomes and available Replay links. Deployment reports separately show reported checks from completed simulation deployments, including failures. Tasks with confirmed deployment checks can report these automatically; the report identifies whether it came from the controller or a task writer. Missing recordings or checks are not a successful result. Check Final task outcome maintained as well as intermediate stages: reaching the goal briefly is not sustained success. Not reported means that outcome is unavailable, not that it passed. These reports are not held-out evaluations or hardware approval. Neither viewing evidence nor finishing training automatically binds or publishes a policy. An evaluated teaching result offers Use on this robot directly. It creates a private controller if needed and assigns it without starting motion; replacing an existing controller requires confirmation. Publish to catalog is separate and does not require assigning the policy to a robot first. Replay displays only recorded channels: object motion appears when the episode includes it; older joint-only recordings do not demonstrate object movement. Policy evidence Replay links frame the recorded robot on entry. You can still orbit, zoom, play and scrub the timeline; this does not start a controller.

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

Task input is not robot output

A goal or requested walking speed is an input to a policy. The policy’s actions are its output to the robot or another controller. These are separate contracts: a walking policy can accept speed and produce joint targets; a navigation policy can produce body velocities for an onboard controller whose actuators are not accessible. Match both contracts to the selected runtime. Simulation actuators in a robot model do not prove that its physical driver exposes them. Cyberwave’s shared adapter has a simulation-only body-velocity output port. For an explicitly authored aerial policy, controller startup checks its frozen interface against the lower controller observed in the running simulator. Unsupported or mismatched interfaces are rejected. Do not treat an evaluated checkpoint or a URDF as hardware readiness. The Velocity preset below means joint velocity, not base/drone velocity. Keep incompatible outputs explicit instead of converting them silently. Authored command-level flight tasks reuse custom action/observation terms and the existing asynchronous training adapter. Their lower controller, output units, frame, limits and cadence must match the training version. Changing that controller requires a fresh task version and evaluation. This adapter support does not yet establish a verified end-to-end drone deployment in the dashboard; inspect actual deployment acceptance and Replay separately from training results. Flight checks use measured robot pose and velocity, without requiring a fake joint-state stream. They require the goal to remain achieved, not merely be crossed once. A missing root feed, a different flight controller or unexplained applied forces makes the result fail. These checks are implementation support; end-to-end drone dashboard verification is still pending.

Joint action presets

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_target — cyberwave-rl-experimental (plain local Python, no Docker) or cyberwave-rl (the dockerized stack).
  2. Pick a runtime_accelerator — gpu 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.

Managed training: training_options.json

A task’s async adapter declares which training strategies cw_request_skill_teaching (or POST /api/v1/skill-teaching-requests) can dispatch by shipping training_options.json next to training.py. This file is the contract between your adapter and the platform: it names each supported strategy, the JSON-Schema-constrained hyperparameters it accepts, and the defaults applied when a caller doesn’t override them.
Fetch the resolved options directly instead of guessing the shape: GET /api/v1/rl-tasks/{uuid}/training-options, cw_inspect_rl_tasks(operation="get"), or the proposal.training.options field on a teaching-request proposal all return the current, validated training_options.json for a task.

Shape

The whole file is capped at 32 KiB and never executed — it’s parsed as data, so authoring it requires no backend source access.

Worked example

This is the real training_options.json for the imitation:behavior_cloning strategy shipped with the in-tree cyberwave-rl/examples/kinova_gen3_pick_place example task:
A caller then requests this strategy with a partial strategy_config, for example {"epochs": 10}; the backend merges it onto defaults (nested objects like augmentation are replaced as a whole, not deep-merged) and validates the result against parameters before freezing it into the job.

Validation errors are field-specific

Every check names the offending path instead of stopping at the first top-level shape mismatch, so you can iterate directly against the error:
  • training_options.json.strategies must be an object keyed by strategy key (e.g. {"rl:ppo": {...}}), got array — a common first guess is authoring strategies as a list of strategy objects instead of a dict keyed by strategy key.
  • strategies['rl:ppo'] is missing required field(s): defaults — a strategy entry must declare exactly parameters and defaults, nothing else.
  • strategies['rl:ppo'].parameters.properties.epochs.type must be one of [...] — nested schema problems point at the exact property.
  • strategies['rl:ppo'].defaults.epochs: 0 is less than the minimum of 1 — invalid defaults report the dotted field path and the underlying JSON Schema message.
A missing_training_input requirement reported by cw_request_skill_teaching or a teaching-request proposal carries this same message, so the specific field is visible without backend source access.

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

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.