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.
60-second map
Section titled “60-second map”| Layer | What you touch |
|---|---|
| 1. Catalog | packages/shared/src/nodes/builtins.ts. Add a NodeDefinition |
| 2. Shape | packages/shared/src/graph.ts. NodeType, data interface, zod branch |
| 3. Defaults | packages/web/src/views/GraphEditor.vue. buildNode branch |
| 4. Card | packages/web/src/canvas/nodes/YourNode.vue + #node-… slot |
| 5. Run | Canvas execNode switch and packages/server/src/addon/workflows/executor.ts |
| 6. Theme | packages/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.
1. Add a NodeDefinition
Section titled “1. Add a NodeDefinition”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:
| Field | Effect |
|---|---|
textSource / textSink | Included in the text→text connection product |
structuralTo | Extra edges (e.g. session → context) outside that product |
forbidTextTo | Block pairs that would otherwise be allowed (skill → output) |
palette: false | Hidden from the Nodes tab (agents/sessions/skills use other tabs) |
wireMenu: false | Hidden from wire-drop / empty-canvas add |
wireMenu: […] | Several flavors (see context) |
skipExec | Never runs (note, group) |
skipInDetached | Also 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, …).
2. Type + persistence
Section titled “2. Type + persistence”In packages/shared/src/graph.ts:
- Add
"echo"to theNodeTypeunion. - Add
EchoNodeDataand include it inGraphNodeData. - Add a zod branch in
nodeDataSchema/graphNodeSchema. - Teach
portableNodeDatahow to export the node if needed.
Until this lands, graphs cannot round-trip the new type.
3. buildNode defaults
Section titled “3. buildNode defaults”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.
4. Vue card + inspector
Section titled “4. Vue card + inspector”- Create
packages/web/src/canvas/nodes/EchoNode.vue(composeNodeCardlike the others). - Register
<template #node-echo>on the Vue Flow canvas inGraphEditor.vue. - Optionally add
EchoPanel.vueand wire it in the inspector switch.
5. Execute on both runners
Section titled “5. Execute on both runners”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.
6. Theme
Section titled “6. Theme”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).
Verify
Section titled “Verify”cd packages/server && npm test -- node-definitionscd packages/web && npm run typecheckThen in the UI: palette → drag Echo → wire prompt → echo → output → ▶ Run. Detached: ≫ with the same graph.
Custom vs built-in
Section titled “Custom vs built-in”| Custom node | Built-in node | |
|---|---|---|
| Where | ~/.config/threadle/nodes/<id>/ | Repo (NodeDefinition + Vue + runners) |
| Ship | Folder / git import | threadle release |
| Catalog | Dynamic scan | builtins.ts |
| When | Your transforms and tools | First-party graph vocabulary |
If you only need stdin→stdout text, stop here and use Custom nodes or the stdlib pack instead.
Related
Section titled “Related”- NodeDefinition. What the catalog owns vs what stays hand-written
- Custom nodes. No core change required
- Stdlib pack. Curated utilities instead of new builtins
- Params & named ports. Richer custom I/O
- Graph model. What a node is on the canvas