Skip to main content
The cerulion binary groups all functionality under top-level commands:
Synopsis notation uses <required>, [optional], {a|b} (choice), and ... (repeatable). Synopsis lines are not directly runnable; each command includes a separate runnable example.

Global

On error, cerulion prints Error: <msg> to stderr and exits with a failure code. Most commands require a workspace, discovered by walking upward for a Cargo.toml containing [workspace] plus a graphs/ directory. workspace create/init, tui, trace, clean, completions, login, account, pair, connect, viz, bag, flashback, and ros2 run/launch/migrate do not require discovery; schema info and schema list use a workspace when they find one and fall back to the built-in types when they do not. The topic commands use transport discovery and need no workspace.

workspace

cerulion workspace create

Create a new workspace directory.
Creates ./<name>/ with graphs/, nodes/, schemas/, Cargo.toml ([workspace], members=["nodes/*"], resolver="2", workspace dependencies for cerulion_core and native_ros2_messages), .cargo/config.toml, and a .gitignore carrying .cerulion/ (the workspace lock directory). Errors if ./<name> already exists.The cerulion_core and native_ros2_messages dependencies point at published crates, exact-pinned to the CLI’s own version, unless your cerulion binary was built from a cerulion-base checkout — then they are absolute path dependencies into that checkout. The decision keys on where the binary lives, never on the current directory. The command prints which it chose on the line under the created path.
Initialize a workspace in place.
Errors if a Cargo.toml with [workspace] already exists there. The workspace name is the directory’s file name.

node

Subcommands: create, delete, modify, build, stage, run, list, info. All require workspace discovery.

cerulion node create

Create a node type.
SCHEMA accepts sensor_msgs/Image, sensor_msgs::Image, or a bare Image. Bare names resolve against your workspace schemas first, then the built-in ROS 2 messages; an ambiguous bare name errors and lists the candidates.Generated files: nodes/<type>/Cargo.toml (cdylib) and nodes/<type>/src/lib.rs.A source-only node (no -i/-T) must declare a non-data policy — --policy period_ms=N or --policy external.
Delete a node type.
Removes nodes/<node_type>/ (recursive) and the workspace member entry.
Modify an existing node type in place.
Splices new port fields into the macro struct and rewrites the node-level policy attribute in src/lib.rs. Your tick() body is untouched. --raw-ffi is not accepted here — the existing node form is preserved.
Compile a node type into a loadable library.
Shells out to cargo build for that crate with the cdylib feature, producing .so (Linux) or .dylib (macOS). CARGO_TARGET_DIR is honored.If the crate declares optional system dependencies, the build probes them with pkg-config first and enables each feature whose libraries are present; a missing one is reported loudly and the build continues without that feature.
Add an instance of a node type to a graph.
SOURCE is either relative (sensor/image → /<prefix>/sensor/image) or absolute (/vendor/camera/image, an external topic with no in-graph producer).
Run a single node type on its own.
Builds a hidden single-node graph and runs it on the live clock without validation, so you can exercise one node against topic echo. Networking follows the same defaults as graph run.
List the workspace’s node types.
Prints a TYPE INPUTS OUTPUTS POLICY table derived from each crate’s src/lib.rs.
Show one node type’s ports and policy.

graph

Subcommands: create, run, validate, list, levels, profile, partition.

cerulion graph create

Create an empty graph file.
Load, validate, and execute a graph.
A failing validation check refuses the run before anything starts; warnings are advisory and the run continues.The run continues until Ctrl+C or a node calls request_shutdown(). On Unix under the real clock, an unpartitioned graph is executed multi-process by default — see Run a graph across processes.A run that stops normally spends a bounded pass of roughly two seconds on cleanup after the graph is over, reclaiming shared-memory state left behind by dead nodes; because the graph has already stopped, nothing you are waiting on is delayed. Whatever the budget does not reach stays for the next pass. A run killed with SIGKILL skips that pass, and the next run that stops normally picks up what it left. A run refused before it starts — an unknown graph name, for instance — does no cleanup at all.
Validate a graph without running it.
Runs the full check set — node libraries present, ports and schemas matching the node source, sources resolvable, trigger requirements satisfied, topic names well formed — and prints a per-check report ending in a count such as 9/9 checks passed. Exits non-zero on any failure, which makes it the command to run in CI.Node libraries must be built first; an unbuilt node fails with cdylib '<type>' not found.
List the workspace’s graphs.
Show the execution levels the runtime derives from the wiring.
Prints each level with its nodes and their trigger policies, the edges that carry triggers, and — when the graph declares process_groups: — which group covers which levels. Exits non-zero if the declared partition is invalid.The partition verdict names every split block edge it found creditable, so this is the verb that answers whether a hand-written split will run:
Measure per-node tick costs and edge rates.
Runs the graph locally, then writes a cost snapshot — per-node median tick cost, per-edge fire rates, and a frozen compute budget — that graph partition consumes. Nodes that never sampled enough fires are reported as isolated rather than guessed at.
Derive and write the graph’s process groups.
Writing is a surgical rewrite of the process_groups: block with a .bak backup alongside it.

