---
title: "Node Documents — Canopy docs | UniverLab"
source: "https://univerlab.org/canopy/docs/graphs-nodes/"
---

[← Canopy](/canopy/)Documentation

# Node Documents

Last updated 2026 · Sol 306 · 16:15 UTC (2026-11-02T16:15Z)

## Nodes

A **node document** is one reusable node — a Rust Designer, a Python Validator, the classic implementer — that is shared and installed like a whole graph, then added to any graph by name:

    {
      "format_version": 1,
      "name": "auditor",
      "kind": "agent",
      "description": "Audits a change against the spec.",
      "config": { "prompt_template": "audit this", "timeout_minutes": 10 },
      "preset": { "name": "auditor-preset", "content": "audit well" }
    }

`kind` is `agent`, `check`, `gate`, `router`, or `ensemble`. An `ensemble` document carries the ensemble shape in `config`: `prompt_template`, `members` (each **without** `platform`/`model`), `min_pass`, and the same fields `graph_ensemble` `add` accepts (`kind`, timeouts, infra budgets, `commit_rights`, `mutex`). A document never carries a harness — who runs a node is the installer’s decision, not the author’s. `canopy graph validate <path>` checks a node document exactly as it checks a graph document (`ok` with `node_kind`, or every problem with its JSON path).

**Install** one from the catalogue:

    canopy node install <slug>
    canopy node install <owner>/<repo>/<slug>@<ref>
    canopy node install draft:<id>
    canopy node install <ref> --as <name> --replace --overwrite-presets --yes --show-prompts --json

The ref grammar is the graph ref grammar: a bare `<slug>` is the official UniverLab entry, `<owner>/<repo>/<slug>` a community one (each with an optional `@<ref>`), plus `draft:<id>` for a node draft shared from the editor — the same one-use, 30-minute, unreviewed draft as a graph, with the same `draft not found` message (see [Install a graph](/canopy/docs/graphs-sharing/#install-a-graph)); a draft holding a graph document is refused, saying which kind it found. The document is fetched through `graphs.catalogue_url` and validated exactly as `canopy graph validate` validates it. Before anything is written, the same **review** `canopy graph install` prints comes first: every shell command (`config.command`) labelled by node, every agent instruction (`prompt_template`, `prompt_override`, and the preset content behind `prompt_preset` — bundled or builtin — with its first 3 lines and its total line count, full text with `--show-prompts`), and the attention marks for command shapes worth a second look (pipe to shell, `sudo`, `rm -rf`, `chmod 777`, `git push`/`--force`, writes under `~/.ssh`, `~/.config` or `/etc`, `eval`, `base64 -d`, pipe to `python -c`/`node -e`). Then it asks `Install <name> from <source>? [y/N]` — official documents are asked about exactly like community ones. Without a TTY and without `--yes` it prints the summary and refuses, exiting 1 with `refusing to install without confirmation: re-run with --yes after reviewing the commands above`; a draft’s refusal adds that the draft is gone and asks for a new link from the editor. `--yes` installs after the summary without the question, and `--json` prints the review as JSON on stdout with the question on stderr.

Only after the confirmation is the document written to `~/.canopy/nodes/<name>.json`, stamped with the catalogue entry it came from (`slug`, `owner/repo/slug`, or `draft:<id>`). Its bundled `preset` is installed as `~/.canopy/prompts/<preset.name>.md` with the same no-silent-overwrite rule as a graph’s presets: a file owned by another source aborts the install unless `--overwrite-presets` (or `--replace`) is given. A graph document fetched from a node endpoint is refused, and a 410 says `<name> was removed from the catalogue after a community report: <reason> (<issue>)`.

A name already installed from a different source is refused, naming both sources — the installed copy (and every graph using it) is never silently replaced. `--as <local-name>` installs under another local name instead (the bundled preset is renamed with it: a `<doc>-…` preset keeps its suffix under the new name, any other preset becomes `<as>-<preset>`, with the config’s `prompt_preset` rewritten to match), or `--replace` overwrites. `--as` takes a plain slug (lowercase letters, digits, `-`); anything else is refused before anything is fetched. Reinstalling from the same source updates in place, and an installed copy still overrides a builtin of the same name.

Over MCP, `node` `install` { `ref`, `as`?, `replace`?, `overwrite_presets`?, `accept`? } follows the same gate: **without** `accept: true` it installs nothing and returns `{ needs_review: true, review: { source, commands, prompts, attention } }`; with `accept: true` it installs and echoes the same review in the result. An agent must show the review to its user before passing `accept: true`.

**List** what is available:

    canopy node list
    canopy node list --json

The list shows installed copies and the builtins embedded in the binary, each marked `installed` or `builtin`, with the catalogue entry that installed it next to the name (`catalogue_source` in `--json`; a hand-written copy shows `local`). The builtins are `implementer`, `cargo-gates`, `reviewer-committer`, `commit-check`, `resilience`, and `ensemble-proposers`; they resolve offline, with no install step. An installed copy of a builtin’s name wins over the builtin.

**Add** one to a graph:

    graph_node { action: "add", spec_id, name: "audit", node: "auditor",
                 config_overrides: { "platform": "claude", "model": "opus" } }
    graph_ensemble { action: "add", spec_id, name: "Proposers", node: "ensemble-proposers",
                     members: [{ "platform": "openrouter" }, …],
                     from_node: "<kickoff>", condition: "always",
                     on_pass_to: "<arbiter>" }

`node` is the alternative to `kind` + `config`, and `config_overrides` shallow-merges on top of the document’s config — the override wins, the rest of the document is preserved. An agent node document has no harness, so the call is refused with a message naming the document unless the overrides supply `platform` (or `cli`, or `crew`). An ensemble document supplies the shared prompt, the member count and the ensemble shape, but its members still need a platform each from the caller’s `members` list.

**Remove** an installed copy:

    canopy node remove <name>

A builtin with no installed copy cannot be removed; an installed copy that overrides a builtin is removed like any other, and the builtin resolves again afterwards.

Node documents replaced the retired blueprint tools and the `blueprint` parameter outright: `graph_node` and `graph_ensemble` `add` take `node`, `kind` + `config`, or — for ensembles — a crew, and nothing else.