Collect Training Data with Behaviors
Use data-recording Behaviors when you want an Objective to collect its own demonstrations. This suits a task the robot can already perform on its own, whether scripted or driven by an existing policy: the Objective starts Trainer, runs the task, keeps a successful episode, and converts the dataset, with no manual timing on your part.
| Behavior | Role in the Objective |
|---|---|
| RecordEpisode | Starts a Trainer recording with a saved Training Config. |
| SaveEpisode | Keeps the current demonstration after the task succeeds. |
| StopRecording | Finishes the session, or safely discards an unsaved failed attempt. |
| ConvertDataset | Starts a background conversion to LeRobotDataset v3.0. |
Prerequisites
- Complete Collect VLA Training Data with Trainer first. It covers Training Config, datasets, recording, and conversion, which this guide assumes you know.
- Choose a runnable task Objective to record. This example wraps Pick April Tag Labeled Object.
This guide runs lab_sim, not the vla_sim configuration used in the previous guide.
Launch the Runtime and Desktop App
We assume you have already installed MoveIt Pro to the default install location. Start the MoveIt Pro Runtime using:
moveit_pro run -c lab_sim
Then launch or connect the separately distributed MoveIt Pro Desktop App. The Runtime does not serve a bundled user interface.
1. Build the wrapper Objective
Create an Objective. Name and Category are required and are yours to choose — this example uses Record Bottle Pick in the Application - Advanced Examples category, the same one Pick April Tag Labeled Object uses. A new Objective starts with a Sequence holding one AlwaysSuccess; delete the AlwaysSuccess and add a Fallback in its place, then give the Fallback two Sequence children:
- In the first Sequence, add these nodes in order:
- RecordEpisode
- WaitForDuration
- Pick April Tag Labeled Object — your task, added as a Subtree
- SaveEpisode
- WaitForDuration
- StopRecording
- ConvertDataset
- In a second Sequence, add StopRecording followed by AlwaysFailure.
Any Objective can be nested inside another as a Subtree. Add one with the editor's + button and pick it from the node list by name, the same way you add a Behavior.
The second branch is deliberate cleanup, not a forced success. If the task fails before SaveEpisode, StopRecording discards that unsaved attempt and AlwaysFailure preserves the Objective's failed result. A failed pick is never labeled as a successful demonstration.
Set the main ports as follows:
| Node | Port | Example value |
|---|---|---|
| RecordEpisode | dataset_name | behavior_bottle_pick |
| RecordEpisode | task | Pick up an AprilTag-labeled bottle. |
| RecordEpisode | config_name | lab_sim |
| RecordEpisode | num_episodes | 1 |
| first WaitForDuration | delay_duration | 2.0 |
| second WaitForDuration | delay_duration | 1.0 |
| ConvertDataset | dataset_name | behavior_bottle_pick |
| ConvertDataset | action_source | next_state |
config_name is the name of the running configuration package, and MoveIt Pro creates a Training Config under that name on first use, so lab_sim already exists. Leave camera_topics empty to use the cameras from it.
The short waits let recording settle before motion begins, and let the saved episode finish updating in Trainer before the session closes.
The Subtree is added collapsed, so the tree shows the recording lifecycle with the whole picking task behind one node.
Select Done to save the Objective.

2. Record the task
Open Trainer and leave the Record tab visible, then select Run on the wrapper Objective. Do not select Start recording manually; RecordEpisode starts the same recording session and its status appears live in Trainer.
The task Subtree now performs the demonstration. SaveEpisode runs only after that Subtree succeeds, so the episode contains a completed bottle pick rather than an unfinished attempt.

When the run completes, StopRecording finishes the session and registers the episode in the dataset.

3. Review the episode
Open the Dataset tab and select the play button on the episode. Use play/pause and the timeline to inspect the recorded motion. Select Stop in the Playback mode card to return to the live robot.
Playback replays the recorded joint states and camera frames, and nothing else. The 3D Visualizer animates the arm from those joint states, but objects in the scene do not move with it: their poses were never recorded, so the bottle stays where the planning scene last had it even as the gripper lifts it. Watch a camera pane to see the pick actually happen.
Playback only reviews recorded data; it does not command the robot. For the same reason the two never run together: playback is refused while an Objective is executing, and an Objective started during playback stops it. Let the wrapper Objective finish before you play an episode.

4. Confirm the conversion
ConvertDataset uses the same dataset_name as RecordEpisode. It reads the camera topics, FPS, joint topics, and robot type the dataset was pinned to when it was recorded — not the Training Config's current values — and creates a sibling dataset named <dataset-name>-lerobot. Each episode is labeled with the task recorded with it, so a dataset holding more than one task converts with per-episode labels.
action_source set to next_stateBy default, conversion labels each frame's action with the commanded joint positions from the pinned command topic and refuses an episode whose command stream is empty or only mirrors the recorded state. Only a teleoperation front end such as Meta Quest publishes that topic, so a task the robot performs on its own, like this one, records nothing on it. Setting the Behavior's action_source port to next_state labels each frame's action with the state of the following frame instead, which is the best available label when no command stream was recorded. Know what that label is: a position target one dataset frame (1/fps) later, not a per-control-cycle command; the episode's last frame is dropped because it has no following state; and a gripper's label is its measured position, so the grasp force of the demonstration is not in the labels. When a recording does carry commanded joint positions, leave the port empty: next_state ignores the recorded commands entirely.
You do not start this conversion yourself: ConvertDataset already queued it when the Objective ran, and the Behavior reports success as soon as the job is accepted rather than when it finishes. Open the Prepare tab to watch it through to completion.
With command labels, ConvertDataset instead fails before starting a job if every recording split's MCAP summary confirms there are no messages on the pinned command topic. For an Objective that records only measured motion, set action_source to next_state as above. The conversion job validates missing or unreadable summaries and checks that require frames, such as commands that mirror measured state.

Example completed conversion in Trainer.
The converted dataset contains LeRobot data and meta folders at:
<TRAINER_DATA_ROOT>/recordings/behavior_bottle_pick-lerobot
<TRAINER_DATA_ROOT> is MOVEIT_PRO_TRAINER_DATA_ROOT when set, or ~/.local/share/moveit_pro/trainer by default.
Camera frames are stored as images rather than video files; see the dataset layout note for what that costs in disk space.
To collect many episodes in one run, start RecordEpisode once with num_episodes set to the batch size. Wrap the demonstration in a RepeatUnlessFailureEachTick decorator with num_cycles set to the same number. Each iteration randomizes the scene, runs the task Subtree, calls SaveEpisode once the task succeeds, and resets the robot for the next attempt. Place StopRecording and ConvertDataset after the decorator so the whole batch is closed and converted once, and keep the failure cleanup branch so a failed attempt is discarded and the batch stops for inspection.
Use RepeatUnlessFailureEachTick rather than Repeat, which is deprecated: it runs its loop inside a single tick, so an always-successful child can deadlock the tree.