Extending CamelGraph — Write Your Own Nodes¶
CamelGraph is designed so that adding a node is a five-minute job: a public static C# method with a couple of attributes is a node. This guide walks through building a complete node pack, from an empty project to nodes showing up in the editor, and then covers the advanced path — interactive NodeModel nodes with custom WPF UI.
Packs are loaded from
%APPDATA%\CamelGraph\Packages(and, for packs installed the old way, from aPackagesfolder next toCamelGraph.App.dll) the first time the editor or the Script Player opens in a Navisworks session, so restart Navisworks after adding or changing a pack. Help ▸ Node Packs… shows the folder, what loaded and what did not, and opens the folder. A pack is code that runs inside Navisworks with your rights, so install packs only from authors you trust.
Contents¶
- How node loading works
- Tutorial: a zero-touch node pack
- Ports, defaults, and multiple outputs
- Lists and replication — what your node sees
- Errors and warnings
- Navisworks node packs
- Custom interactive nodes (NodeModel + WPF view)
- Conventions checklist
- Making a node look right in the editor
- Changing a node that is already shipped
1. How node loading works¶
At startup, CamelGraph's zero-touch loader (in CamelGraph.Core) reflects over node assemblies and registers every public static method of every public class (generic methods, property accessors and methods marked [IsVisibleInLibrary(false)] are skipped). [NodeName] sets the display name; without it the node is called Class.Method. Each parameter becomes an input port; the return value becomes the output port (or several, with [MultiReturn]). The built-in libraries (CamelGraph.Nodes, CamelGraph.Navisworks) are loaded this way — your pack uses exactly the same mechanism, so anything the built-in nodes can do, yours can too.
The loader also scans two folders for node packs (subfolders included), in this order:
%APPDATA%\CamelGraph\Packages\<YourPackName>\ your packs: kept when CamelGraph is updated or removed
%APPDATA%\Autodesk\ApplicationPlugins\CamelGraph.bundle\<year>\Packages\... the old place: the installer deletes it on every update
YourPack.dll (plus any private dependencies)
Put your packs in the first folder. It lives with your settings, so installing a new version of CamelGraph does not touch it, and one copy serves Navisworks 2024, 2025 and 2026. A pack that needs a different build for each Navisworks year goes in the Packages folder of that year's folder inside the bundle instead; the installer replaces the whole bundle on an update, so copy it there again afterwards. If the same file name is in both places, the one in %APPDATA% is used.
The scan happens once, when the editor or the Script Player first opens in a Navisworks session. A pack that is added or changed later shows up after you restart Navisworks (a loaded DLL cannot be replaced while Navisworks runs). Each DLL is loaded in isolation: one that fails is skipped and listed with its reason, and it can never take down CamelGraph or Navisworks. Help ▸ Node Packs… shows the folder, what was loaded and what was not, and offers to open the folder; Help ▸ Copy Diagnostics lists the same in its "Node packs" section. Copies of CamelGraph's and Navisworks' own libraries (CamelGraph.*, Autodesk.*, Newtonsoft.Json, Nodify, System.*) are ignored if they end up in a pack folder.
2. Tutorial: a zero-touch node pack¶
Step 1 — create the project¶
A general-purpose pack (no Navisworks API) targets netstandard2.0 and references CamelGraph.Core only:
<!-- RebarToolkit.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<RootNamespace>RebarToolkit</RootNamespace>
</PropertyGroup>
<ItemGroup>
<!-- During development, a project or DLL reference to CamelGraph.Core.
(A CamelGraph.Core NuGet package is planned alongside the M5 package manager.) -->
<Reference Include="CamelGraph.Core">
<HintPath>path\to\CamelGraph.Core.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
Private=false matters: CamelGraph already provides CamelGraph.Core at runtime — your pack must not ship its own copy.
A pack built for version 0.48 or earlier, when the product was called Dyncamelo, references Dyncamelo.Core.dll and the value types DyncameloColor, DyncameloPoint, …; it has to be built again against CamelGraph.Core.dll to load in newer versions.
Step 2 — write a node¶
using CamelGraph.Core.Loader; // the attributes: [NodeName], [NodeCategory], [NodeDescription], [MultiReturn]…
namespace RebarToolkit;
/// <summary>Rebar quantity helpers.</summary>
public static class Rebar
{
/// <summary>Weight in kilograms of a straight rebar.</summary>
[NodeName("Rebar.BarWeight")]
[NodeCategory("RebarToolkit.Rebar")]
[NodeDescription("Weight in kg of a straight rebar from its diameter (mm) and length (m).")]
public static double BarWeight(double diameterMm, double lengthM, double density = 7850)
{
double areaM2 = System.Math.PI * System.Math.Pow(diameterMm / 2000.0, 2);
return areaM2 * lengthM * density;
}
}
That is the entire node. What the attributes do:
| Attribute | Effect |
|---|---|
[NodeName("Rebar.BarWeight")] |
The node's display name and search key. Follow the Category.Verb/Category.Noun convention — see the catalog |
[NodeCategory("RebarToolkit.Rebar")] |
Position in the library tree (dots nest) |
[NodeDescription("...")] |
Tooltip/help text shown to users — write it for end users |
| (method signature) | diameterMm, lengthM become required input ports; density becomes a defaulted port (unconnected = 7850); the return value becomes the output port |
Step 3 — test it¶
Your pack is plain .NET — test it with xunit on any OS, no Navisworks needed:
[Fact]
public void BarWeight_D16_1m_IsAboutOnePoint58Kg()
=> Assert.Equal(1.58, Rebar.BarWeight(16, 1.0), 2);
Step 4 — install it¶
Choose Help ▸ Node Packs… in the editor and answer Yes to open your packs folder (it is made if it does not exist yet), or open it yourself: %APPDATA%\CamelGraph\Packages. Copy the build output into a folder of its own there, then restart Navisworks:
Your nodes appear under RebarToolkit → Rebar in the node browser, with your descriptions as tooltips. Done. If they do not, Help ▸ Node Packs… lists every DLL it found and why one was not loaded (a DLL built for another .NET, a missing dependency, a copy of a library CamelGraph already has).
3. Ports, defaults, and multiple outputs¶
Input ports come from parameters — name, advisory type, and rank are all inferred:
double,int,string,bool,DateTime, your own classes → scalar (rank 0) ports.List<T>/IList<T>/IEnumerable<T>→ list (rank 1) ports; nested lists → rank 2.- Optional parameters (
double density = 7850) → defaulted ports: usable unconnected, overridable by wire. objectaccepts anything (coercion off — you receive the raw value).
Multiple outputs use [MultiReturn] with a Dictionary<string, object> return; each key becomes an output port:
/// <summary>Splits a full bar mark like "16-B-250" into its parts.</summary>
[NodeName("Rebar.ParseBarMark")]
[NodeCategory("RebarToolkit.Rebar")]
[NodeDescription("Splits a bar mark (e.g. \"16-B-250\") into diameter, grade and spacing.")]
[MultiReturn("diameter", "grade", "spacing")]
public static Dictionary<string, object> ParseBarMark(string barMark)
{
string[] parts = barMark.Split('-');
return new Dictionary<string, object>
{
["diameter"] = double.Parse(parts[0], CultureInfo.InvariantCulture),
["grade"] = parts[1],
["spacing"] = double.Parse(parts[2], CultureInfo.InvariantCulture),
};
}
Value types across nodes: ports can carry any CLR type. Prefer the shared CamelGraph.Core value types (Point, Vector, BoundingBox, Color) where they fit so your nodes compose with the built-in library, and give custom types a meaningful ToString() so Watch shows something useful.
4. Lists and replication — what your node sees¶
You do not write loops. Declare the rank you actually need and the engine's replication does the rest (ARCHITECTURE.md §4):
BarWeight(double, double, double)fed a list of 500 diameters is invoked 500 times and yields a list of 500 weights. Lacing (Shortest/Longest/Cross-Product) governs how multiple lists pair up — the user controls that per node instance, your code never sees it.- Take a
List<object>parameter only when the node genuinely needs the whole list at once (aggregation, sorting, joining) — a list-typed port absorbs a list instead of mapping over it. - Never mutate an input (lists included) — return new collections. Upstream cached values are shared; mutation corrupts other consumers.
5. Errors and warnings¶
The contract (see ARCHITECTURE.md §9):
- Throw for real failures. Any exception is caught by the engine and shown as that node's
Errorstate with your message. ThrowArgumentExceptionand friends with messages an end user can act on ("Bar mark must look like '16-B-250', got 'x'"). The run continues; Navisworks never crashes. - Warn and keep going for recoverable issues. Return
null(or a documented sentinel likedouble.NaN) for a missing/unparseable value and callNodeWarnings.Add("…")(namespaceCamelGraph.Core.Execution) so the node shows the amberWarningbadge with your sentence instead of a hard error. The node still delivers its result to the nodes after it. - Call it from inside the node method (or any helper it calls); the engine collects the messages per call of your method. The same text reported several times in one call is shown once with a count, and at most five different texts are listed.
- Under replication the messages of all calls are summarised in one line,
3 of 40 calls: <first message>, so a thousand bad elements cannot flood the badge. - Outside a run (a unit test that calls your method directly)
NodeWarnings.Adddoes nothing and never throws, so a node stays testable on its own. NodeWarnings.IsLacedis true while the call that is running is one of several the engine makes for one run of the node (an input held a list the node is mapped over). A node that must change the model once per intent can refuse a list in a scalar input with it (Document.Openthrows "opens one file" instead of opening each path in turn and replacing the contents every time); a node whose edits add up can warn that they will.NodeWarnings.WarnIfNotFinite(value, "The result")is a one-line check for a node whose arithmetic can quietly produceNaNorInfinity: it reports "The result is not a finite number (NaN)." and returns true when it did.- Write the message for the person at the keyboard: what was wrong and what the node did about it ("2 of 10 values were not numbers and were skipped"), not an exception dump.
using CamelGraph.Core.Execution;
public static double SafeRatio(double part, double total)
{
if (total == 0)
{
NodeWarnings.Add("The total is 0, so the ratio is 0.");
return 0;
}
return part / total;
}
EvaluationContext.Current?.Checkpoint() (namespace CamelGraph.Core.Execution) before each step: it lets the host repaint and poll for Stop, and throws OperationCanceledException when the user stopped the run. Let that exception through (do not catch it as a failure of the step); the node keeps its previous outputs and stays dirty, so the next run starts it again. EvaluationContext.Current is the context of the run that is calling your method, and null outside a run (a unit test), so the call is safe everywhere. EvaluationContext.Current.CancellationToken is the token itself, for code that wants to pass it on.
- Never show message boxes, write to the console, or swallow exceptions silently from library nodes.
6. Navisworks node packs¶
A pack that talks to the Navisworks API targets net48 and adds the same compile-time-only references the built-in library uses:
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Speckle.Navisworks.API" Version="2024.0.0" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
<Reference Include="CamelGraph.Core" ... Private="false" />
</ItemGroup>
ExcludeAssets="runtime" is essential: you compile against the API surface, but at runtime your DLL binds the genuine Autodesk assemblies already loaded in the Navisworks process. Never copy Autodesk DLLs into your pack folder.
Rules for Navisworks nodes (the built-in library follows the same ones):
- Threading is solved for you — nodes execute on the Navisworks main thread by construction (plan §7). Do not spawn threads or use
Task.Run/asyncinside a node. - Emit and accept flat
List<ModelItem>so your nodes compose with search, sets, clash, and appearance nodes — it is the lingua franca of the Navisworks library. - Take a
Documentparameter (it defaults to the active document when unconnected) rather than readingApplication.ActiveDocumentmid-method — it keeps nodes testable and multi-doc-ready. - Mutate the document only through the documented
Document*edit APIs (DocumentClashTests,DocumentTimeliner,Document.Models.Override...) so the Navisworks UI stays in sync. Do not promise users one undo step per run: the node host opens no transaction around a run, so Navisworks records one step per modifying call where it records one at all (a node mapped over a list makes one call per element). If your node makes several edits that belong together, open a transaction around them yourself, as the clash nodes do (doc.BeginTransaction). - Convert at the boundary: accept/return
CamelGraph.Coregeometry (Point,BoundingBox,Color) instead ofPoint3D/BoundingBox3D/Api.Color, so downstream pure nodes can consume your outputs.
7. Custom interactive nodes (NodeModel + WPF view)¶
Zero-touch covers everything that is "inputs in, outputs out". Subclass NodeModel only when a node needs state or UI of its own — inline editors (sliders), variable ports (List.Create), pass-through viewers (Watch), or OS dialogs (File Path). The built-in interactive nodes are implemented through exactly this seam, so it is a supported, stable extension point — not internals.
The shape of it (illustrative — the CamelGraph.Core XML docs are the normative API reference):
using CamelGraph.Core.Graph;
namespace RebarToolkit;
/// <summary>Slider that snaps to standard rebar diameters (8, 10, 12, 16, 20, 25, 32 mm).</summary>
public class RebarDiameterSlider : NodeModel
{
private static readonly double[] Standard = { 8, 10, 12, 16, 20, 25, 32 };
private double _diameter = 16;
public RebarDiameterSlider()
{
Name = "Rebar Diameter";
Category = "RebarToolkit.Rebar";
AddOutPort("diameter", typeof(double));
}
/// <summary>The selected diameter in millimetres. Setting it marks the node dirty.</summary>
public double Diameter
{
get { return _diameter; }
set { _diameter = SnapToStandard(value); MarkDirty(); }
}
// Evaluation: publish the current value to the out-port.
// Persistence: the node's state (Diameter) round-trips through the .dyc "data" bag.
// See CamelGraph.Core docs for the exact override points.
}
Two halves, strictly separated:
- The model lives in your pack assembly (no WPF references) — ports, state, dirty-marking, evaluation,
.dycpersistence of itsdatabag. Because it is UI-free it remains unit-testable on Linux like everything else. - The view is a WPF
DataTemplatekeyed by your model type, supplied in a companion UI assembly loaded from the same pack folder.CamelGraph.UIresolves templates for node models it does not know from loaded packs; a model without a template still works — it renders with the default node chrome (ports and name), just without custom controls.
Keep custom UI minimal (a slider, a text box, a swatch). Anything heavier belongs in a dialog opened from the node, not on the canvas.
Give the node search words, as [NodeSearchTags] does for a zero-touch method, by overriding SearchTags: public override IReadOnlyList<string> SearchTags { get; } = new[] { "rebar", "bar", "diameter" };. The library and the quick search match them besides the name, folder and description, so people find the node by what they call it (the built-in Choice node answers to dropdown, select and pick; Watch to preview, inspect and debug). A test fails for a NodeModel in this repository without them.
8. Conventions checklist¶
Before publishing a pack:
- [ ] Node names follow
Category.Verb/Category.Noun; interactive nodes use friendly names. No collisions with catalog names. - [ ] Every node has
[NodeDescription]and XML<summary>written for end users. - [ ] Port names are lowercase-camel, short, and self-explanatory (
modelItems,path,ignoreCase). - [ ] Optional parameters used for sensible defaults; no boolean traps (name flags clearly:
includeSelf,overwrite). - [ ] Culture-invariant parsing/formatting throughout (
CultureInfo.InvariantCulture). - [ ] Inputs never mutated; collections returned fresh.
- [ ] A node that acts on something returns that thing (so the next node can be chained to it); a bare
doneflag is only for acts on the whole document. - [ ] A
[MultiReturn]node also declares[PortKinds(...)], one kind per output, so its sockets are coloured before the graph has run (a test fails when it is missing). - [ ] Errors thrown with actionable messages; recoverable issues warn + return null; no UI, no console, no threads.
- [ ] Pure logic covered by xunit tests (runnable on Linux).
- [ ] Pack folder contains only your DLLs (+ third-party MIT/Apache/BSD dependencies you are licensed to ship) — never
CamelGraph.*orAutodesk.*assemblies. - [ ] LICENSE file included in the pack folder; license shown in your README.
9. Making a node look right in the editor¶
The editor builds a node's rows from your method signature, so most nodes need nothing extra. A handful of optional attributes (all in CamelGraph.Core.Loader) tune how the rows look. They are advisory: none of them changes the node's definition id, so adding one to an existing parameter never breaks saved .dyc files.
| You write | The editor shows |
|---|---|
double width = 200 |
A draggable number field with the default remembered; a dot marks it when changed. |
[NodeRange(0, 100, SoftMin = 0, SoftMax = 10, Step = 0.5, Unit = "mm")] double gap |
The field clamps to 0–100, its drag range is 0–10, it steps by 0.5 and prints mm after the value. |
[NodeChoices("Model", "Object", "Face")] string level |
A dropdown instead of a free text box — or a segmented switcher when there are two or three short values (24 characters in all). |
[NodeTabChoice("item")] string categoryName and [NodePropertyChoice("item", "categoryName")] string propertyName |
The text box stays (typing always works) and gets a small magnifier button. Pressed, it lists the property tabs — or the properties of the tab named by categoryName — of the element carried by the node's item input, in a drop-down. It reads only that element (a picked one, or what the wire delivered in the last run), only when pressed, and never searches the model; add IncludeAncestors = true when the node also looks at the element's parents, and UserDefinedOnly = true on a tab input when the node can only change the tabs a person added (the button then lists those and not the tabs that come from the model's source files). A node that searches the whole model has no element input; pass NodeDataSource.Selection as the source ([NodeTabChoice(NodeDataSource.Selection)]) and the button lists what the elements selected in the host right now carry. Only on string parameters; the host supplies the reader through ModelPropertyHost.Current (CamelGraph does for Navisworks). |
[NodePanel("Advanced")] double tolerance = 0.01 |
The input sits in a foldable Advanced panel (DefaultOpen = true starts it expanded). All inputs with the same panel name share one header, which starts collapsed and says "(2 set)" when values typed into it differ from the defaults. A collapsed panel only hides the rows: a typed value still reaches the node, and a wired input keeps its socket. Which panels the user opened is saved with the graph. |
[MultiInput] IEnumerable<ModelItem> items (any list-typed parameter) |
A multi-input pill: any number of wires connect to it. One wire arrives untouched — so adding the attribute to an existing parameter never changes a saved graph — and two or more arrive combined into one list, in the order the wires were made (list-valued wires contribute their elements, other values themselves, nulls nothing). Ignored on parameters that are not list-typed. |
[PortKinds("viewpoint*")] on an object parameter, or [PortKinds("text*", "integer")] on a [MultiReturn] method |
The socket takes the colour and shape of that kind: a family name (number, integer, boolean, text, datetime, colour, geometry, item, selection, viewpoint, clash, document, data, file, action), then * for a list or ** for a list of lists. |
ModelItem, List<ModelItem> or ModelItemCollection parameter |
A model-element picker: Use selection takes the current Navisworks selection, clicking the value re-selects it, ✕ clears it. |
bool, Color, DateTime, enums |
A checkbox, a colour swatch, a text field holding an ISO date, and a dropdown (or segmented switcher) of the enum's names. |
[NodePath(NodePathMode.Save, Filter = "Excel workbooks (*.xlsx)\|*.xlsx")] string path |
A file field whose … button opens the dialog you name: Open (the file must exist; a file the node reads), Save (a save dialog, a file that does not exist yet can be chosen; a file the node writes or creates) or Folder (a folder chooser). Filter is a Windows file dialog filter ("Text (*.txt)\|*.txt\|All files (*.*)\|*.*"); a save dialog adds the first extension when the user types a name without one. Only on string parameters. Put it on every path parameter of a file node: it also gives the button to a parameter whose name does not look like a path. |
a string parameter whose name ends in path, file, filename, folder, directory or dir, or is executable (or source / destination on a File.*, Directory.* or Zip.* node), with no [NodePath] |
A file field with a … button. The dialog is guessed: names ending in folder, directory or dir (and the path of a Directory.* node) open a folder chooser; a name that says output, save, export, target or destination, or a node whose operation starts with Write, Save, Export, Append, Snapshot or Create (or reads To…File, or is Export.*), opens a save dialog; everything else an open dialog. A string that merely sounds like a path but is not one on disk (a viewpoint folder name, say) takes [PortKinds("text")] to lose the button. |
Guidelines:
- Give every number a
[NodeRange]when a sensible range exists — it turns a blind text box into a slider-like field and stops absurd values. - Prefer
[NodeChoices]to documenting "one of A, B, C" in the description. - A node that takes an element plus the name of one of its tabs or properties should mark those parameters with
[NodeTabChoice]/[NodePropertyChoice], pointing at the element input; a node that searches the whole model should not (there is no element to read). - Mark every file or folder parameter with
[NodePath]instead of leaning on its name: a writer that opens an Open dialog cannot be pointed at a file that does not exist yet. A File Path input node wired into a writer opens a save dialog by itself. - Keep the main inputs unpaneled and move rare options into one
[NodePanel("Advanced")]; the node stays short and the panel is one click away. - Use
[PortKinds]whenever you returnobjectfrom a[MultiReturn]method, so the wires downstream are coloured correctly and the editor can filter the node search when a wire is dropped on the canvas. - Mark a list parameter
[MultiInput]when a caller would reasonably want to feed it from several places — "these items, and those, and the current selection". Do not use it on a list whose nesting matters (a list of lists that should replicate the node once per sublist): with several wires the outer level is concatenated, so each wire's sublists merge into one list of sublists. - In a hand-written
NodeModel, declare the port withAddMultiInput(name, typeof(IList<object>))in the constructor. - Every attribute is listed with its editor result in the editor guide.
Nodes that read or write files: resolve the path¶
A node that opens, writes, lists, copies or deletes a file passes the path it is given through PathResolver.Resolve (namespace CamelGraph.Core.Files) before it touches the disk:
using CamelGraph.Core.Files;
public static string ReadAll([NodePath(NodePathMode.Open)] string path)
=> System.IO.File.ReadAllText(PathResolver.Resolve(path));
Resolve removes the spaces and quotes around a path pasted from Explorer's "Copy as path", and turns a relative path into one next to the graph: the host sets GraphContext.Folder before it runs a graph (the editor: the folder of the open graph file, or Documents\CamelGraph for a graph that has not been saved; the Script Player and CamelGraph.Cli: the folder of the script file). Without a host folder it falls back to the process's current directory, which inside Navisworks is the program folder, so never rely on that. A blank path comes back as it is and Resolve never throws, so the node reports a missing path in its own words. A host that runs graphs itself wraps the run in using (GraphContext.Use(GraphContext.FolderFor(graphFilePath))) { ... }, which puts the previous folder back afterwards. Also declare what the node does to the disk with [NodeEffects(NodeEffects.WritesFiles)] (it creates, replaces or appends to a file) or ChangesFiles (it deletes, moves or copies over files), and ChangesModel for a node that edits the open Navisworks model: the editor and the Script Player list such nodes before they run a graph that came from a file.
Attributes that change how a node runs¶
Some attributes say nothing about how the node looks; they tell the engine how to treat a port or the whole node. They are advisory in the same sense as the ones above: none of them changes the definition id, so adding one to a shipped node never breaks a saved graph.
[AcceptsNull]on a parameter — by default anullelement of a list the node is mapped over never reaches the node: that position gets anullresult and the node shows one warning ("1 of 3 laced calls received a null element"). Mark the parameter when the node's job is to answer the empty case itself — a test for "is this blank?", a join that treats a missing cell as empty text. The null is then passed to the method and its answer is used. Only the marked parameter changes; a single call with a null, and every other parameter, behave as before. The parameter must be able to hold null (a reference or nullable type): on a plaindoublethe engine still says "Null value passed to input".
// ["a", null, ""] gives [false, true, true]; without [AcceptsNull] it gave [false, null, true] plus a warning.
public static bool IsBlank([AcceptsNull] string text) => string.IsNullOrWhiteSpace(text);
[ScalarInput]on anobjectparameter — anobjectport means "anything", so a list wired to it arrives whole and the node runs once. When the parameter semantically takes one thing (a name, a point, a vector, a viewpoint, a value to compare), mark it: the port then counts as rank 0, like adouble, and a list maps the node over its elements (lacing), so "batch it by wiring a list" is true. Nested lists map level by level. A node that really wants the whole list should not use it, and the port's List Levels setting can still hand a whole list over. It is ignored on parameters that are not declaredobject. The socket is drawn as a single item.
// A list of names wired to 'name' renames once per name; a single name still works.
public static object Rename(object item, [ScalarInput] object name) { /* ... */ }
[LiveState]on a method — the engine only runs nodes whose inputs changed and serves the stored output of the rest. A node that reads live host state (the current selection, the open document, the list of selection sets) has no input that changes when that state does, so without help it shows the state of its first run for ever. Mark it and the engine runs it on every run. The nodes wired after it run again only when what it produced is different from the previous run (lists are compared item by item), so a second Run with the same selection does not repeat the edits further down. A node group with such a node inside runs on every run too. Auto-run is not set off by it (nothing in the graph was edited), and a frozen or muted node is left alone.
10. Changing a node that is already shipped¶
Saved graphs are the contract. A .dyc file refers to a zero-touch node by its definition id — Namespace.Class.Method@parameterTypes — and stores each wire and each typed-in value by the port name (the parameter name, the [MultiReturn] key, or the return name). What you may change:
| Change | Safe? | What to do |
|---|---|---|
Display name ([NodeName]), category, description, search tags |
Yes | Nothing: none of them is part of the id. |
| Add, remove or retype a parameter (this changes the id) | With an alias | Put the old id on the method: [NodeAliases("Ns.Class.Method@double,double")]. Old files resolve to the new method and write the new id when saved. |
| Rename an input or an output | With an alias | [PortAlias("oldName", "newName")] on the method, once per renamed port. A wire or value saved under the old name finds the port. |
| Retire a node | With a replacement | Mark it [NodeDeprecated("Use Category.Replacement")] and have it call the replacement. It stays registered — old graphs load and run — but is left out of the library, the quick search, the CLI list and the catalogue, and its description says what to use instead. Never delete a shipped node. |
Without an alias, a wire or value whose port no longer exists is dropped when the graph opens; the editor then says so in the status bar ("N connections or values could not be restored") instead of losing it silently.
After adding, renaming or retiring a node, run python3 tools/generate_node_catalog.py and commit docs/camelgraph-nodes.json and docs/NODE_CATALOG.md; CI fails when they are out of date.