Overview
This guide explains how to create custom nodes for NodeTool. Whether you’re adding a new AI model, a data transformation utility, or an integration with an external service, all nodes follow the same architectural patterns.
Node Architecture
Base Classes
All nodes inherit from one of three base classes provided by nodetool.workflows.base_node:
BaseNode: The standard class for processing nodes. Most of your nodes will inherit from this.InputNode: Special nodes that serve as entry points for data into the workflow.OutputNode: Special nodes that capture the final results of a workflow.
The Processing Context
Every node execution receives a ProcessingContext object. This is your gateway to the runtime environment, providing access to:
- Asset Management: Loading and saving images, audio, and files.
- AI Providers: Accessing configured LLM, embedding, or image generation providers.
- Logging: Sending logs and progress updates to the UI.
Anatomy of a Node
A typical node consists of four main parts:
- Class Definition: Inherits from
BaseNode. - Metadata: Docstrings and class methods that define how the node appears in the UI.
- Inputs: Pydantic fields that define parameters and connections.
- Processing Logic: An async method that performs the actual work.
Example Structure
from pydantic import Field
from nodetool.workflows.base_node import BaseNode
from nodetool.workflows.processing_context import ProcessingContext
class MyCustomNode(BaseNode):
"""
Short description of the node.
keyword1, keyword2, search_terms
Use cases:
- Explain when to use this node
"""
# Inputs
text: str = Field(default="", description="Input text")
factor: float = Field(default=1.0, ge=0.0, le=10.0)
# Processing Logic
async def process(self, context: ProcessingContext) -> str:
return self.text * int(self.factor)
Implementation Patterns
1. Simple Processing
For nodes that take inputs and return a single result, implement the process method. The return type annotation is used to determine the output type in the UI.
async def process(self, context: ProcessingContext) -> str:
return "Result"
2. Multiple Outputs
If your node produces multiple results (e.g., an audio transcription that returns both text and detected language), use a TypedDict to define the output structure.
from typing import TypedDict
class OutputType(TypedDict):
text: str
confidence: float
async def process(self, context: ProcessingContext) -> OutputType:
return {"text": "Hello", "confidence": 0.99}
3. Streaming (Generators)
For nodes that produce results incrementally (like reading a large file or generating tokens), use gen_process and return an AsyncGenerator.
from typing import AsyncGenerator
async def gen_process(self, context: ProcessingContext) -> AsyncGenerator[str, None]:
for i in range(5):
yield f"Chunk {i}"
4. Dynamic Nodes
Some nodes need to change their inputs or outputs based on configuration.
- Dynamic Inputs: Set
_is_dynamic = True. - Dynamic Outputs: Set
_supports_dynamic_outputs = Trueand implementget_dynamic_output_slots().
Working with Assets
NodeTool uses reference objects (like ImageRef, AudioRef) to pass heavy assets between nodes without loading them entirely into memory.
Receiving Assets
Use the ProcessingContext to convert references into usable data.
async def process(self, context: ProcessingContext) -> ImageRef:
# Convert input ImageRef to PIL Image
pil_image = await context.image_to_pil(self.input_image)
# Process the image...
processed_pil = pil_image.rotate(90)
# Save back to an asset
return await context.image_from_pil(processed_pil)
Saving Assets
When your node creates a new file, save it using the context to ensure it’s properly stored and accessible.
# Save text to a file asset
asset_ref = await context.create_asset(
filename="output.txt",
mime_type="text/plain",
data=b"Hello World"
)
Best Practices
- Docstrings Matter: The first line of your docstring becomes the node’s description in the UI. The lines following are used for search keywords.
- Type Hints: Always type-hint your inputs and return values. The system uses these to validate connections between nodes.
- Pydantic Constraints: Use
Field(ge=0, le=100)to enforce limits on numeric inputs directly in the UI. - Async/Await: All processing is asynchronous. Use
awaitfor I/O operations to keep the workflow responsive. - Error Handling: Let exceptions propagate or raise clear
ValueErrors. The system will catch them and display them on the node in the UI.
File Structure
Nodes are organized by category in src/nodetool/nodes/:
nodetool/: Core nodes (text, image, control flow).lib/: Wrappers for standard libraries (json, math, pillow).openai/,gemini/, etc.: Provider-specific integrations.
To add a new node, simply create a new file in the appropriate directory (or add to an existing one). The system automatically discovers all classes inheriting from BaseNode.