The command line

ros2 dynrec drives a running recorder without service-call syntax, and without a display, which is the access pattern that matters on a robot.

ros2 dynrec status            # what is it doing
ros2 dynrec topics            # what is it recording, one per line
ros2 dynrec add /scan         # start recording /scan, leave everything else alone
ros2 dynrec remove /scan      # stop recording it
ros2 dynrec set /tf /odom     # record exactly these
ros2 dynrec profile navigation
ros2 dynrec profiles          # list configured profiles, active one marked
ros2 dynrec pause | resume | toggle | split | snapshot | stop | record

Finding the recorder

With one recorder on the graph there is nothing to configure. It is found by looking for a node offering ~/get_status of our type, so a recorder launched under any name or pushed into any namespace is found, and a node that merely borrows the name is not.

Pass --node when several are running. With more than one it refuses to guess and lists them, because guessing could stop the wrong recording.

Every verb also takes --timeout N (seconds to wait for a service and its reply, default 10) and --spin-time N (seconds to spin for graph discovery before looking, default 1).

Exit codes

Every verb exits non-zero when the recorder refuses, so a supervisor script can rely on the exit code alone and never has to parse return_code out of a service reply:

ros2 dynrec add /scan || echo "could not record /scan"

Failures go to stderr, where a supervisor’s log will not mix them into the output.

Patterns

ros2 dynrec add -e '^/camera/'                        # everything under /camera
ros2 dynrec set -e '^/robot/' --exclude-regex depth   # all of /robot but the depth camera
ros2 dynrec remove -e '/image'                        # drop the heavy ones

-e/--regex selects by pattern rather than by name and --exclude-regex filters the result. On add and set the pattern searches the graph; on remove it searches what is actually being recorded, since only a recorded topic can be dropped.

A pattern that matches nothing is refused on add, because adding nothing is a request that was not met and the likely cause is a typo. On set it is allowed, because an empty result is the documented way to record nothing without closing the bag.

Naming a type

ros2 dynrec add /scan:sensor_msgs/msg/LaserScan

That is how you record a topic whose publisher has not started yet. Without it the type is discovered from the graph, which needs the topic to be there already.

Profiles

ros2 dynrec profiles lists the configured profiles with the active one marked * and each profile’s topics beneath it, or says none are configured. --names prints just the names, one per line, for scripting. ros2 dynrec profile <name> applies one; see Profiles for how they are declared.

Scheduling

resume, split and record take --at:

ros2 dynrec resume --at +30s
ros2 dynrec split  --at 14:05
ros2 dynrec record --at 2026-09-03T14:05

Accepted forms are +30s/+5m/+1h, 14:05 (today, or tomorrow if it has passed), an ISO 8601 instant, or raw epoch seconds.

--mode picks the clock for resume and split: node (default) fires on a timer and works on a robot that has gone quiet; publish and receive are evaluated as messages arrive and cannot fire without traffic. --topic restricts those to one topic, which must be one the recorder is subscribed to.

Note

Relative and wall-clock times are resolved against your clock, while the recorder compares against its node clock. On one machine, or a clock-synced fleet, those agree; across a robot whose clock has drifted they do not, which is why the absolute forms exist.

Only the newest schedule of each operation is live; scheduling it again replaces it, and there is no cancel. status lists whatever is queued, by any client, under scheduled, saying which clock each waits on.

Reading a bag back

ros2 dynrec info /tmp/mybag

The one verb that does not talk to a running recorder. It reads a finished bag and reports what each channel really did:

topic                                 msgs  recorded   active       rate    averaged
/demo/alpha                            590     29.5s    29.5s   20.00 Hz    16.12 Hz
/demo/beta                             218     11.0s    11.0s   19.87 Hz     5.95 Hz
/demo/delta                             80      4.0s     4.0s   20.25 Hz     2.19 Hz
/demo/gamma                            365     18.2s    18.2s   20.10 Hz     9.97 Hz

Every one of those topics published at 20 Hz. The averaged column is what ros2 bag info and mcap info report (count over the whole bag), which describes a channel that stopped early as a slow sensor. The rate column measures each channel over the time it was actually subscribed and not paused, which the recorder’s events make knowable.

It exits 2 when the bag holds a hole that nothing explains: a gap shared by every live channel with no pause event behind it, which is what a crash, a stall, or a recording made with record_pause_events:=false all look like. A sparse channel on its own is normal and is not an error. --json gives the same report as data, with unknowable values as null, and --storage-id overrides the storage plugin when it cannot be detected from the bag.

Machine-readable status

ros2 dynrec status --json

Fields the recorder cannot vouch for come out as null, never as a zero. messages_missed is null when the middleware supplies no publication sequence numbers, the difference between “nothing was missed” and “no idea”. The per-topic breakdown, topic_losses, follows the same rule, and the human-readable status lists it under by topic only when some topic has lost something.

A recorder launched with use_sim_time reports state: waiting for /clock until the simulation publishes its first clock message, and --json carries use_sim_time and waiting_for_clock for a script. No bag is open yet, so the recorder is up and waiting rather than stopped or unreachable; see Simulation time.

ros2 dynrec topics | xargs -n1 echo recording:

topics prints bare names and nothing else, so it pipes into xargs without any parsing. status is where the decorated version lives.


For anything longer than a shell one-liner, the Python library is the better tool: it can watch the recorder’s event streams, which shelling out cannot do.