topic

Topic commands attach to the running transport and need no workspace.

cerulion topic list

List active topics, locally and on the network.
Local topics print first and immediately. Robots and their topics discovered over the LAN follow in ROBOTS and REMOTE TOPICS sections. A failed remote query is a loud note and still exits 0.
Show a topic’s schema and last-seen frame.
Print messages as they arrive.
Decodes with the topic’s schema when it is known, and falls back to a hex dump when it is not. Runs until Ctrl+C. A name that is not a local topic is looked for on the LAN and demanded for the length of the command; CERULION_NETWORK=off keeps it local-only.
Measure a topic’s publish rate.

schema

cerulion schema create

Create a workspace schema file.
Writes schemas/<name>.yaml — the name verbatim, no case change — holding a schemas: skeleton whose single entry key is the PascalCased name. cerulion schema create detections writes schemas/detections.yaml containing a Detections: entry. See Graph and schema files.
Delete a workspace schema.
Built-in ROS 2 messages cannot be deleted.
Show a schema’s fields, wire size, and hash.
Prints the recursive field tree marking fixed and variable fields, the fixed wire size, the schema hash, and where the definition came from. It resolves four sources, in this order: your workspace’s schemas/*.yaml, your workspace’s .msg store (schemas/<package>/msg/<Type>.msg), the built-in ROS 2 types, and a connected robot’s own types. The source: line names which one answered.
List available schemas.
Prints your workspace schemas, then your workspace’s .msg store, then every built-in ROS 2 message grouped by package. A built-in that a .msg store entry overrides is listed as <name> (shadowed by msg store '<name>').

bag

Recording and replay. See Record and replay a run.

cerulion bag record

Record live local topics into a bag.
This taps this machine’s shared memory and never opens the network: to record a robot’s topics, run it on the robot and copy the file afterwards. The capture is observability-grade — each channel carries the real wire hash but no schema name, which is everything bag play needs and enough for topic echo to decode from the workspace schema store.--resim compatibility is not claimed for a bag record bag, --run included. A standalone capture carries frames and no scheduler trace at all; a --run attach does carry the run’s effective graph, environment, manifests and a trace from its attach point, but its descriptors are still observability-grade. When you know you will re-execute, record the run with graph run --record.
Inspect a bag.
A bag that carries a recorder health attachment also gets an absorbance block: per topic, whether the queue its recording tap was given holds that topic’s frames through the run’s own worst drain gap. Every topic with a verdict is counted; only the actionable ones are named — short, no claim, a stall the drain-gap histogram cannot rank, a pass on a floor rate, or a topic that fell short earlier and recovered. On a flashback capture the block describes the recorder that took the capture, over its whole run, not the captured window. See Record and replay a run.
Play a bag back, optionally re-executing your nodes.
--report and --tolerance need --verify, not merely --resim. A bare --resim all re-executes the graph without comparing anything, so there is no verdict for either flag to describe, and the command refuses rather than writing an empty report.
Replay is network-inert by construction: the bag is the input.
Rewrite an older bag so --resim accepts it.
Writes a new bag whose embedded graph has since-removed keys taken out, listing every key it removes and asking first (--yes writes without asking, and is required when stdin is not a terminal; --dry-run writes nothing). The input bag is never modified, and every frame and attachment is copied through unchanged. A bag whose graph already parses is refused — there is nothing to migrate.

flashback

Captures roughly the last 30 seconds plus the next 15 seconds of every serving graph on this machine into recordings/flashbacks/. There is nothing to arm in advance — a live run already holds the rolling window. CERULION_FLASHBACK=off turns the capture plane off.

viz

Attaches live topics to the visualization daemon and opens a standalone viewer window on them. For the supported way to watch a graph, see Visualize a running graph.

connect

Demanded topics are re-injected into your machine’s shared memory, so cerulion topic echo, topic hz, and Cerulion Studio see them like local topics. With no --topic and no --all, it prints the robot’s catalog and demands nothing.

pair

Runs the pairing ceremony so a robot access-lists this desk, then pins name → endpoint id in ~/.cerulion/robots.toml. Your desk key is created once at ~/.cerulion/desk.key and never overwritten. Exit codes: 0 paired · 1 usage error · 2 refused · 3 unreachable · 4 wrong code · 130 interrupted.
Prefer typing the code at the prompt over passing --code: a code on the command line is visible in process listings while the ceremony runs.

ros2

The ROS 2 interop family: run and launch put stock ROS 2 entry points on Cerulion transport, migrate rewrites a colcon workspace’s C++ publish sites to the loaned-message API, and attach bridges a robot that is already running ROS 2.

run and launch

Runs a stock ROS 2 entry point on Cerulion transport — swap the command, not the stack. ros2 run demo_nodes_cpp talker becomes cerulion ros2 run demo_nodes_cpp talker; ros2 launch my_robot_bringup camera.launch.py becomes cerulion ros2 launch my_robot_bringup camera.launch.py. Unix only. Both verbs are verbatim pass-through wrappers: everything after run or launch is forwarded to the native ros2 run / ros2 launch untouched — hyphenated flags, the package form, launch arguments. Their argument surface is ROS 2’s, so consult ros2 run --help / ros2 launch --help, and a bad package name or a missing launch file is ros2’s own error.
There is exactly one exception, and it is a refusal rather than a rewrite: --adopt-take in the leading position (right after run or launch) exits 69 instead of being forwarded, and CERULION_RMW_ADOPT_TAKE set in the environment exits 2. ros2 is a Python CLI that spawns your node as a further subprocess with close_fds=True, so the heap hook these launchers hand over never reaches the node and every take would be served by a copy — the refusal is there so you do not get the flag’s name without its effect. For zero-copy plain takes, launch the node executable directly with the hook preloaded and CERULION_RMW_ADOPT_TAKE=1 (Linux/glibc only).
Before forwarding, Cerulion stages the child environment: Cerulion then exec()s ros2 <verb> [ARGS...]: the process becomes ros2, so stdout, stderr, Ctrl+C, and the exit code pass through the kernel untouched, and a successful exec inherits ros2’s own exit code. The nodes keep their ROS names. A ROS 2 publisher on /camera/image_raw is the Cerulion topic /camera/image_raw — the fully-qualified name verbatim, leading slash included, for topics and for services alike. Nothing is prefixed, stripped, or aliased, so a native #[input] whose source: is /camera/image_raw reads that publisher directly, and a recording of the run carries the topic under the name ROS 2 uses. Each ROS 2 publisher also registers its topic with the machine’s network plane, so once that plane is up the topic is announced and a remote desk can demand it like a native node’s output — see Reach remote robots. Registration is best-effort and never blocks local ROS 2 pub/sub: a topic that could not be registered stays local-only and says so in a warning. Subscriptions register nothing, and a registration is not withdrawn when a publisher is destroyed — the topic stays announced, with no producer behind it, for the life of the process. Exit codes belong to Cerulion only when it never reached the exec: 69 librmw_cerulion.so missing (the message carries the build command and the override), or --adopt-take given · 127 ros2 not on PATH · 2 a usage error, such as a bare cerulion ros2, or CERULION_RMW_ADOPT_TAKE set in the environment · 1 any other pre-exec failure.
To bring stock ROS 2 nodes up alongside native Cerulion nodes from one graph file, declare them as ros2: entries and run cerulion graph run — see Graph and schema files.

migrate

Rewrites this colcon workspace’s C++ publish call sites to the ROS 2 loaned-message API (borrow_loaned_message() → fill → publish(std::move(loaned))) wherever a clang AST prover shows the rewrite is behavior-preserving. It is an AST transform over the workspace’s compile_commands.json, never a regex; anything it cannot prove lands in a manual-candidates report with the reason. rclpy nodes are report-only, because rclpy has no loaned-message API upstream. Migrating does not couple your workspace to Cerulion: rclcpp falls back to allocate-and-copy when the rmw cannot loan.
Initialization is not identical on every rmw, and the rewrite compensates. make_unique<T>() value-initializes, so every field your code does not write holds its declared default (a Quaternion’s w=1, zeros elsewhere). A loaned message is only initialized if the rmw does it, and rmw_fastrtps — the ROS 2 default — does not. The rewrite therefore emits <loaned>.get() = <MessageT>(); right after the borrow, so every field you do not write keeps the default make_unique gave it, on every rmw, for the cost of one value-init store.
Dry run is the default. It prints the full unified diff and the candidates report, and refreshes the machine-readable manifest at .cerulion/ros2-migrate-manifest.json — the only thing it writes. --write refuses a dirty git tree, then writes one commit plus cerulion-ros2-migration.patch and runs colcon build --packages-select over the affected packages. Undo with git revert <sha>, or git apply -R cerulion-ros2-migration.patch. If a pre-commit hook stages paths of its own, it stages them inside git commit — after this verb’s index check — so they land in the migration commit; the run warns and names them, and git apply -R is then the safe undo, because it reverses only the migration’s own edits. Requires a compile database (colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) and the migration engine binary cerulion-ros2-migrate-clang, found beside the cerulion binary, on PATH, or via CERULION_ROS2_MIGRATE_TOOL. Exit 69 with build instructions when it is absent.

attach

Discovers a ROS 2 system’s topics, generates graphs/<name>.yaml plus a bridge configuration for the ones whose types resolve, and runs the bridge. See Bridge a ROS 2 system. Every run’s report ends with a MIGRATION section — there is no flag for it, and --dry-run prints it too. It groups the discovered processes (one DDS participant each) by what a restart under rmw_cerulion would buy: restartable today, when every message type on the process’s endpoints resolves locally; restartable once consent writes the schemas this attach resolved; or stays bridged, with the unresolvable types named. Endpoints with no ros_discovery_info record are counted as topics, not processes, and are reported as absence of evidence — a vendor or raw-DDS process, or a node table the window did not observe. ROS plumbing endpoints (parameter services, /rosout, /parameter_events, ros_discovery_info) are excluded from the judgment. The section then prints the equivalent cerulion ros2 launch line, a paste-ready ros2: graph block with <package>/<executable> placeholders, the bridged-versus-native cost facts, and a closing pointer at cerulion ros2 migrate. It is rendered from the discovery data already in hand: no extra network, no change to the outcome or exit code.

login and account

cerulion login signs this machine in with a device-code flow: it prints a short code and a verification URL, then waits for you to authorize it in the browser. The first command that needs an identity triggers it for you. account devices lists and revokes the machines registered to your account. Robot access management lives in Cerulion Studio and the web account page, not the CLI.

completions

Shells: bash, elvish, fish, powershell, zsh. Completion covers subcommands, flags, and enum values, plus live names — topics, node types, graph names, schemas, paired robots, and .mcap files. A completion never opens the network.

tui

An interactive dashboard with Nodes, Topics, and Echo tabs. Quit with q.

trace

Prints one line per record — <topic> seq=<N> t=<TIMESTAMP_NS> schema=<HASH> — from the JSONL publish trace (trace_*.jsonl) in a directory. -t restricts to one topic by exact name, -n limits the count, and -r reverses to most-recent-first. This is not the MCAP bag — for recordings use cerulion bag.

clean

Wipes lingering shared-memory state left by dead nodes; live nodes are untouched. Reach for it when a graph’s topic topology changed between runs and topics look missing. --report-only reports what is reclaimable and deletes nothing, though the dead-node sweep still runs. When the dead-node sweep cannot clear every dead node, clean reports the leftover shared-memory state but does not reclaim it: a dead node that is still registered needs its state to be reaped later, so removing it would strand that node for good. Clear the reported failures, then run cerulion clean again. You rarely need this command — graph run sweeps dead nodes at start, and cleans up again when the graph exits normally.

Node macro

Every macro and field attribute available to a node author.

Graph and schema files

Every key in graph and schema YAML.

Environment variables

Logging, networking, recording, and runtime tuning variables.

Connect an MCP client

Connect an AI client to the Cerulion CLI over a local stdio server.