| name | node-creator |
| description | Create custom Genfeed nodes using the SDK. Triggers on "create a new node", "add a custom node type", "build a node for X". |
| license | MIT |
| metadata | {"author":"genfeedai","version":"1.0.0"} |
Node Creator
You are an expert at creating custom nodes for Genfeed using the SDK. When the user describes a node they want to create, you generate the complete TypeScript code for the node definition.
SDK Overview
The Genfeed SDK provides a fluent builder API for creating custom nodes:
import { createNode, registerNode } from '@genfeedai/sdk';
const myNode = createNode('myOrg/customNode')
.name('My Custom Node')
.description('Does something useful')
.category('processing')
.input('image', 'image', 'Input Image', { required: true })
.output('image', 'image', 'Processed Image')
.config({ key: 'intensity', type: 'slider', label: 'Intensity', min: 0, max: 100 })
.process(async (data, ctx) => {
return { outputs: { image: processedImageUrl } };
})
.build();
registerNode(myNode);
Core Interfaces
CustomNodeDefinition
interface CustomNodeDefinition<TData extends CustomNodeData = CustomNodeData> {
type: string;
name: string;
description: string;
category: NodeCategory | 'custom';
icon?: string;
inputs: HandleDefinition[];
outputs: HandleDefinition[];
defaultData: Partial<TData>;
configSchema?: ConfigField[];
process: NodeProcessor<TData>;
validate?: NodeValidator<TData>;
?: <>;
}
HandleDefinition
interface HandleDefinition {
id: string;
type: HandleType;
label: string;
multiple?: boolean;
required?: boolean;
}
ConfigField
interface ConfigField {
key: string;
type: 'text' | 'number' | 'select' | 'checkbox' | 'slider' | 'textarea' | 'color';
label: string;
description?: string;
defaultValue?: unknown;
required?: boolean;
options?: Array<{ value: string; label: string }>;
min?: number;
max?: number;
step?: number;
placeholder?: string;
showWhen?: Record<string, unknown>;
}
ProcessorContext
interface ProcessorContext {
nodeId: string;
executionId: string;
workflowId: string;
inputs: Record<string, unknown>;
updateProgress: (percent: number, message?: string) => Promise<void>;
log: (message: string) => Promise<void>;
signal: AbortSignal;
}
ProcessorResult
interface ProcessorResult {
outputs: Record<string, unknown>;
metadata?: Record<string, unknown>;
}
Handle Types
| Type | Description | Example Use Cases |
|---|
image | Image URL (string) | Photos, generated images, frames |
video | Video URL (string) | Generated videos, clips |
audio | Audio URL (string) | Voice, music, sound effects |
text | Text string | Prompts, transcripts, captions |
number | Numeric value | Counts, dimensions, timestamps |
Node Categories
| Category | Description | Placement |
|---|
input | Data source nodes | Left side of canvas |
ai | AI generation/processing | Middle-left |
processing | Transform/modify media | Middle-right |
output | Final output collectors | Right side |
composition | Subworkflow nodes | Variable |
custom | User-defined nodes | Based on function |
Common Icons (Lucide)
Image, Video, AudioLines, MessageSquare, FileText, Sparkles, Brain, Mic,
Wand2, Layers, Scissors, Film, Crop, Maximize, Grid3X3, Pencil, Subtitles,
CheckCircle, GitBranch, ArrowRightToLine, Navigation
NodeBuilder Methods
| Method | Description |
|---|
.name(string) | Set display name |
.description(string) | Set description |
.category(string) | Set category ('input', 'ai', 'processing', 'output', 'custom') |
.icon(string) | Set Lucide icon name |
.input(id, type, label, options?) | Add input handle |
.output(id, type, label, options?) | Add output handle |
.config(ConfigField) | Add configuration field |
.defaults(Partial<TData>) | Set default data values |
.process(ProcessorFunction) | Set processing function |
.validate(ValidatorFunction) | Set validation function |
.cost(CostEstimator) | Set cost estimator |
.build() | Build the final definition |
Example Nodes
Image Filter Node
import { createNode, registerNode } from '@genfeedai/sdk';
interface ImageFilterData {
label: string;
status: 'idle' | 'pending' | 'processing' | 'complete' | 'error';
inputImage: string | null;
outputImage: string | null;
filterType: 'blur' | 'sharpen' | 'grayscale' | 'sepia';
intensity: number;
}
const imageFilterNode = createNode<ImageFilterData>('custom/imageFilter')
.name('Image Filter')
.description('Apply visual filters to images')
.category('processing')
.icon('Wand2')
.input('image', 'image', 'Input Image', { required: true })
.output('image', 'image', 'Filtered Image')
.config({
key: 'filterType',
type: 'select',
: ,
: ,
: [
{ : , : },
{ : , : },
{ : , : },
{ : , : },
],
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: { : [, ] },
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
})
.( (data, ctx) => {
{ inputImage, filterType, intensity } = data;
imageUrl = ctx.. ;
ctx.(, );
response = (, {
: ,
: .({ imageUrl, filterType, intensity }),
: ctx.,
});
result = response.();
ctx.(, );
{
: { : result. },
: { : filterType },
};
})
.( {
(!inputs.) {
{ : , : [] };
}
{ : };
})
.( ({
: ,
: ,
}))
.();
(imageFilterNode);
Text Summarizer Node
import { createNode, registerNode } from '@genfeedai/sdk';
interface TextSummarizerData {
label: string;
status: 'idle' | 'pending' | 'processing' | 'complete' | 'error';
inputText: string | null;
outputText: string | null;
maxLength: number;
style: 'bullet' | 'paragraph' | 'tldr';
}
const textSummarizerNode = createNode<TextSummarizerData>('custom/textSummarizer')
.name('Text Summarizer')
.description('Summarize long text into concise summaries')
.category('ai')
.icon('FileText')
.input('text', 'text', 'Input Text', { required: true })
.output('text', 'text', 'Summary')
.config({
key: 'style',
type: 'select',
label: ,
: ,
: [
{ : , : },
{ : , : },
{ : , : },
],
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
})
.( (data, ctx) => {
inputText = ctx.. ;
{ maxLength, style } = data;
ctx.(, );
summary = (inputText, { maxLength, style });
ctx.(, );
{
: { : summary },
};
})
.();
(textSummarizerNode);
Video Watermark Node
import { createNode, registerNode } from '@genfeedai/sdk';
interface VideoWatermarkData {
label: string;
status: 'idle' | 'pending' | 'processing' | 'complete' | 'error';
inputVideo: string | null;
inputImage: string | null;
outputVideo: string | null;
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
opacity: number;
scale: number;
}
const videoWatermarkNode = createNode<VideoWatermarkData>('custom/videoWatermark')
.name('Video Watermark')
.description('Add a watermark image overlay to videos')
.category('processing')
.icon('Layers')
.input('video', 'video', 'Input Video', { required: true })
.input('image', , , { : })
.(, , )
.({
: ,
: ,
: ,
: ,
: [
{ : , : },
{ : , : },
{ : , : },
{ : , : },
{ : , : },
],
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
: ,
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
: ,
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
})
.( (data, ctx) => {
videoUrl = ctx.. ;
watermarkUrl = ctx.. ;
{ position, opacity, scale } = data;
ctx.(, );
result = (videoUrl, watermarkUrl, {
position,
: opacity / ,
: scale / ,
});
ctx.(, );
{
: { : result. },
};
})
.();
(videoWatermarkNode);
Multi-Input Combiner Node
import { createNode, registerNode } from '@genfeedai/sdk';
interface ImageCombinerData {
label: string;
status: 'idle' | 'pending' | 'processing' | 'complete' | 'error';
inputImages: string[];
outputImage: string | null;
layout: 'grid' | 'horizontal' | 'vertical';
gap: number;
}
const imageCombinerNode = createNode<ImageCombinerData>('custom/imageCombiner')
.name('Image Combiner')
.description('Combine multiple images into a single image')
.category('processing')
.icon('Grid3X3')
.input('images', 'image', 'Input Images', { multiple: true, required: true })
.output('image', 'image', 'Combined Image')
.config({
key: 'layout',
type: ,
: ,
: ,
: [
{ : , : },
{ : , : },
{ : , : },
],
})
.({
: ,
: ,
: ,
: ,
: ,
: ,
: ,
})
.({
: ,
: ,
: [],
: ,
: ,
: ,
})
.( (data, ctx) => {
images = ctx.. [];
{ layout, gap } = data;
(images. < ) {
();
}
ctx.(, );
result = (images, { layout, gap });
ctx.(, );
{
: { : result. },
};
})
.();
(imageCombinerNode);
Backend Processor Pattern (BullMQ)
For nodes that require long-running jobs:
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
@Processor('custom-node-queue')
export class CustomNodeProcessor extends WorkerHost {
async process(job: Job<CustomNodeJobData>) {
const { nodeId, executionId, data, inputs } = job.data;
await job.updateProgress(10);
const result = await this.processNode(data, inputs);
await job.updateProgress(100);
return result;
}
}
Registration Pattern
import { imageFilterNode } from './imageFilter';
import { textSummarizerNode } from './textSummarizer';
import { videoWatermarkNode } from './videoWatermark';
export const customNodes = [
imageFilterNode,
textSummarizerNode,
videoWatermarkNode,
];
import { nodeRegistry } from '@genfeedai/sdk';
nodeRegistry.registerAll(customNodes);
Plugin Manifest
For distributing as a plugin:
import { PluginManifest } from '@genfeedai/sdk';
export const manifest: PluginManifest = {
name: '@myorg/genfeed-plugin-filters',
version: '1.0.0',
description: 'Image and video filter nodes for Genfeed',
author: 'My Organization',
license: 'MIT',
nodes: ['custom/imageFilter', 'custom/videoWatermark'],
minGenfeedVersion: '1.0.0',
homepage: 'https://github.com/myorg/genfeed-plugin-filters',
};
Instructions
When the user describes a custom node they want to create:
- Understand the purpose: What does the node do? What inputs/outputs does it need?
- Define the data interface: Create a typed interface for the node's data
- Choose appropriate handles: Select input/output types based on data flow
- Design configuration: Add config fields for user-adjustable settings
- Implement processing: Write the async process function with progress updates
- Add validation: Include input validation where appropriate
- Estimate costs: Add cost estimation if the node uses paid APIs
Always output complete, runnable TypeScript code that follows the SDK patterns.