Node communication
A node by itself isn’t very useful without the ability to communicate with other nodes in the node stack.
Each node is only aware of the interfaces it exposes and the ones it subscribes to, as defined in its peppy.json5 configuration. For example:
{ schema_version: 1, manifest: { name: "controller", tag: "0.1.0", language: "python", }, process: { add_cmd: ["uv", "sync"], start_cmd: ["uv", "run", "controller"] }, interfaces: { exposes: { topics: [], services: [], actions: [], }, subscribes_to: { topics: [], services: [], actions: [], }, }}{ schema_version: 1, manifest: { name: "controller", tag: "0.1.0", language: "rust", }, process: { add_cmd: ["cargo", "build", "--release"], start_cmd: ["./target/release/controller"] }, interfaces: { exposes: { topics: [], services: [], actions: [], }, subscribes_to: { topics: [], services: [], actions: [], }, }}Here we have interfaces with exposes and subscribes_to containing topics, services, and actions. These interfaces define the dependencies between nodes.
Topics, Services & Actions
Section titled “Topics, Services & Actions”PeppyOS provides three primary communication patterns for nodes to exchange data:
-
Topics are used for continuous, unidirectional data streams. A node publishes messages to a topic, and any number of nodes can subscribe to receive them. This is ideal for sensor data, state updates, or any information that flows continuously (e.g., camera images, odometry).
-
Services implement a request-response pattern. A node connects to another node and waits for a response. Use services for quick operations that need a result, like querying a node’s state or triggering a one-time computation.
-
Actions are for long-running tasks that need feedback and cancellation support. A client sends a goal to an action node, which provides periodic feedback during execution and a final result upon completion. Actions are built on top of topics and services internally. Use them for tasks like navigation or arm movement. Actions are processed serially by a node: two requests to the same action cannot run in parallel. For example, a
move_armaction that moves a robot’s arm can only perform one movement at a time.
Subscribed interfaces
Section titled “Subscribed interfaces”In our hello_world_param node, we’ve already exposed a topic. Let’s try to make this topic communicate with another node.
We first need to initialize the hello_receiver node in a new folder:
-
Initialize the node:
Terminal window peppy node init --toolchain uv hello_receiverTerminal window peppy node init --toolchain cargo hello_receiver -
Navigate into the directory:
Terminal window cd hello_receiver
with the following peppy.json5:
{ schema_version: 1, manifest: { name: "hello_receiver", tag: "0.1.0", language: "python", }, process: { add_cmd: [ "uv", "sync" ], start_cmd: [ "uv", "run", "hello_receiver" ] }, interfaces: { subscribes_to: { topics: [ { id: "hello_world_param_subscriber", // subscriber_id, up to you to decide node: "hello_world_param", // Will look for that node name tag: "0.1.0", // On that particular tag/version name: "message_stream", // With this topic name } ], } }}{ schema_version: 1, manifest: { name: "hello_receiver", tag: "0.1.0", language: "rust", }, process: { add_cmd: [ "cargo", "build", "--release" ], start_cmd: [ "./target/release/hello_receiver" ] }, interfaces: { subscribes_to: { topics: [ { id: "hello_world_param_subscriber", // subscriber_id, up to you to decide node: "hello_world_param", // Will look for that node name tag: "0.1.0", // On that particular tag/version name: "message_stream", // With this topic name } ], } }}and the following source file:
import asyncio
from peppygen import NodeBuilder, NodeRunnerfrom peppygen.parameters import Parametersfrom peppygen.subscribed_topics import hello_world_param_message_stream
async def setup(_params: Parameters, node_runner: NodeRunner) -> list[asyncio.Task]: return [asyncio.create_task(receive_messages(node_runner))]
async def receive_messages(node_runner: NodeRunner): while True: ( instance_id, message, ) = await hello_world_param_message_stream.on_next_message_received(node_runner) print(f"Received from {instance_id}: {message.message}")
def main(): NodeBuilder().run(setup)
if __name__ == "__main__": main()use std::sync::Arc;
use peppygen::subscribed_topics::hello_world_param_message_stream;use peppygen::{NodeBuilder, NodeRunner, Parameters, Result};
fn main() -> Result<()> { NodeBuilder::new().run(|_args: Parameters, node_runner| async move { tokio::spawn(receive_messages(node_runner)); Ok(()) })}
async fn receive_messages(node_runner: Arc<NodeRunner>) { loop { let result = hello_world_param_message_stream::on_next_message_received( &node_runner, None, None, ) .await;
match result { Ok((instance_id, message)) => println!("Received from {instance_id}: {}", message.message), Err(e) => { eprintln!("Error receiving message: {e}"); break; } } }}Finally, add this new node to the stack:
-
Sync the node interfaces:
Terminal window peppy node sync -
Add the node to the stack:
Terminal window peppy node add .
Starting the nodes
Section titled “Starting the nodes”Now we need to make sure our nodes are started. If we take a look at our node stack:
❯ peppy stack listListing nodes...Requesting node stack graph from core 'sweet-germain-4388'...Node stack: - sweet-germain-4388:core-node (/Users/tuatini/workspace/peppy) (1 instance: ["brave-zhukovsky-6918"]) - hello_receiver:0.1.0 (/Users/tuatini/.peppy/nodes/hello_receiver_0.1.0_d41475) (0 instances: []) - hello_world_param:0.1.0 (/Users/tuatini/.peppy/nodes/hello_world_param_0.1.0_9d5fa0) (0 instances: [])Dependencies: - hello_receiver:0.1.0 -> hello_world_param:0.1.0We need to make sure that at least one instance is started for hello_receiver:0.1.0 and another one for hello_world_param:0.1.0.
The order in which the nodes are started does not affect the node communication.
Let’s start a hello_world_param:0.1.0 instance:
peppy node start hello_world_param:0.1.0 name=planetSince our node requires a name, we provide it with that argument on startup.
Now let’s start the hello_receiver node:
❯ peppy node start hello_receiver:0.1.0Running node hello_receiver:0.1.0...Starting node hello_receiver:0.1.0 with instance_id 'vigorous-buck-8117' and 0 argument(s)...Calling node_start for hello_receiver:0.1.0 (instance_id=vigorous-buck-8117)...Log file: /Users/tuatini/.peppy/logs/start/vigorous-buck-8117.logStarted node instance 'vigorous-buck-8117' (pid: 77190)When you start the node, you can see a path to the logs. In my case: /Users/tuatini/.peppy/logs/start/vigorous-buck-8117.log.
If we open it up:
[2026-01-24T10:02:47.630] [stderr] Finished `release` profile [optimized] target(s) in 0.21s[2026-01-24T10:02:47.635] [stderr] Running `target/release/hello_receiver`[2026-01-24T10:02:51.243] [stdout] Received from gifted-moser-9365: hello planet count 7[2026-01-24T10:02:54.243] [stdout] Received from gifted-moser-9365: hello planet count 8[2026-01-24T10:02:57.244] [stdout] Received from gifted-moser-9365: hello planet count 9We can see the messages received from the hello_world_param:0.1.0 instance!
Starting a second instance with different parameters
Section titled “Starting a second instance with different parameters”Now let’s push things a little further. Imagine we need a second instance with different parameters—we can start one like this:
❯ peppy node start hello_world_param:0.1.0 name=youRunning node hello_world_param:0.1.0...Starting node hello_world_param:0.1.0 with instance_id 'admiring-black-0614' and 1 argument(s)...Calling node_start for hello_world_param:0.1.0 (instance_id=admiring-black-0614)...Log file: /Users/tuatini/.peppy/logs/start/admiring-black-0614.logStarted node instance 'admiring-black-0614' (pid: 78063)And we check the logs again:
❯ tail /Users/tuatini/.peppy/logs/start/vigorous-buck-8117.log[2026-01-24T10:04:36.242] [stdout] Received from gifted-moser-9365: hello planet count 42[2026-01-24T10:04:39.244] [stdout] Received from gifted-moser-9365: hello planet count 43[2026-01-24T10:04:42.244] [stdout] Received from gifted-moser-9365: hello planet count 44[2026-01-24T10:04:45.244] [stdout] Received from gifted-moser-9365: hello planet count 45[2026-01-24T10:04:47.085] [stdout] Received from admiring-black-0614: hello you count 1[2026-01-24T10:04:48.243] [stdout] Received from gifted-moser-9365: hello planet count 46[2026-01-24T10:04:50.085] [stdout] Received from admiring-black-0614: hello you count 2[2026-01-24T10:04:51.244] [stdout] Received from gifted-moser-9365: hello planet count 47We can see the messages from both instances!
❯ peppy stack listListing nodes...Requesting node stack graph from core 'sweet-germain-4388'...Node stack: - sweet-germain-4388:core-node (/Users/tuatini/workspace/peppy) (1 instance: ["brave-zhukovsky-6918"]) - hello_receiver:0.1.0 (/Users/tuatini/.peppy/nodes/hello_receiver_0.1.0_d41475) (1 instance: ["vigorous-buck-8117"]) - hello_world_param:0.1.0 (/Users/tuatini/.peppy/nodes/hello_world_param_0.1.0_9d5fa0) (2 instances: ["gifted-moser-9365", "admiring-black-0614"])Dependencies: - hello_receiver:0.1.0 -> hello_world_param:0.1.0We’ll explore services and actions in the advanced guides, although they fundamentally work the same way. Refer to nodes in this repository for more examples of topics/service/action usage.