Skip to content

NodeDefinition

A NodeDefinition is the declarative description of one built-in graph node type. Prompt, Delay, Merge, Session, and so on. The catalog lives in threadle’s shared package (packages/shared/src/nodes/) and is the single place that owns identity, connectivity, port limits, and menu metadata for first-party types.

Custom nodes (folders under ~/.config/threadle/nodes/) are not NodeDefinitions. They all reuse the one built-in type custom. See Custom nodes.


Before the registry, adding a built-in meant editing about six parallel lists by hand: connection tables, port limits, palette rows, wire-menu rows, mute allowlist, “can run” switches. Those lists drifted.

Now you register once. Menus, wiring product, ports, mute, and run eligibility are derived from the definition. Vue cards and runner case arms stay explicit. They need real UI and I/O code. But the checklist is short. See Adding a built-in node.


FileRole
packages/shared/src/nodes/definition.tsNodeDefinition interface + PORT_UNLIMITED
packages/shared/src/nodes/builtins.tsOne entry per built-in type
packages/shared/src/nodes/registry.tsLookups + derived sets/menus

Import helpers from @threadle/shared:

import {
getNodeDefinition,
allNodeDefinitions,
paletteBlockEntries,
wireMenuBlocks,
isValidConnection,
REQUIRES_INPUT,
MUTABLE_NODE_TYPES,
} from "@threadle/shared";

FieldMeaning
idNodeType string ("prompt", "knot", …)
label / glyphCanonical name and tile glyph
wireColorCSS color, or "provider" for session/agent colors
textSource / textSinkParticipate in the text→text connection product
structuralToExtra edges outside that product (e.g. Session → context)
forbidTextToBlock pairs that would otherwise be allowed
portsDefault handle capacity (in / out. Use PORT_UNLIMITED)
requiresInputPreflight: must have an inbound wire
emitsTextSoft lint for dangling outbound text
mutableMute / bypass allowed
executesOnRunCounts toward ▶ Run (true or "session-if-wired")
skipExec / skipInDetachedNever run / also skip in the server executor
paletteNodes-tab row, or false if listed elsewhere
wireMenuWire-drop / empty-canvas entry (or an array of flavors)

Example (simplified Delay):

{
id: "delay",
label: "Delay",
glyph: "◷",
wireColor: "var(--wire-delay)",
textSource: true,
textSink: true,
ports: { in: 1, out: 1 },
requiresInput: true,
emitsText: true,
executesOnRun: true,
mutable: true,
palette: { /* label, glyph, hint, iconBg, order */ },
wireMenu: { /* same shape */ },
}

From the catalog, threadle builds:

  • Valid connections. Text sources × text sinks, plus structuralTo, minus forbidTextTo
  • Port limits. Default max wires per handle
  • Palette blocks. Nodes tab “blocks” section
  • Wire / add menu. Including context flavors (three context entries)
  • Mute set. Context-menu mute/bypass
  • Run eligibility. Which nodes make ▶ Run available
  • Skip sets. Canvas and detached executors

You should not hand-edit those lists for a new built-in. Change the definition instead.


Still written by hand when you add a type:

  1. NodeType + data shape + zod in graph.ts
  2. buildNode defaults in the editor
  3. Vue Flow card (#node-… slot)
  4. Canvas runner + detached executor cases
  5. Optional CSS --wire-… token

That split is intentional: the registry owns policy and discovery. The editor/server own pixels and side effects.


NodeDefinition (built-in)Custom node
Who ships itthreadle releaseYou (or an import)
StorageRepo catalog~/.config/threadle/nodes/<id>/
Graph typeIts own NodeTypeAlways type: "custom"
When to useFirst-party vocabularyYour transforms and tools

If you only need stdin→stdout text, stop at Custom nodes or the stdlib pack. Reach for NodeDefinition only when the type must be part of threadle itself.