Skip to content

Concepts

A Node is the fundamental unit of computation in Peppy. It represents a runnable application or service that can expose interfaces (topics, services, actions) and consume interfaces from other nodes.

A node is defined by its configuration file (peppy.json5) which includes:

  • Manifest: The node’s identity
  • Execution: Commands to build and run the node, plus the parameters it accepts
  • Interfaces: What the node exposes and consumes

The manifest defines a node’s identity:

FieldDescription
nameA validated identifier (ASCII letters, digits, _, -)
tagA short contract identifier (e.g., v1, donut). Must start with an ASCII letter; allowed chars are letters, digits, _, -. Dots are forbidden.
labelsOptional metadata labels
depends_onOptional dependency declarations on other nodes (depends_on.nodes) or contracts (depends_on.contracts); see Contract implementation

A node is uniquely identified by its name:tag combination.

Tags are not semver. Bumping a tag always means “incompatible with the previous one”: there is no notion of a backward-compatible patch or minor release. Pick a label that identifies the contract (v1, v2, donut), not a version number.

The execution section defines how to prepare and launch the node:

FieldDescription
languageThe programming language of the node (rust, python)
parametersOptional parameter schema for runtime configuration
build_cmdCommand to run during the node build phase
run_cmdCommand to launch the node
containerOptional container configuration (mutually exclusive with build_cmd/run_cmd)

Nodes communicate through three types of interfaces:

TypeDescription
TopicPublish/subscribe messaging for streaming data
ServiceRequest/response communication for synchronous calls
ActionLong-running tasks with feedback and cancellation support

A node can expose interfaces (make them available to others) and consume interfaces from other nodes.

Two higher-level constructs build on these: contract implementation, where the contract lives in a standalone document and any implementing producer can fill the consumer’s slot, and pairing, an exclusive 1:1 bidirectional topic exchange between two instances. See Choosing a communication pattern for how to pick between all of them.

A Node Instance represents a single running execution of a node. Each instance has:

  • Instance ID: A unique identifier for this running process
  • PID: The process ID (when running locally)
  • State: Its runtime lifecycle state: starting while it comes up, running once started, then the terminal finished (clean exit) or failed (crash) if its process exits on its own
  • Health: The result of the core node’s latest liveness probe against a running instance, healthy or unhealthy (not applicable once the instance is terminal)

A node can have multiple instances running simultaneously. For example, you might run multiple instances of a camera node, each connected to separate physical devices.

The Node Stack is the central data structure that manages all active nodes in the system. It maintains a directed acyclic graph (DAG) where:

  • Vertices are node entities
  • Edges represent dependency relationships, pointing from a dependent node to its dependency. These edges are derived from interface connections between nodes (exposers and consumers).
  1. Dependency Management: Tracks which nodes depend on which other nodes
  2. Interface Validation: Ensures nodes expose the interfaces their dependents require
  3. Instance Lifecycle: Manages the creation, health tracking, and removal of node instances. Running instances are continuously health-probed; a failing probe flags an instance unhealthy but never removes it, so an instance leaves the stack only when it is explicitly stopped or removed. See Instance health and lifecycle for details.
  4. Root Node: Always contains a root node (the core node) that cannot be removed

When a node is added, the stack:

  1. Validates that all required dependencies exist
  2. Checks that dependencies expose the required interfaces
  3. Tracks pending requirements when dependencies are not yet available
  4. Resolves pending requirements when dependencies are added

The stack-list API exposes the nodes and dependency edges as a serialized JSON graph for programmatic access. The peppy stack list command renders that graph as operator-friendly tables inside a separate outer panel for each live core node.

The Core Node is a special node that serves as the root of the node stack and is always present. It is the daemon (peppy service serve) that the peppy CLI talks to. It is responsible for:

  • Dependency Creation: When a node consumes an interface, the core node creates a dependency on the node that exposes that interface
  • Stack Management: Managing the lifecycle of all other nodes in the system: spawning them, probing their health, and tearing them down so none is left orphaned when the daemon stops or dies
  • System Coordination: Acting as the central coordinator for the local Peppy runtime

When the core node shuts down cleanly it stops every spawned node (cooperatively, then force-killing any straggler’s process group); if it dies unexpectedly, each node’s watchdog notices the missing heartbeat and shuts the node down after a grace period. See Daemon shutdown and orphan prevention for details.

Each Peppy runtime has exactly one core node that manages its local DAG of nodes. In a distributed deployment, multiple runtimes (each with their own core node) can run on separate machines. The core nodes operate independently, managing their local node stacks without a centralized controller. Nodes communicate across runtimes through the shared messaging layer, enabling a fully decentralized system where each core node is responsible only for its own set of nodes.

Each machine runs at most one daemon per Peppy data root. peppy service serve takes an exclusive lock at startup, and a second invocation refuses to boot while the first runs.

Because core nodes are addressed by name over the messaging layer, every core node reachable over the same router or federation must have a unique name. Each daemon derives a stable, machine-specific name by default, or you can pin one with core_node_name; a daemon whose name is already taken refuses to boot.

Node Stack
├── Core Node (always present)
│ └── Instance (always a single instance)
└── Other Nodes
├── Node A
│ ├── Configuration
│ └── Instances (1 or more)
└── Node B
├── Configuration
└── Instances (1 or more)