| name | node-generator |
| description | Generate new workflow nodes with implementation, tests, and registry registration |
Node Generator Skill
Generate workflow nodes for Dafthunk: research requirements, create implementation and tests, register in the node registry.
Step 1: Research and Define Requirements
When a user requests a new node, research first, then present a complete specification for confirmation.
Research the functionality:
- If based on a library/API: Use WebSearch or WebFetch to find official documentation
- Look for function signatures, parameters, return types, and examples
- Check if the package exists in
apps/api/package.json or search npm for the latest version
Check existing patterns:
- Search
apps/api/src/nodes/<category>/ for similar nodes
- Examine 2-3 similar implementations to understand input/output patterns and validation approaches
Draft complete requirements:
- Node purpose, category, name, and kebab-case ID
- Inputs: names, types, descriptions, required/optional, defaults, repeated (from function signature/docs)
- Outputs: primary outputs and metadata outputs (hidden: true for counts, flags, etc.)
- Icon: appropriate lucide icon name
- Tags: category + relevant keywords
- Dependencies: package name and version if needed
Present for confirmation:
Based on [library/API/functionality], here's the proposed node:
**Name**: [Node Name]
**ID**: `node-id`
**Category**: category
**Icon**: icon-name
**Inputs**:
- `inputName` (type, required/optional): Description
**Outputs**:
- `outputName` (type): Description
- `metadata` (type, hidden): Description
**Dependencies**:
- package-name@^version
**Tags**: Category, Tag1, Tag2
Does this match your requirements?
Only ask for information you cannot reasonably infer or research. The goal is to present a complete, research-backed specification that the user only needs to approve or tweak.
Step 2: Create Node Implementation
File: apps/api/src/nodes/<category>/<node-id>.ts
import { NodeExecution, NodeType } from "@dafthunk/types";
import { ExecutableNode, NodeContext } from "../types";
export class [NodeClassName]Node extends ExecutableNode {
public static readonly nodeType: NodeType = {
id: "[node-id]",
name: "[Node Display Name]",
type: "[node-id]",
description: "[One-line description]",
tags: ["Category", "Tag1", "Tag2"],
icon: "[icon-name]",
documentation: "[Detailed documentation]",
inlinable: false,
asTool: false,
inputs: [
{
name: "[inputName]",
type: "[type]",
description: "[Description]",
required: true,
repeated: false,
},
],
: [
{
: ,
: ,
: ,
},
],
};
(: ): <> {
{
{ input1, optionalInput = } = context.;
(input1 === || input1 === ) {
.();
}
( input1 !== ) {
.(
);
}
(.(input1)) {
( i = ; i < input1.; i++) {
( input1[i] !== ) {
.(
);
}
}
}
result = (input1);
.({ : result });
} (err) {
error = err ;
.();
}
}
}
Defensive programming checklist:
- Validate null/undefined, then types, then ranges/constraints
- Handle single values and arrays for repeated inputs
- Use descriptive error messages with input names and types
- Use nested try-catch for risky operations (parsing, external APIs)
- Handle edge cases: empty arrays/strings, zero/negative numbers
Step 3: Create Test File
File: apps/api/src/nodes/<category>/<node-id>.test.ts
import { Node } from "@dafthunk/types";
import { describe, expect, it } from "vitest";
import { NodeContext } from "../types";
import { [NodeClassName]Node } from "./<node-id>";
describe("[NodeClassName]Node", () => {
const createContext = (inputs: Record<string, unknown>): NodeContext => ({
nodeId: "[node-id]",
inputs,
getIntegration: async () => { throw new Error("No integrations in test"); },
env: {},
} as unknown as NodeContext);
it("should [perform expected operation]", async () => {
const node = new [NodeClassName]Node({ nodeId: "[node-id]" } as unknown as Node);
result = node.(({ : }));
(result.).();
(result.?.).();
});
(, () => {
node = []({ : } );
result = node.(({ : }));
(result.).();
});
(, () => {
node = []({ : } );
result = node.(({}));
(result.).();
(result.).();
});
(, () => {
node = []({ : } );
result = node.(({ : }));
(result.).();
(result.).();
});
(, () => {
node = []({ : } );
result = node.(({ : [, ] }));
(result.).();
});
(, () => {
node = []({ : } );
result = node.(({ : [, ] }));
(result.).();
(result.).();
});
});
Test coverage: Happy path, edge cases (empty/boundary values), error cases (missing/wrong types), array handling, type coercion (if applicable), domain-specific cases.
Step 4: Register the Node
File: apps/api/src/nodes/cloudflare-node-registry.ts
Add import (alphabetically within category):
import { [NodeClassName]Node } from "./<category>/<node-id>";
Register in constructor (alphabetically within category):
this.registerImplementation([NodeClassName]Node);
Step 5: Run Tests
pnpm typecheck
pnpm --filter '@dafthunk/api' test <node-id>
Step 6: Summary
List files created, confirm registry registration, show test command, note any dependencies to install.
Common Patterns
Repeated inputs (single value or array):
if (typeof values === "string") { }
if (Array.isArray(values)) { }
Number coercion:
const num = Number(input);
if (isNaN(num)) { return this.createErrorResult("Invalid number"); }
Optional inputs:
const { required, optional = "default" } = context.inputs;
External libraries:
try {
const result = library.function(input);
} catch (err) {
return this.createErrorResult(`Operation failed: ${(err as Error).message}`);
}