Skip to main content
Cerulion recordings are standard MCAP files. One recording is one .mcap file, uncompressed, carrying every frame exactly as it was published — wire sequence and timestamp included. There are three ways to get one, and they answer different questions:

Record a run

Add --record to the run. The bare flag writes into ./recordings/; a custom directory needs the = form.
The recording keeps the network on — the robot stays viewable while you record — and finalizes on shutdown, including when the run is stopped with Ctrl+C. Recording requires the live clock, so it cannot be combined with --time-source virtual or external, and it is refused together with --no-rings. It is also refused together with --no-validate: that flag skips the schema: checks, and a bag whose channel labels were never checked cannot be honestly replay-verified — a re-execution can refuse a healthy bag, or replay one under the wrong label. Record with validation on, or drop --record.
A recorded run captures the graph, the environment, and the run’s own record of what fired when. That is what makes it re-executable later — see Re-execute your nodes.
The recorder also picks up live producers the graph never declared and records them alongside your topics. Those extra channels are not replay-grade — the bag says so when it is created, cerulion bag info names anything the recorder could not cover, and a re-execution skips them instead of verifying them. Set CERULION_RECORD_DISCOVERY=off to record only the graph’s declared topics. By default the environment is captured as names for everything and values only for Cerulion’s own variables, so a bag never leaks a secret you happened to export. --record-env all captures every value verbatim for full replay fidelity, and warns loudly that the bag now embeds them.

Record topics that are already live

cerulion bag record taps this machine’s shared memory. It never opens the network, so recording a robot’s topics means running it on the robot and copying the file afterwards.
Exactly one selection source is allowed — combining a positional list with -a or -e is an error, because the merge you would assume (“union? intersection?”) is not knowable. Narrow with -x instead. To describe a run rather than a topic set, attach to a live one:
The bag then carries that run’s effective graph, environment, and recorder manifests, and picks up its scheduler trace from the attach point. A bare --run means the sole live run; naming one needs the = form, since every word after a bare --run is read as a topic.

Capture the recent past

Every serving graph on Unix already holds a rolling window of roughly the last 30 seconds in memory. cerulion flashback writes that window out — plus the following 15 seconds — as an ordinary bag. Nothing has to be armed in advance.
Captures land in recordings/flashbacks/. Add --pin to keep one out of routine retention, and --no-wait to return as soon as the request is accepted instead of waiting for the file. CERULION_FLASHBACK=off turns the capture plane off entirely. A capture also carries the recorder’s own health, so cerulion bag info reports an absorbance verdict for it — scoped to the recorder that took the capture, over its whole run rather than the captured window:

Inspect a bag

state: finalized is the one to read first: playback needs the summary footer that a clean recorder shutdown writes, so a bag from a killed recorder is refused by bag play while bag info still reports what it can read.

Read the absorbance verdict

A bag that carries a recorder health document — a graph run --record recording, or a flashback capture — ends its report with an absorbance block:
The verdict answers one question per topic: when the recorder pauses for as long as this run’s worst drain gap, does that topic’s shared-memory queue hold its frames until the recorder drains again? A topic that absorbs them keeps its frames; a topic that falls short can drop frames at its own queue while the recorder is away, and frames_lost=0 does not contradict that — the loss happens before the recorder ever sees the frame. Every topic is counted in the summary, and only the ones you can act on are printed: a topic that fell short, one that makes no claim, one whose stall the drain-gap histogram cannot rank, one that passed on a floor rate rather than a measured one, and one that fell short earlier in the run. A topic that absorbed every measured stall on an exact rate, with nothing else to flag, is counted and not printed. A printed row names the topic, the depth its recording tap was given, and the rate the verdict used:
The per-topic detail behind the block — each topic’s own rate, tail, depth and loss_counting_basis — is in the bag’s health attachment: __cerulion/record_health.json on a recording, and __cerulion/capture_recorder_health.json on a flashback capture, which carries the recorder’s document rather than one of its own.

Play a bag back

Playback republishes the recorded frames onto shared memory, so everything downstream — topic echo, topic hz, Cerulion Studio, a consumer node — sees what a live robot would.
Two behaviors are worth knowing before you interpret a playback:
  • A topic that a live producer already owns is refused by name, and the rest of the bag still plays. Topics are single-writer, so a player must not compete with a live producer.
  • Each topic keeps its own recorded rate. Stamps from different producers share no number line, so playback runs one timeline per topic, anchored on that topic’s own first frame.

Re-execute your nodes

--resim is the other half of the verb: instead of republishing recorded frames, it re-executes the bagged graph’s nodes against the recorded input, on the recording’s own clock.
--resim all re-executes every native node; a node subset is not supported yet and is refused by name rather than quietly widened. A ros2: entry in the bagged graph is never respawned — its topics are recorded inputs — and one warning names the entries it skipped, which --report also lists under ros2_entries_skipped. A bare --resim makes no claim about whether the result matches the recording — divergence is the product, so a completed re-execution exits 0. --verify is the verdict: it byte-compares every produced frame and exits non-zero on divergence. Add --report PATH for a machine-readable verdict, --tolerance PATH for fields that may legitimately differ, and --strict-state to refuse the run unless every node’s state was restored.
A passing --verify is a strong statement: your current code, re-run against real recorded input, produced the same bytes the robot produced. That is the property to put in CI.
Two things will make a re-execution refuse or produce nothing, and both are by design:
  • Environment divergence is reported, not ignored. A variable that was present at record time and absent at replay is a warning naming the variable; the run continues.
  • A bag recorded with bag record is not replay-grade. It carries frames, not the graph and trace a re-execution needs. Use graph run --record for a bag you intend to re-execute, and cerulion bag migrate to bring an older replay-grade bag forward.

Next steps

Visualize a running graph

Watch a live run, or a bag playing back, on the Studio stage.

Run a graph across processes

The default deployment shape, and what it means for recordings.