Skip to content

4 · Expert

Goal: stop adapting to threadle and make threadle yours — your own node types, your own reusable building blocks, graphs other people can run.


A custom node is a directory under ~/.config/threadle/nodes/<name>/ containing a node.json descriptor plus either a TypeScript class or any executable at all:

{
"id": "shout",
"label": "Shout",
"glyph": "!",
"entry": "node.ts",
"input": "text",
"output": "text"
}
export default class Shout {
async run(input: string): Promise<string> {
return input.toUpperCase() + "!!!";
}
}

Text arrives on stdin, text leaves on stdout. Prefer Python, jq, or a compiled binary? Swap entry for a command argv array. Nodes run as isolated child processes with a timeout, an output cap, and a minimal environment — a crashing node can’t take the server with it.

Settings → custom nodes → + Create node scaffolds both files and opens your editor.

See Custom nodes.


Two declarations turn a script into a first-class node.

Params render as widgets directly on the node card — numbers, checkboxes, dropdowns, textareas — and their values are stored per node in the graph:

"params": [
{ "name": "width", "type": "int", "label": "max width", "default": 72, "min": 10 },
{ "name": "mode", "type": "choice", "options": ["fold", "strip"] }
]

They reach your process as THREADLE_PARAMS (one JSON object), as THREADLE_PARAM_<NAME> variables, and as ctx.params for class nodes.

Named ports give the node more than one lane in and out:

"inputs": [
{ "name": "text" },
{ "name": "parts", "maxConnections": 8 }
],
"outputs": [{ "name": "head" }, { "name": "tail" }]

The card grows one labeled, typed handle per port; wires are validated per port; unwired required ports are flagged before the run. Each port is a single-wire slot by default — set maxConnections to allow fan-in / fan-out (the card shows ×N). The protocol switches to JSON — stdin is an object keyed by input port, stdout must be an object keyed by output port (a class node can simply return { head, tail }).

See Params & named ports.


Ports carry a value type: text, int, float, bool, json. Incompatible wires are refused while you drag (int widens to float; anything fits text), and values are validated again at run time — a node that claims int output but prints prose fails loudly instead of poisoning everything downstream.

See Lanes & value types.


4.4 Sub-workflows — build once, reuse everywhere

Section titled “4.4 Sub-workflows — build once, reuse everywhere”

Select a working cluster of nodes and save selection as sub-workflow. You get a linked ▦ frame: edit the sub-workflow once and every graph that uses it follows. At run time the frame resolves to its entry and exit members, so wiring into a frame behaves exactly like wiring into the subgraph.

See Frames & sub-workflows.


Workflows are portable JSON (threadle/graph@1). ⤒ Export copies a runtime-stripped graph to the clipboard; import pastes it back as a fresh copy with new ids, idle statuses, and edges re-validated. Machine-local references — session ids, payload hashes, absolute skill paths — are cleared on export, and skills/rules re-resolve by name on import, so a graph survives the trip to another machine.

Terminal window
threadle run ./my-flow.json --approve-all --param task="..."

Custom nodes ship the same way: push the folder to git, and anyone installs it with ⇣ Import. Importing never executes anything.

See Portable graphs and Sharing & importing.


4.6 The guarded fan — everything at once

Section titled “4.6 The guarded fan — everything at once”

The Guarded parallel example is the expert-level composite: parallel agents under a ply cap, per-branch tripwires, a knot merging the survivors, a splice gate before the result lands, and a graph-wide spend ceiling over the whole thing.

If you can read that graph and predict its behavior, you know threadle.

▶ Example: Guarded parallel fan


  • you’ve written a custom node with at least one param
  • you have a sub-workflow used by two different graphs
  • you’ve exported a workflow and run it on another machine
  • you reach for a tripwire before letting anything run unattended