Bridging to ROS2 with OmniGraph
For a robot inside the simulator to publish a LiDAR point cloud on /scan and drive its wheels in response to an external /cmd_vel command, you need a bridge between the simulator and ROS2. In Isaac Sim, that bridge is OmniGraph (Action Graph).
Data Flow Built by Connecting Nodes
OmniGraph is a visual compute graph — nodes (boxes) wired together to form a data pipeline. Publishing a sensor as a topic is typically a three-node chain:
Rendering diagram…
The same pattern extends to every sensor and interface:
| Graph | Role |
|---|---|
LiDAR → /scan | Publish distance point cloud |
Camera → /image_raw | Publish image stream |
IMU → /imu/data | Publish inertial data |
Clock → /clock | Publish simulation time |
/cmd_vel → DifferentialController | Command → wheels |
Every graph follows the same skeleton: tick → Isaac Read → ROS2 Publish**. Once you know this skeleton, adding a new sensor reduces to "wire three nodes."
The Common Pitfall — Receiving External Commands
Publishing works cleanly with the pattern above. The friction point is receiving /cmd_vel sent by an external ROS2 node (e.g., Nav2).
⚠️ OmniGraph's Twist subscriber node may not receive external commands — The graph's cmd_vel subscriber works fine for intra-graph communication, but there are cases where it cannot receive messages published by an external ROS2 node. A more robust approach is to receive commands through a dedicated ROS2 client (rclpy) buffer outside the graph and apply them from there. Splitting responsibilities — "publish via OmniGraph, receive external commands via a separate subscriber" — makes debugging significantly easier.
This pitfall is the go-to culprit behind "my topics are publishing fine, but why isn't the command being received?" If you don't know that the publish and receive paths are separate, you can spend a long time chasing the wrong thing.
In our runs the two paths genuinely live side by side, and the log shows it plainly.
[Isaac] rclpy initialized with domain_id=53 <- separate ROS2 client
[Isaac] OmniGraph cmd_vel integration enabled
[Bridge] Graph created at /ActionGraph/ROS2_Bridge_HO_A with 2 OG sensors
[Bridge] Added 13 OSC subscriber nodes (OmniGraph)
Each robot gets one bridge graph carrying two sensors, plus thirteen subscriber nodes for incoming commands. The line worth noticing is that rclpy is initialised separately. If the graph handled everything, that line would not need to exist. Publishing goes through a graph stored in the scene; external commands arrive through a separate client — the split is visible in four lines of log.
Publishing rides a graph stored in the scene; receiving rides a separate client. Both sit on ROS 2, but the two paths are built from different parts — and so they fail separately.
💡 When debugging, ask which path first. No topic coming out means look at the graph; a command not landing means look at the subscriber. Same ROS 2, but the two paths fail independently.
Why a Graph?
You could write a plain ROS2 node in code — so why a visual graph? Because graphs are saved as part of the scene and travel with the robot asset. When sensor graphs are embedded in the robot USD, that robot brings its topic publishing with it no matter which environment it's spawned into — a natural fit for the platform philosophy of adding a robot with a single manifest.
In a nutshell
📌 OmniGraph publishes sensors as topics via a "tick → Isaac Read → ROS2 Publish" node chain (LiDAR / camera / IMU / clock). Publishing is clean, but receiving external ROS2 commands may require a separate subscriber path — remember that the publish and receive paths are not symmetric.
References
See standard test results with videos and verdict evidence on the imSim Portal.
View public results →