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:

  • peppy_schema: The schema identifier, always "node/v1"
  • 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 (optional; a node with no interfaces omits it)

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
implementsOptional list of contracts this node implements, making it eligible to fill contract slots; see Contract implementation
depends_onOptional dependency declarations, in four lists: nodes, contracts, pairings, and pairing_observers; see Contract implementation and Pairing

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. Exactly one of run_cmd or container must be present; a container node’s build goes through the container image, so build_cmd is not used

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.

Three 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; pairing, an exclusive 1:1 bidirectional topic exchange between two instances; and observer slots, read-only taps on one role of an existing pairing. 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
  • 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. Edges come from each node’s depends_on.nodes declarations; contract, pairing, and observer slots do not add DAG edges. The declared interfaces are then validated against those edges.
  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 Attachment: When a node declares a dependency in depends_on.nodes, the core node attaches the edge and validates that the dependency exposes the interfaces the dependent consumes
  • 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, and nodes communicate across runtimes through the shared messaging layer. Each core node owns only its local stack. A federated launch is driven by a coordinator: the daemon that received the launch resolves every deployment once and drives the participating core nodes, which run exactly what it resolved (see Federation).

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)