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)
- Framework: the restart policy, a knob peppy reads the same way for every node (optional)
Manifest
Section titled “Manifest”The manifest defines a node’s identity:
| Field | Description |
|---|---|
name | A validated identifier (ASCII letters, digits, _, -) |
tag | A short contract identifier (e.g., v1, donut). Must start with an ASCII letter; allowed chars are letters, digits, _, -. Dots are forbidden. |
labels | Optional metadata labels |
implements | Optional list of contracts this node implements, making it eligible to fill contract slots; see Contract implementation |
depends_on | Optional dependency declarations, in five lists: nodes, contracts, daemon, pairings, and pairing_observers; see Contract implementation, Daemon targets 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.
Execution
Section titled “Execution”The execution section defines how to prepare and launch the node:
| Field | Description |
|---|---|
language | The programming language of the node (rust, python) |
parameters | Optional parameter schema for runtime configuration |
build_cmd | Command to run during the node build phase |
run_cmd | Command to launch the node |
container | Optional 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 |
endpoints | Optional map of the sockets the node binds during setup, keyed by label (lowercase letters, digits and underscores, starting with a letter). Each entry declares a kind, page for a URL opened in a browser or mcp for one an MCP client attaches to, and a non-empty description. The node announces the socket it bound under each label, and the daemon reports the URLs; a declared label that setup never announces fails the start |
setup_timeout_secs | Optional setup budget of the node: how long the daemon waits for its setup to end once the node answers ready, a whole number of seconds from 1 to 540. Without it, the default setup budget of 15 seconds applies. The node reads its budget at run time; see How an instance starts |
Framework
Section titled “Framework”The framework block holds framework knobs peppy reads the same way for every node; a launcher writes restart under the same key.
| Field | Description |
|---|---|
restart | Optional: what the daemon does when the instance’s process exits on its own or stops answering health checks, never, on_failure or always, or { policy, max_restarts }, where max_restarts caps the restart attempts in a row, failed starts included, and the next exit or failed start past them leaves the instance failed. Under on_failure or always the daemon also kills an instance that stops answering health checks and treats that exit as a failure (the rule and its outage guard are in Instance health and lifecycle). A launcher’s instance entry, an adjustment or --restart on peppy node run or peppy node add --run wins over this value, which wins over the launcher root’s; with none declared the policy is on_failure. See Restart policy |
Interfaces
Section titled “Interfaces”Nodes communicate through three types of interfaces:
| Type | Description |
|---|---|
| Topic | Publish/subscribe messaging for streaming data |
| Service | Request/response communication for synchronous calls |
| Action | Long-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.
Node Instance
Section titled “Node Instance”A Node Instance is one deployment of a node under an id; a restart starts a new process under the same id. Each instance has:
- Instance ID: The instance’s unique identifier, kept across its restarts
- State: Its runtime lifecycle state:
startingwhile it comes up (until its ready signal, then until its setup ends, within its setup budget),runningonce started, then the terminalfinished(clean exit) orfailed(crash) if its process exits on its own, orrestartingwhile the daemon waits to start it again under its restart policy - Health: Whether a running instance answers the core node’s health checks,
healthyorunhealthy; it turnsunhealthyonly after three consecutive missed health checks (not applicable while the instance isrestartingor 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.
Node Stack
Section titled “Node Stack”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.nodesdeclarations; contract, pairing, and observer slots do not add DAG edges. The declared interfaces are then validated against those edges.
Key responsibilities
Section titled “Key responsibilities”- Dependency Management: Tracks which nodes depend on which other nodes
- Interface Validation: Ensures nodes expose the interfaces their dependents require
- Instance Lifecycle: Manages the creation, health tracking, and removal of node instances. Running instances are continuously health-checked; a sustained run of missed health checks flags an instance
unhealthy, and underon_failureoralwaysan instance that stops answering is killed and its exit treated as a failure, by the rule in Instance health and lifecycle. The monitor never removes an instance, so an instance leaves the stack only when it is explicitly stopped or removed. See Instance health and lifecycle for details. - Root Node: Always contains a root node (the core node) that cannot be removed
How dependencies work
Section titled “How dependencies work”When a node is added, the stack:
- Validates that all required dependencies exist
- Checks that dependencies expose the required interfaces
- Tracks pending requirements when dependencies are not yet available
- Resolves pending requirements when dependencies are added
Stack inspection
Section titled “Stack inspection”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
Section titled “The 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, or at once when a new daemon of the same core node sends its first heartbeat. 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.
The fleet
Section titled “The fleet”The fleet of a stack is the robots of the launch that the core node coordinates: each copy of the launch is a robot, a device that a client changes included. The core node serves the fleet in the daemon interface fleet:v1 (see Robot leases and the halt):
- A lease gives one client the right to move one robot. Only a node whose launcher scopes it to hold leases takes one.
- A mark on a contract member, such as
moves_robot, says what the member does. Each MCP server of peppy,peppy node calland each lease holder ask the core node before they call a member with the markmoves_robot. - The halt ends every lease and stops each client that asks before it moves a robot, until a person or a program clears it. It stays after a restart of the core node.
- The robot commander of a robot is the option of its copy on the axis that the launcher marks with
role: "robot_commander": a node that drives its own robot outside the leases and the halt, such as a leader arm or the browser panel.
The leases and the halt prevent conflicts between clients. They are not a safety function.
Summary
Section titled “Summary”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)