> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cerulion.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Record and replay a run

> Record a run to an MCAP bag, inspect it, play it back as a robot substitute, and re-execute your nodes against it.

Cerulion recordings are standard [MCAP](https://mcap.dev) 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:

| Command                              | Use it when                                                                              |
| ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `cerulion graph run <name> --record` | You are about to run the graph and want a **replay-grade** bag you can re-execute later. |
| `cerulion bag record`                | Something is already running (yours or not) and you want its topics on disk.             |
| `cerulion flashback`                 | Something already went wrong and nobody was recording.                                   |

## Record a run

Add `--record` to the run. The bare flag writes into `./recordings/`; a custom directory needs the `=` form.

```bash theme={null}
cerulion graph run perception --record
```

```text theme={null}
recordings/perception_20260827T234549Z.mcap
messages=722 bytes=87001 frames_lost=0 dropped_unwritten=0
```

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`.

<Note>
  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](#re-execute-your-nodes).
</Note>

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.

```bash theme={null}
cerulion bag record -a -o session.mcap --duration 30
```

| Selection      | Meaning                                                                       |
| -------------- | ----------------------------------------------------------------------------- |
| `TOPIC...`     | Exactly these topics. A named topic that is not live here is refused by name. |
| `-a` / `--all` | Every live local topic.                                                       |
| `-e PATTERN`   | Every live local topic matching a regex.                                      |
| `-x PATTERN`   | Narrows any of the above. Repeatable.                                         |

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:

```bash theme={null}
cerulion bag record --run=perception
```

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.

```bash theme={null}
cerulion flashback --note "arm jerked left on the pick"
```

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.

## Inspect a bag

```bash theme={null}
cerulion bag info recordings/perception_20260827T234549Z.mcap
```

```text theme={null}
bag: recordings/perception_20260827T234549Z.mcap
state: finalized
frames: 722 across 2 topic(s); span 11.880s

TOPIC                                       FRAMES   MAX BYTES  SCHEMA
/perception/detector/detection                 361         126  geometry_msgs/PoseStamped
/perception/sensor/image                       361         115  sensor_msgs/Image
```

`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.

## 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.

```bash theme={null}
cerulion bag play session.mcap --loop
```

| Flag                      | Meaning                                                                                     |
| ------------------------- | ------------------------------------------------------------------------------------------- |
| `-r` / `--rate N`         | Rate multiplier: `2` for twice the recorded pace, `0.5` for half.                           |
| `--loop`                  | Restart at the beginning until interrupted.                                                 |
| `--topics TOPIC`          | Play only these topics. A name the bag does not carry is a loud error listing what it does. |
| `-s` / `--start-offset S` | Skip the first S seconds of bag time.                                                       |
| `-u` / `--duration D`     | Cover only the first D seconds of bag time.                                                 |

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.

```bash theme={null}
cerulion bag play recordings/perception_20260827T234549Z.mcap --resim all --verify
```

```text theme={null}
replay PASS: recordings/perception_20260827T234549Z.mcap (11164 tick(s) replayed, 2/2 topic(s) matched byte-for-byte)
```

|                     | `bag play <bag>`                      | `bag play <bag> --resim all`             |
| ------------------- | ------------------------------------- | ---------------------------------------- |
| What runs           | The recorded frames are republished   | Your current node builds are re-executed |
| Output topics       | Replayed verbatim                     | Recomputed                               |
| Pacing              | Wall-paced from the recorded timeline | Unpaced — it never reads the wall clock  |
| Needs the workspace | No                                    | Yes, it loads your node libraries        |

`--resim all` re-executes every node; a node subset is not supported yet and is refused by name rather than quietly widened.

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.

<Check>
  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.
</Check>

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

<CardGroup cols={2}>
  <Card title="Visualize a running graph" icon="chart-line" href="/cerulion/guides/visualize-in-studio" color="#0080FF">
    Watch a live run, or a bag playing back, on the Studio stage.
  </Card>

  <Card title="Run a graph across processes" icon="microchip" href="/cerulion/guides/run-multi-process" color="#0080FF">
    The default deployment shape, and what it means for recordings.
  </Card>
</CardGroup>
