Skip to content

Adding a built-in node

Prefer a custom node when the behavior is yours (a script, a transform, a local tool). Including utilities from the stdlib pack (jq, diff, rg, …). Use a built-in node only when threadle itself must own the type. New graph JSON, canvas chrome, runners, and docs.

Built-ins are registered once as a NodeDefinition. Menus, wiring rules, port limits, and mute/run flags are derived from that catalog so you do not hand-edit six parallel lists. This page is the contributor checklist. The linked page explains the catalog itself.

Recent example: Wait for idle (type: "wait-idle"). Park until a session is not generating. Also Until, Judge, agent out:err, and iterator mode: parallel. See Wait for idle · Until · Judge · Iterator · Agents.


LayerWhat you touch
1. Catalogpackages/shared/src/nodes/builtins.ts. Add a NodeDefinition
2. Shapepackages/shared/src/graph.ts. NodeType, data interface, zod branch
3. Defaultspackages/web/src/views/GraphEditor.vue. buildNode branch
4. Cardpackages/web/src/canvas/nodes/YourNode.vue + #node-… slot
5. RunCanvas execNode switch and packages/server/src/addon/workflows/executor.ts
6. Themepackages/web/src/theme/theme.css. --wire-your-type if it needs a color

Steps 1–2 are required for the type to exist. Steps 3–5 are required for it to appear and run. Palette / wire menu / mute / “can run” / connection product update automatically from the definition when you set the right flags.


Open packages/shared/src/nodes/builtins.ts and append an entry:

{
id: "echo", // must match NodeType
label: "Echo",
glyph: "↺",
wireColor: "var(--wire-echo)",
textSource: true, // can feed text sinks
textSink: true, // can accept text sources
ports: { in: 1, out: 1 },
requiresInput: true, // preflight: must be wired
emitsText: true, // soft lint for dangling outs
mutable: true, // mute / bypass in the context menu
executesOnRun: true, // counts toward ▶ Run
palette: {
label: "Echo",
glyph: "↺",
hint: "pass text through unchanged",
iconBg: "var(--accent)",
iconFg: "#0a0a0b",
order: 85,
},
wireMenu: {
label: "Echo",
glyph: "↺",
hint: "pass text through unchanged",
iconBg: "var(--accent)",
order: 85,
},
},

Useful flags:

FieldEffect
textSource / textSinkIncluded in the text→text connection product
structuralToExtra edges (e.g. session → context) outside that product
forbidTextToBlock pairs that would otherwise be allowed (skill → output)
palette: falseHidden from the Nodes tab (agents/sessions/skills use other tabs)
wireMenu: falseHidden from wire-drop / empty-canvas add
wireMenu: […]Several flavors (see context)
skipExecNever runs (note, group)
skipInDetachedAlso skipped by the server executor (subagent-run)
executesOnRun: "session-if-wired"Editor decides (session continuation)

The catalog lives under packages/shared/src/nodes/ (definition.ts, builtins.ts, registry.ts). Import helpers from @threadle/shared (getNodeDefinition, paletteBlockEntries, wireMenuBlocks, …).


In packages/shared/src/graph.ts:

  1. Add "echo" to the NodeType union.
  2. Add EchoNodeData and include it in GraphNodeData.
  3. Add a zod branch in nodeDataSchema / graphNodeSchema.
  4. Teach portableNodeData how to export the node if needed.

Until this lands, graphs cannot round-trip the new type.


In GraphEditor.vue, extend DragPayload.kind if needed and add a branch in buildNode that returns a GraphNode with sensible data defaults (empty text, labels, etc.).

Context-style flavors pass extra fields via wireMenu[].contextKind. See the existing context definition.


  1. Create packages/web/src/canvas/nodes/EchoNode.vue (compose NodeCard like the others).
  2. Register <template #node-echo> on the Vue Flow canvas in GraphEditor.vue.
  3. Optionally add EchoPanel.vue and wire it in the inspector switch.

Canvas (GraphEditor.vue → execNode) and detached (packages/server/src/addon/workflows/executor.ts) each need a case "echo":. Keep semantics aligned: same inputs, same outputs.set, same status transitions. Detached runs have no interactive UI. Gate on approveAll or skip if the node needs a human.


If wireColor points at a new token, define it:

:root {
--wire-echo: #94a3b8;
}
.vue-flow__edge.src-echo.vue-flow__edge-path {
stroke: var(--wire-echo);
}

Provider-colored nodes use wireColor: "provider" instead (session / agent-def).


Terminal window
cd packages/server && npm test -- node-definitions
cd packages/web && npm run typecheck

Then in the UI: palette → drag Echo → wire prompt → echo → output → ▶ Run. Detached: ≫ with the same graph.


Custom nodeBuilt-in node
Where~/.config/threadle/nodes/<id>/Repo (NodeDefinition + Vue + runners)
ShipFolder / git importthreadle release
CatalogDynamic scanbuiltins.ts
WhenYour transforms and toolsFirst-party graph vocabulary

If you only need stdin→stdout text, stop here and use Custom nodes or the stdlib pack instead.