template-generator

Generate workflow templates with coherent node graphs and integration tests

$ 安裝

git clone https://github.com/dafthunk-com/dafthunk /tmp/dafthunk && cp -r /tmp/dafthunk/.claude/skills/template-generator ~/.claude/skills/dafthunk

// tip: Run this command in your terminal to install the skill


name: template-generator description: Generate workflow templates with coherent node graphs and integration tests

Template Generator Skill

Generate workflow templates: discover nodes, design graphs, wire edges correctly, create tests.

Runtime Essentials

  • Topological execution: Nodes run in dependency order (edges define order)
  • Data flows through edges: sourceOutputtargetInput (port names must match exactly)
  • Skipping: If all upstream edges fail/skip, downstream nodes skip (not an error)
  • No cycles: Runtime rejects circular dependencies

Discover Nodes

Directory structure:

apps/api/src/nodes/
├── input/      # TextInputNode, ImageInputNode, NumberInputNode...
├── preview/    # TextPreviewNode, ImagePreviewNode, NumberPreviewNode...
├── text/       # Summarization, translation, sentiment
├── image/      # Generation, manipulation
├── audio/      # Processing, transcription
├── anthropic/  # Claude models
├── openai/     # GPT models
├── logic/      # ConditionalForkNode, ConditionalJoinNode
└── ...         # json/, math/, fetch/, browser/, etc.

Search commands:

Grep pattern="translate" path="apps/api/src/nodes" glob="*.ts"
Glob pattern="apps/api/src/nodes/text/*.ts"

Read node interface - look for nodeType.inputs and nodeType.outputs:

Read file_path="apps/api/src/nodes/text/bart-large-cnn-node.ts"

Key fields: inputs[].nametargetInput, outputs[].namesourceOutput

Trigger Types

The type field defines how the workflow is triggered:

TypeDescriptionEntry Node
manualUser-initiated via UI/APIInput nodes (TextInputNode, etc.)
email_messageTriggered by incoming emailReceiveEmailNode
http_requestTriggered by HTTP request (sync)HttpRequestNode
http_webhookTriggered by webhook (async)HttpRequestNode
scheduledTriggered on schedule (cron)ReceiveScheduledTriggerNode
queue_messageTriggered by queue messageReceiveQueueMessageNode

Finding trigger-compatible nodes: Nodes declare which triggers they work with via the compatibility field in their nodeType. Search for compatible nodes:

Grep pattern="compatibility:.*email_message" path="apps/api/src/nodes" glob="*.ts"
Grep pattern="compatibility:.*http_request" path="apps/api/src/nodes" glob="*.ts"

Create Template

File: apps/api/src/templates/{template-id}.ts

import type { WorkflowTemplate } from "@dafthunk/types";
import { TextInputNode } from "../nodes/input/text-input-node";
import { BartLargeCnnNode } from "../nodes/text/bart-large-cnn-node";
import { TextPreviewNode } from "../nodes/preview/text-preview-node";

export const myTemplate: WorkflowTemplate = {
  id: "my-template",
  name: "My Template",
  description: "What it does",
  icon: "file-text",
  type: "manual",
  tags: ["text", "ai"],
  nodes: [
    TextInputNode.create({
      id: "text-to-process",
      name: "Text to Process",
      position: { x: 100, y: 100 },
      inputs: { value: "Sample text...", rows: 4 },
    }),
    BartLargeCnnNode.create({
      id: "summarizer",
      name: "Summarizer",
      position: { x: 500, y: 100 },
    }),
    TextPreviewNode.create({
      id: "result",
      name: "Summary",
      position: { x: 900, y: 100 },
    }),
  ],
  edges: [
    { source: "text-to-process", target: "summarizer", sourceOutput: "value", targetInput: "inputText" },
    { source: "summarizer", target: "result", sourceOutput: "summary", targetInput: "value" },
  ],
};

Positioning: Inputs at x:100, processing at x:500, outputs at x:900. Stack vertically with 200px spacing.

Naming: IDs are kebab-case (text-to-translate). Names are short Title Case, omit "Preview" for outputs.

Logic Nodes

ConditionalForkNode - splits flow based on boolean:

  • Inputs: condition (boolean), value (any)
  • Outputs: true, false (only ONE has value)

ConditionalJoinNode - merges exclusive branches:

  • Inputs: a, b (exactly ONE must have value)
  • Output: result
[BooleanInput] ──condition──► [Fork] ──true──► [ProcessorA] ──►┐
[TextInput] ────value──────►        ──false─► [ProcessorB] ──►├─► [Join] ──► [Preview]

Register & Test

Register in apps/api/src/templates/index.ts:

import { myTemplate } from "./my-template";
export const workflowTemplates = [..., myTemplate];

Test file {template-id}.integration.ts:

describe("My Template", () => {
  it("should have valid structure", () => {
    expect(myTemplate.nodes).toHaveLength(3);
    expect(myTemplate.edges).toHaveLength(2);
    const nodeIds = new Set(myTemplate.nodes.map(n => n.id));
    for (const edge of myTemplate.edges) {
      expect(nodeIds.has(edge.source)).toBe(true);
      expect(nodeIds.has(edge.target)).toBe(true);
    }
  });
});

Run: pnpm typecheck && pnpm --filter '@dafthunk/api' test {template-id}

Type Compatibility

OutputCompatible Inputs
stringstring, any
numbernumber, any
booleanboolean, any
imageimage, blob, any
audioaudio, blob, any
jsonjson, any

Checklist

  • Nodes exist in codebase (verify with Glob/Read)
  • Edge ports match node definitions exactly
  • Types are compatible
  • Registered in index.ts
  • Tests pass