The TypeScript DSL (@nodetool/dsl) provides type-safe factory functions for building NodeTool workflows in code. Define workflows programmatically with full IDE autocompletion, then serialize them to the same JSON format used by the visual editor.

Table of Contents

  1. Installation
  2. Core Concepts
  3. Basic Workflow
  4. Connecting Nodes
  5. Multi-Output Nodes
  6. Building the Workflow Graph
  7. Namespaces
  8. Code Generation
  9. Best Practices

Installation

The @nodetool/dsl package is part of the NodeTool monorepo:

npm install  # from the repo root — installs all workspace packages

Import namespaces directly:

import { math, constant, text, image } from "@nodetool/dsl";
import { workflow } from "@nodetool/dsl";

Or import from a specific namespace for tree-shaking:

import { add, multiply } from "@nodetool/dsl/generated/nodetool.math";

Core Concepts

OutputHandle

When you create a node, you get back a DslNode object. Its output() method returns an OutputHandle — a symbolic reference to one of the node’s output slots. You pass handles as inputs to other nodes to create connections.

const a = constant.integer({ value: 5 });
a.output()  // → OutputHandle<number> — reference, not the value itself

Connectable

Every input field accepts either a literal value or an OutputHandle:

const sum = math.add({ a: 1, b: 2 });           // literal values
const sum2 = math.add({ a: a.output(), b: 2 });  // connection + literal

DslNode

The frozen object returned by every factory function:

const node = constant.integer({ value: 42 });
node.nodeId    // unique UUID
node.nodeType  // "nodetool.constant.Integer"
node.inputs    // { value: 42 }
node.output()  // OutputHandle for the node's default output slot

Basic Workflow

A workflow follows three steps: create nodes, connect them, build the graph.

import { constant, math } from "@nodetool/dsl";
import { workflow } from "@nodetool/dsl";

// 1. Create nodes
const x = constant.float({ value: 3.14 });
const y = constant.float({ value: 2.0 });

// 2. Connect nodes by passing output handles
const sum = math.add({ a: x.output(), b: y.output() });

// 3. Build the workflow graph
const wf = workflow(sum);

console.log(wf.nodes);  // 3 nodes
console.log(wf.edges);  // 2 edges (x→sum, y→sum)

The workflow() function traces all connections from the terminal nodes back to their sources, producing a serializable Workflow object with nodes and edges.


Connecting Nodes

Linear Chain

const a = constant.integer({ value: 5 });
const b = math.add({ a: a.output(), b: 1 });
const c = math.multiply({ a: b.output(), b: 2 });

const wf = workflow(c);
// 3 nodes, 2 edges: a→b→c

Diamond (Shared Dependencies)

A node’s output can be connected to multiple downstream nodes. The graph builder deduplicates automatically.

const shared = constant.float({ value: 10 });
const left = math.add({ a: shared.output(), b: 1 });
const right = math.multiply({ a: shared.output(), b: 2 });
const final = math.add({ a: left.output(), b: right.output() });

const wf = workflow(final);
// 4 nodes, 4 edges — `shared` appears only once

Multiple Terminal Nodes

Pass multiple nodes to workflow() to trace all branches:

const branch1 = math.add({ a: x.output(), b: 1 });
const branch2 = math.multiply({ a: x.output(), b: 2 });

const wf = workflow(branch1, branch2);

Multi-Output Nodes

Some nodes produce multiple outputs (e.g., If has if_true and if_false). Use output("slotName") to select the slot you want:

import { control } from "@nodetool/dsl";

const branch = control.if_({ condition: true, value: "hello" });

// Access named outputs
branch.output("if_true")   // → OutputHandle
branch.output("if_false")  // → OutputHandle

// Calling output() without a slot throws when there is no default output

Each output slot is individually typed, so TypeScript catches type mismatches at compile time.


Building the Workflow Graph

workflow()

function workflow(...terminals: DslNode<any>[]): Workflow;

Traces from terminal nodes via BFS, discovers all connected nodes and edges, performs topological sort, and returns a frozen Workflow object.

The result can be serialized to JSON:

const wf = workflow(outputNode);
const json = JSON.stringify(wf, null, 2);

This JSON is compatible with the NodeTool workflow format used by the visual editor and the workflow runner.

run() / runGraph()

async function run(wf: Workflow, opts?: RunOptions): Promise<WorkflowResult>;
async function runGraph(...terminals: DslNode<any>[]): Promise<WorkflowResult>;

run() executes the graph locally via WorkflowRunner. By default it resolves executors from NodeRegistry.global, or you can pass an explicit registry via RunOptions.registry.


Namespaces

All 800+ nodes are organized by namespace. Import the namespace object and call factory functions:

Import Description Example
constant Fixed-value nodes constant.integer({ value: 5 })
math Math operations math.add({ a: 1, b: 2 })
text Text processing text.concat({ a: "hi", b: " there" })
image Image I/O image.loadImageFile({ path: "..." })
audio Audio processing audio.sliceAudio({ ... })
video Video processing video.trimVideo({ ... })
control Flow control control.if_({ condition: true, value: x })
agents AI agents agents.agent({ prompt: "..." })
kieImage KIE image services kieImage.removeBackground({ ... })
geminiText Google Gemini geminiText.gemini({ ... })
openaiText OpenAI text openaiText.chatGPT({ ... })
skills* Skill agents skillsBrowser.browserAgent({ ... })

See the full list in packages/dsl/src/generated/index.ts.


Code Generation

The factory functions are auto-generated from node metadata. To regenerate after adding or modifying nodes:

npm run codegen --workspace=packages/dsl

This reads all nodes registered in @nodetool/base-nodes, introspects their metadata (inputs, outputs, types, defaults), and emits typed factory functions into packages/dsl/src/generated/.

Generated files are committed to git. The codegen script is at packages/dsl/scripts/codegen.ts.

Type Mapping

Node Type TypeScript Type
str string
int, float number
bool boolean
image ImageRef
audio AudioRef
video VideoRef
list[T] T[]
dict[K,V] Record<K, V>
enum string literal union
any unknown

Best Practices

  1. Use namespace importsimport { math } from "@nodetool/dsl" gives you autocompletion for all math nodes.

  2. Let TypeScript catch errors — the DSL is fully typed. If you pass a string where a number is expected, the compiler tells you.

  3. Don’t reuse handles across builds — after calling workflow(), the internal registry is cleared. Handles from previous builds are stale and will throw if used in a new workflow() call.

  4. Build workflows linearly — create source nodes first, then processing nodes. The immutable API prevents cycles by construction.

  5. Serialize for interopJSON.stringify(workflow(node)) produces a workflow that can be loaded in the visual editor or executed via the API.