Agent Skillsnodetool-ai/nodetool › nodetool-custom-node-developer

nodetool-custom-node-developer

GitHub

用于开发 NodeTool 自定义节点,指导创建 BaseNode 子类、配置属性装饰器及实现处理逻辑。

.claude/skills/nodetool-custom-node-developer/SKILL.md nodetool-ai/nodetool

Trigger Scenarios

创建自定义节点 扩展 NodeTool 功能

Install

npx skills add nodetool-ai/nodetool --skill nodetool-custom-node-developer -g -y
More Options

Non-standard path

npx skills add https://github.com/nodetool-ai/nodetool/tree/main/.claude/skills/nodetool-custom-node-developer -g -y

Use without installing

npx skills use nodetool-ai/nodetool@nodetool-custom-node-developer

指定 Agent (Claude Code)

npx skills add nodetool-ai/nodetool --skill nodetool-custom-node-developer -a claude-code -g -y

安装 repo 全部 skill

npx skills add nodetool-ai/nodetool --all -g -y

预览 repo 内 skill

npx skills add nodetool-ai/nodetool --list

SKILL.md

Frontmatter
{
    "name": "nodetool-custom-node-developer",
    "description": "Create custom NodeTool nodes, implement BaseNode subclasses, use @prop decorators, build node packages with process\/genProcess methods, register nodes, handle media refs and secrets. Use when user asks to create a node, add a node type, build a custom node, implement a processor, or extend NodeTool with new functionality."
}

You are a NodeTool node developer. You create TypeScript nodes that extend BaseNode from @nodetool-ai/node-sdk.

Package Layout

my-nodes/
├── package.json          # depends on @nodetool-ai/node-sdk
├── tsconfig.json         # extends ../../tsconfig.base.json
├── src/
│   ├── index.ts          # exports registration function + ALL_NODES array
│   └── nodes/
│       ├── category-a.ts # node classes
│       └── category-b.ts
└── tests/
    └── nodes.test.ts

Node Template

import { BaseNode, prop } from "@nodetool-ai/node-sdk";

export class MyNode extends BaseNode {
  // REQUIRED static fields
  static readonly nodeType = "mypack.category.NodeName"; // namespace.category.Name
  static readonly title = "Human-Readable Title";
  static readonly description = "Brief description.\n    search, keywords, here";
  static readonly metadataOutputTypes = { output: "str" }; // declare all outputs

  // Inputs via @prop decorator
  @prop({ type: "str", default: "", title: "Input Text" })
  declare input_text: any;

  @prop({ type: "int", default: 10, min: 1, max: 100, title: "Count" })
  declare count: any;

  // Process method — MUST return all keys from metadataOutputTypes
  async process(): Promise<Record<string, unknown>> {
    const text = String(this.input_text ?? "");
    const count = Number(this.count ?? 10);
    return { output: text.repeat(count) };
  }
}

Input Resolution Pattern

Property values and connected input values are assigned to instance fields before process() is called, so always read inputs from this:

const value = this.field ?? defaultValue;
  • this.field — resolved value (connection value if connected, else the user-set property, else the @prop default)
  • defaultValue — extra in-code fallback for null/undefined

Do not declare a process(inputs: Record<string, unknown>) parameter — the runtime calls process(context?: ProcessingContext), so an inputs parameter typed that way fails strict type-checking.

@prop Decorator Options

Option Type Notes
type string Required: "int", "float", "str", "bool", "list[any]", "image", "audio", "video", "document", "dataframe", "enum", "any"
default unknown Default value
title string Display name in UI
description string Tooltip text
min / max number Numeric bounds
required boolean Validation flag
values (string|number)[] Enum options

Patterns

Multi-Output Node

static readonly metadataOutputTypes = { text: "str", confidence: "float" };

async process(): Promise<Record<string, unknown>> {
  return { text: "result", confidence: 0.95 }; // ALL keys must be returned
}

Streaming Node (genProcess)

static readonly isStreamingOutput = true;
static readonly metadataOutputTypes = { output: "str" };

async *genProcess(): AsyncGenerator<Record<string, unknown>> {
  for (const item of items) {
    yield { output: item };
  }
}

Stateful Collector

static readonly syncMode = "on_any"; // fires on each incoming value

@prop({ type: "any", title: "Item" })
declare item: unknown;

private collected: unknown[] = [];

async initialize(): Promise<void> {
  this.collected = []; // reset per run
}

async process(): Promise<Record<string, unknown>> {
  this.collected.push(this.item);
  return { output: this.collected };
}

Enum Input

@prop({ type: "enum", values: ["low", "medium", "high"], default: "medium", title: "Quality" })
declare quality: any;

Secrets / API Keys

static readonly requiredSettings = ["MY_API_KEY"];

async process(): Promise<Record<string, unknown>> {
  // Resolved secrets are available via the `_secrets` getter.
  const apiKey = this._secrets.MY_API_KEY || process.env.MY_API_KEY;
  if (!apiKey) throw new Error("MY_API_KEY not configured");
  // use apiKey...
}

Media Refs (Image/Audio/Video)

@prop({ type: "image", title: "Input Image" })
declare image: { uri?: string; data?: string } | undefined;

async process(): Promise<Record<string, unknown>> {
  const img = this.image;
  if (!img) throw new Error("No image provided");
  // img.data is base64, img.uri is a URL
  const resultData = await processImage(img);
  return { output: { type: "image", data: resultData } }; // return base64
}

Lifecycle Hooks

async initialize(): Promise<void> { }   // once at start of run
async preProcess(): Promise<void> { }   // before each process() call
async finalize(): Promise<void> { }     // after all processing

Optional Static Properties

static readonly isDynamic = true;            // dynamic schema
static readonly supportsDynamicOutputs = true;
static readonly isStreamingInput = true;
static readonly basicFields = ["prompt", "model"]; // shown first in UI

Scaffold a Pack

The fastest start is the scaffolder, which generates a self-contained pack (package.json with the nodetool manifest field, tsconfig, an example node, a test, and a README):

npm run create:pack -- @myorg/my-nodes        # in the nodetool repo
# or directly: node scripts/create-pack.mjs @myorg/my-nodes ./my-nodes

Registration

// src/index.ts
import type { NodeClass, NodeRegistry } from "@nodetool-ai/node-sdk";
import { MyNode, OtherNode } from "./nodes/my-nodes.js";

export const ALL_NODES: readonly NodeClass[] = [MyNode, OtherNode];

export function register(registry: NodeRegistry): void {
  for (const nodeClass of ALL_NODES) {
    registry.register(nodeClass);
  }
}

The server auto-loads packs — no need to edit its source. Mark the package as a pack with a nodetool field in package.json, naming the export above:

{
  "name": "@myorg/my-nodes",
  "main": "dist/index.js",
  "nodetool": { "apiVersion": 1, "register": "register" }
}

On startup the server scans installed dependencies, imports any package with a nodetool field, and calls the named export with the registry. Install the built pack where the server can resolve it (npm install <pack>, or npm link for local dev) and restart. The export may be sync or async.

Trust model. Custom nodes run in-process as the server user (full filesystem/network/secret access, no sandbox), so loading is gated. In development unlisted packs load automatically; in production (NODETOOL_ENV=production) only packs on the allowlist (NODETOOL_PACKS_ALLOWLIST or ~/.config/nodetool/packs.json) load. Packs also cannot register under reserved namespaces (nodetool., lib., provider names) or shadow an existing node type. Only install packs you trust.

Testing

import { describe, it, expect } from "vitest";
import { MyNode } from "../src/nodes/my-nodes.js";

describe("MyNode", () => {
  it("processes input", async () => {
    // Constructor assigns properties to instance fields.
    const node = new MyNode({ input_text: "hello", count: 3 });
    const result = await node.process();
    expect(result.output).toBe("hellohellohello");
  });
});

Common Pitfalls

  • Forgetting output keys: Every key in metadataOutputTypes must appear in the return object
  • Wrong input access: Read inputs from this.field (assigned before process()); do not add an inputs parameter to process()
  • nodeType format: Must be namespace.category.Name with dots as separators
  • Mutable state without initialize(): If using instance fields, reset them in initialize()
  • Missing secrets declaration: Add to requiredSettings array for proper UI prompting

Version History

  • a6a7e57 Current 2026-08-20 09:50

Same Skill Collection

.claude/skills/ask-matt/SKILL.md
.claude/skills/ast-grep-outline/SKILL.md
.claude/skills/ast-grep/SKILL.md
.claude/skills/codebase-design/SKILL.md
.claude/skills/companion-clis/SKILL.md
.claude/skills/diagnosing-bugs/SKILL.md
.claude/skills/domain-modeling/SKILL.md
.claude/skills/flash/SKILL.md
.claude/skills/implement/SKILL.md
.claude/skills/improve-codebase-architecture/SKILL.md
.claude/skills/nodetool-api-reference/SKILL.md
.claude/skills/nodetool-browser-agent/SKILL.md
.claude/skills/nodetool-chat-cli/SKILL.md
.claude/skills/nodetool-deployment/SKILL.md
.claude/skills/nodetool-model-provider-config/SKILL.md
.claude/skills/nodetool-rag-indexing/SKILL.md
.claude/skills/nodetool-troubleshooter/SKILL.md
.claude/skills/nodetool-workflow-builder/SKILL.md
.claude/skills/prototype/SKILL.md
.claude/skills/research/SKILL.md
.claude/skills/resolving-merge-conflicts/SKILL.md
.claude/skills/setup-matt-pocock-skills/SKILL.md
.claude/skills/tdd/SKILL.md
.claude/skills/to-spec/SKILL.md
.claude/skills/to-tickets/SKILL.md
.claude/skills/triage/SKILL.md
.claude/skills/wayfinder/SKILL.md
.claude/skills/wizard/SKILL.md
.claude/skills/yts806379-everything-claude-code-e2e-testing/SKILL.md
packages/sandbox-packs/sandbox-aws/SKILL.md
packages/sandbox-packs/sandbox-chrono/SKILL.md
packages/sandbox-packs/sandbox-color/SKILL.md
packages/sandbox-packs/sandbox-csv/SKILL.md
packages/sandbox-packs/sandbox-dates/SKILL.md
packages/sandbox-packs/sandbox-decimal/SKILL.md
packages/sandbox-packs/sandbox-diff/SKILL.md
packages/sandbox-packs/sandbox-docx/SKILL.md
packages/sandbox-packs/sandbox-dsl/SKILL.md
packages/sandbox-packs/sandbox-epub/SKILL.md
packages/sandbox-packs/sandbox-exif/SKILL.md
packages/sandbox-packs/sandbox-expr/SKILL.md
packages/sandbox-packs/sandbox-fabric/SKILL.md
packages/sandbox-packs/sandbox-flow/SKILL.md
packages/sandbox-packs/sandbox-gif/SKILL.md
packages/sandbox-packs/sandbox-html/SKILL.md
packages/sandbox-packs/sandbox-ics/SKILL.md
packages/sandbox-packs/sandbox-jmespath/SKILL.md
packages/sandbox-packs/sandbox-mammoth/SKILL.md
packages/sandbox-packs/sandbox-markdown/SKILL.md

Metadata

Files
0
Version
a6a7e57
Hash
0d97b2c7
Indexed
2026-08-20 09:50

- 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-26 01:26
浙ICP备14020137号-1 $방문자$