From n8n-workflow-sdk
Builds, tests, validates, and manages n8n workflows programmatically using @n8n/workflow-sdk. Supports code/JSON conversion, test data generation, validation, and AI agent workflows.
npx claudepluginhub geckse/n8n-skills --plugin n8n-workflow-sdkThis skill uses the workspace's default tool permissions.
Build, test, validate, and manage n8n workflows programmatically using the `@n8n/workflow-sdk`. This skill enables code-based workflow construction with full validation, JSON round-tripping, and testing support.
evals/evals.jsonreferences/code-generation.mdreferences/control-flow.mdreferences/expressions-and-code.mdreferences/node-registry-community.jsonreferences/node-registry-official.jsonreferences/nodes-and-connections.mdreferences/plugins-and-advanced.mdreferences/validation-and-testing.mdreferences/workflow-building.mdscripts/refresh-node-registry.shscripts/setup.shCreates, edits n8n workflows as TypeScript files with node docs access and n8nac CLI for workspace init, preventing param errors.
Provides proven architectural patterns for n8n workflows including webhook processing, HTTP API integration, database operations, AI agents, and scheduled tasks. Useful for high-level workflow design before node-by-node building.
Builds, tests, and deploys n8n workflows via REST API with incremental node testing. Activates for automation creation, workflow debugging, nodes, expressions, credentials, and JS/Python Code nodes.
Share bugs, ideas, or general feedback.
Build, test, validate, and manage n8n workflows programmatically using the @n8n/workflow-sdk. This skill enables code-based workflow construction with full validation, JSON round-tripping, and testing support.
This skill uses progressive disclosure. The SKILL.md covers the full workflow, decision-making, and quick-reference API. For complete patterns and examples, read these reference files:
references/workflow-building.md — Workflow creation, settings, JSON import/export, and complete building patternsreferences/nodes-and-connections.md — All node factories, subnodes (AI/LangChain), connection patterns, and credentialsreferences/control-flow.md — IF/Else, Switch/Case, Split in Batches, Merge, error handling, and branchingreferences/expressions-and-code.md — Expression system, Code node helpers, fromAi, and expression contextreferences/validation-and-testing.md — Workflow validation, pin data, test data generation, and testing strategiesreferences/code-generation.md — JSON-to-code, code-to-JSON, parseWorkflowCode, and the generation pipelinereferences/plugins-and-advanced.md — Plugin system, type generation, Zod schemas, and extensibilityreferences/node-registry-official.json — Cached registry of all 556 official n8n nodes (name, version, description)references/node-registry-community.json — Cached registry of community nodes (name, version, packageName)references/node-registry-properties.jsonl — Node properties/parameters (one JSON line per node, grep for a specific node)Read the appropriate reference file before writing any code.
Before writing SDK code, the project needs @n8n/workflow-sdk and TypeScript tooling installed. If the project already has these, skip this section.
For a new project:
mkdir my-workflow-project && cd my-workflow-project
npm init -y
npm install @n8n/workflow-sdk
npm install --save-dev typescript tsx @types/node
For an existing project:
npm install @n8n/workflow-sdk
npm install --save-dev typescript tsx @types/node
Running workflow scripts:
npx tsx src/my-workflow.ts
The SDK is a pure TypeScript/JavaScript library — no build step is required. tsx runs .ts files directly. If the project already uses ts-node or another TypeScript runner, that works too.
DO NOT invent or guess node type identifiers. n8n has hundreds of nodes, each with a specific type string (e.g., n8n-nodes-base.slack). Using a wrong or made-up type produces a workflow that fails in n8n.
Always look up the real node type from the cached node registry before using it.
This skill ships with cached copies of the n8n node registries. Read these files to look up node types — no network requests needed:
references/node-registry-official.json — Index of all 556 built-in n8n nodes (name, version, description)references/node-registry-community.json — Index of community-contributed nodes (name, version, packageName)references/node-registry-properties.jsonl — Full node properties/parameters (one JSON line per node)Each official node entry has:
name — The node type identifier to use in node() / trigger() (e.g., "n8n-nodes-base.slack")displayName — Human-readable name (e.g., "Slack")version — Current version numberdescription — What the node doesgroup — "[\"trigger\"]" or other classificationalias — Alternative names for search (e.g., ["message", "chat"])categories — Node categories (e.g., ["Communication"])Each community node entry has:
name — The node type identifierdisplayName — Human-readable namepackageName — The npm package the user must install (e.g., "@mendable/n8n-nodes-firecrawl")version — Version numberisOfficialNode — Whether verified by n8nHow to find a node: Read the appropriate registry JSON index and search for the node by displayName, name, alias, or description.
Once you've found a node's name in the index, grep the properties file to get its available parameters:
grep 'n8n-nodes-base.slack' references/node-registry-properties.jsonl
Each line is a JSON object: {"node": "n8n-nodes-base.slack", "properties": [...]} where properties contains the node's parameter definitions — field names, types, defaults, options, required flags, and conditional display rules. Use this to correctly configure the parameters object in node() / trigger().
CRITICAL: Use option value, not display name. For options-type parameters, the properties JSONL lists both a display name (what the UI shows) and the actual value (what goes in the workflow JSON). These are often different — you must use the value. Common traps:
| Node | Parameter | Display Name | Actual Value |
|---|---|---|---|
| Set | mode | JSON | raw |
| HTTP Request | contentType | Form Urlencoded | form-urlencoded |
| Code | language | Python | pythonNative |
| Webhook | responseMode | Immediately | onReceived |
| Merge | mode | SQL Query | combineBySql |
CRITICAL: Set node v3.3+/3.4 uses assignments, NOT fields. The parameter schema changed at v3.3. If you're using Set node v3.4, you MUST use assignments.assignments (with type: 'string'/'number'/'boolean' and a single value field). The old fields.values / stringValue / numberValue format is for v3.0–3.2 only. See nodes-and-connections.md for full examples.
When the registry cache version field appears to be a rounded major version (e.g., 4 instead of 4.4), you can derive the actual latest version from @version hints in the properties JSONL. About 96 of 554 nodes include these hints.
Node properties use displayOptions.show["@version"] and displayOptions.hide["@version"] to control which fields appear for which versions. These arrays contain the version numbers the node supports, letting you infer the latest version.
How to extract version hints:
grep 'n8n-nodes-base.set' references/node-registry-properties.jsonl"@version" values in displayOptions"@version": [3, 3.1, 3.2] — the node supports versions 3, 3.1, and 3.2"@version": [{"_cnd": {"gte": 3.1}}] — applies to version ≥ 3.1Examples from real nodes:
"@version": [3, 3.1, 3.2] → latest is at least 3.2 (actual defaultVersion: 3.4)"@version": [{"_cnd": {"gte": 3.1}}] → latest is at least 3.1 (actual: 3.1)"@version": [{"_cnd": {"gte": 3.3}}] → latest is at least 3.3 (actual: 3.4)Limitations: This technique only provides a lower bound (the actual defaultVersion may be higher). ~458 of 554 nodes have no @version hints at all. The registry cache version field (updated via refresh-node-registry.sh which extracts real defaultVersion from npm packages) is always the most accurate source.
If a node can't be found in the cached registries (they may be outdated), run the refresh script to pull the latest data from the live APIs:
bash scripts/refresh-node-registry.sh
This fetches from https://api.n8n.io/api/nodes and https://api.n8n.io/api/community-nodes, installs the n8n-nodes-base and @n8n/n8n-nodes-langchain npm packages to extract accurate defaultVersion values (the API only returns major versions), strips heavy fields, and updates all three cache files. Run this when you suspect the cache is stale or when a user asks for a node that should exist but isn't in the cache.
When using a community node, the user must install the npm package in their n8n instance first. Always add a sticky() note to the workflow warning about the required community node package:
const installNote = sticky(
'⚠️ Required Community Node: Install the "@mendable/n8n-nodes-firecrawl" package in your n8n instance (Settings → Community Nodes → Install) before using this workflow.',
[firecrawlNode],
{ color: 5 }
)
references/node-registry-official.json and search for the matching nodereferences/node-registry-community.json and search therebash scripts/refresh-node-registry.sh to update the cache, then search againname and version from the registry in your node() / trigger() callreferences/node-registry-properties.jsonl for the node name to get its available parameters and configure it correctlysticky() note listing the required npm packageThese fundamental utility node type names are always safe to use without a registry lookup. However, always look up the correct version from references/node-registry-official.json — do NOT copy version numbers from code examples, as they may be outdated.
| Type | Description |
|---|---|
n8n-nodes-base.manualTrigger | Manual execution trigger |
n8n-nodes-base.webhook | HTTP webhook trigger |
n8n-nodes-base.scheduleTrigger | Cron/interval trigger |
n8n-nodes-base.httpRequest | Generic HTTP request |
n8n-nodes-base.set | Set/transform data fields (v3.4 uses assignments format — never fields.values or { options: {} }) |
n8n-nodes-base.code | Custom JavaScript/Python code |
n8n-nodes-base.if | Conditional branching |
n8n-nodes-base.switch | Multi-branch routing |
n8n-nodes-base.merge | Merge multiple inputs |
n8n-nodes-base.splitInBatches | Batch processing loop |
n8n-nodes-base.noOp | No operation (passthrough) |
n8n-nodes-base.stickyNote | Canvas annotation |
n8n-nodes-base.respondToWebhook | Respond to webhook |
n8n-nodes-base.filter | Filter items |
n8n-nodes-base.executeWorkflow | Execute sub-workflow |
@n8n/n8n-nodes-langchain.agent | AI Agent |
⚠️ IMPORTANT: Version numbers in code examples throughout this skill are illustrative and may be outdated. The only reliable source for the current version of any node is references/node-registry-official.json (or references/node-registry-community.json for community nodes). Always read the registry cache to get the correct version before using any node, even core utility nodes listed above.
For ANY integration node not in this list (Slack, Gmail, Notion, Airtable, Google Sheets, Postgres, etc.), you MUST look up the correct type and version from the registry cache before using it.
The SDK provides a fluent, chainable API for building n8n workflows in TypeScript/JavaScript. Instead of hand-crafting JSON, you construct workflows programmatically with full type safety, validation, and testing capabilities.
Key capabilities:
workflow(), node(), trigger()workflow.fromJSON(json) loads any n8n workflow JSON.toJSON() produces n8n-compatible workflow JSONgenerateWorkflowCode(json) produces readable SDK codeparseWorkflowCode(code) parses SDK code back to JSONvalidateWorkflow() checks structure, connections, expressions, and best practices.generatePinData() creates mock data from output declarationsimport { workflow, node, trigger, validateWorkflow } from '@n8n/workflow-sdk'
const myTrigger = trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} })
const httpNode = node({
type: 'n8n-nodes-base.httpRequest', version: 4.4,
config: {
parameters: { url: 'https://api.example.com/data', method: 'GET' },
output: [{ json: { id: 1, name: 'Example' } }]
}
})
const wf = workflow('my-workflow', 'My Workflow')
.add(myTrigger)
.to(httpNode)
.generatePinData()
const validation = validateWorkflow(wf)
if (validation.valid) {
const json = wf.toJSON() // Ready for n8n
}
import { workflow, node, generateWorkflowCode, validateWorkflow } from '@n8n/workflow-sdk'
// 1. Load from JSON (user provides their workflow JSON)
const wf = workflow.fromJSON(existingWorkflowJSON)
// 2. Convert to code to understand it
const code = generateWorkflowCode(existingWorkflowJSON)
// 3. Modify — add a new node
const newNode = node({
type: 'n8n-nodes-base.set', version: 3.4,
config: {
name: 'Transform',
parameters: {
mode: 'manual',
assignments: {
assignments: [
{ name: 'processed', value: true, type: 'boolean' },
{ name: 'label', value: '={{ $json.name }}', type: 'string' }
]
},
includeOtherFields: true,
options: {}
}
}
})
// Rebuild with modifications using the SDK
// 4. Validate
const result = validateWorkflow(wf)
console.log(result.errors, result.warnings)
// 5. Export back to JSON
const updatedJSON = wf.toJSON()
import { generateWorkflowCode, parseWorkflowCode, validateWorkflow } from '@n8n/workflow-sdk'
// JSON → Code (for reading/understanding)
const code = generateWorkflowCode(workflowJSON)
// Modify the code as needed...
// Code → JSON (for importing back to n8n)
const newJSON = parseWorkflowCode(modifiedCode)
// Validate before deploying
const result = validateWorkflow(newJSON)
import { workflow, node, trigger, languageModel, tool, memory, fromAi } from '@n8n/workflow-sdk'
const model = languageModel({
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi', version: 1.3,
config: {
parameters: {
model: { __rl: true, mode: 'list', value: 'gpt-4o-mini', cachedResultName: 'gpt-4o-mini' },
options: {}
},
credentials: { openAiApi: { name: 'OpenAI', id: 'cred-123' } }
}
})
const emailTool = tool({
type: '@n8n/n8n-nodes-langchain.toolGmail', version: 1,
config: {
parameters: {
recipient: fromAi('recipient', 'Email recipient address'),
subject: fromAi('subject', 'Email subject line')
}
}
})
const agent = node({
type: '@n8n/n8n-nodes-langchain.agent', version: 3.1,
config: {
name: 'AI Agent',
parameters: {
promptType: 'define',
text: '={{ $json.chatInput }}',
options: { systemMessage: 'You are a helpful AI assistant.' }
},
subnodes: { model, tools: [emailTool], memory: memoryNode }
}
})
const wf = workflow('ai-workflow', 'AI Agent Workflow')
.add(trigger({ type: 'n8n-nodes-base.manualTrigger', version: 1, config: {} }))
.to(agent)
⚠️ Model IDs in examples are illustrative and will become outdated. Always choose the most appropriate current model for the user's use case — do NOT copy model IDs like gpt-4o-mini from these examples. Check the provider's latest model offerings.
| Function | Purpose | Reference |
|---|---|---|
workflow(id, name, options?) | Create workflow builder | references/workflow-building.md |
workflow.fromJSON(json) | Import from n8n JSON | references/workflow-building.md |
node(input) | Create a regular node | references/nodes-and-connections.md |
trigger(input) | Create a trigger node | references/nodes-and-connections.md |
sticky(content, nodes?, config?) | Create a sticky note | references/nodes-and-connections.md |
placeholder(hint) | Create a placeholder value | references/nodes-and-connections.md |
newCredential(name) | Mark credential as new | references/nodes-and-connections.md |
ifElse(config) | Create IF node | references/control-flow.md |
switchCase(config) | Create Switch node | references/control-flow.md |
merge(config) | Create Merge node | references/control-flow.md |
splitInBatches(config) | Create Split In Batches | references/control-flow.md |
nextBatch(sib) | Loop-back for SIB | references/control-flow.md |
| Function | Purpose |
|---|---|
languageModel(input) | Language model (OpenAI, Anthropic, etc.) |
memory(input) | Memory (Buffer Window, etc.) |
tool(input) | Tool (Calculator, Code, Gmail, etc.) |
outputParser(input) | Output parser |
embedding(input) / embeddings(input) | Embedding model |
vectorStore(input) | Vector store (Pinecone, Qdrant, etc.) |
retriever(input) | Retriever |
documentLoader(input) | Document loader |
textSplitter(input) | Text splitter |
fromAi(key, description?, type?, defaultValue?) | AI-driven parameter |
| Function | Purpose |
|---|---|
serializeExpression(fn) | $ => $.json.name → ={{ $json.name }} |
parseExpression(expr) | ={{ $json.name }} → $json.name |
isExpression(value) | Check if string is an expression |
expr(expression) | Mark string as expression (adds = prefix) |
| Function | Purpose |
|---|---|
runOnceForAllItems(fn) | Code node: process all items at once |
runOnceForEachItem(fn) | Code node: process one item at a time |
| Function | Purpose |
|---|---|
validateWorkflow(wf, options?) | Validate workflow structure |
.generatePinData(options?) | Generate pin/test data from outputs |
.validate(options?) | Validate on the builder directly |
| Function | Purpose |
|---|---|
generateWorkflowCode(json) | JSON → TypeScript SDK code |
parseWorkflowCode(code) | SDK code → JSON |
parseWorkflowCodeToBuilder(code) | SDK code → WorkflowBuilder |
| Method | Purpose |
|---|---|
.add(node) | Add node to workflow |
.to(target) | Connect current node to target |
.connect(src, srcOut, tgt, tgtIn) | Explicit indexed connection |
.settings(settings) | Set workflow settings |
.getNode(name) | Get node by name |
.validate(options?) | Validate the workflow |
.toJSON() | Export as n8n JSON |
.toFormat(format) | Serialize with plugin |
.generatePinData(options?) | Generate test data |
.regenerateNodeIds() | Regenerate deterministic IDs |
| Method | Purpose |
|---|---|
.to(target, outputIndex?) | Connect to next node(s) |
.input(index) | Create input target (for Merge) |
.output(index) | Select specific output index |
.onTrue(target) | IF node: true branch |
.onFalse(target) | IF node: false branch |
.onCase(index, target) | Switch node: case branch |
.onError(handler) | Set error handler |
.update(config) | Create updated copy |
.getConnections() | Get declared connections |
Creating a brand-new workflow:
references/node-registry-official.json (and references/node-registry-community.json if needed) to get the correct type and version for every integration nodenode() and trigger() using the real types from the registrysticky() notes listing required npm packages.add() and .to()validateWorkflow().toJSON()Modifying an existing n8n workflow (JSON):
workflow.fromJSON(json)generateWorkflowCode(json) to understand it.toJSON()Understanding a complex workflow:
generateWorkflowCode(json) — produces readable TypeScriptTesting/verifying a workflow:
output declarations to nodes (expected output shapes).generatePinData() to create test datavalidateWorkflow() to check for errorsRound-tripping (code ↔ JSON):
generateWorkflowCode(json)parseWorkflowCode(code)parseWorkflowCodeToBuilder(code) (allows validation before JSON export)// Sequential: A → B → C
workflow('id', 'name').add(a).to(b).to(c)
// Fan-out: A → [B, C, D] (parallel branches, output indices 0, 1, 2)
workflow('id', 'name').add(a).to([b, c, d])
// Multi-input (Merge): A → merge.input(0), B → merge.input(1)
workflow('id', 'name')
.add(a).to(merge.input(0))
.add(b).to(merge.input(1))
// Explicit indexed: source output 0 → target input 1
workflow('id', 'name').connect(source, 0, target, 1)
// Output selection: use specific output of a multi-output node
nodeA.output(0).to(nodeB)
nodeA.output(1).to(nodeC)
// IF branching
ifNode.onTrue(trueNode).onFalse(falseNode)
// Switch branching
switchNode.onCase(0, caseA).onCase(1, caseB).onCase(2, caseC)
// Error handling
sourceNode.onError(errorHandler)
// Split in Batches loop
sib.onEachBatch(process.to(nextBatch(sib))).onDone(finalNode)
The SDK validates for 23+ error conditions. Key ones to watch for:
| Code | Meaning |
|---|---|
NO_NODES | Workflow has no nodes |
MISSING_TRIGGER | No trigger node found |
DISCONNECTED_NODE | Node not connected to flow |
INVALID_CONNECTION | Invalid connection between nodes |
MISSING_PARAMETER | Required parameter missing |
CIRCULAR_REFERENCE | Circular connection detected |
INVALID_EXPRESSION | Malformed expression |
AGENT_STATIC_PROMPT | AI agent has static prompt |
HARDCODED_CREDENTIALS | Credentials hardcoded in parameters |
MERGE_SINGLE_INPUT | Merge node has only one input |
TOOL_NO_PARAMETERS | Tool has no AI-driven parameters |
FROM_AI_IN_NON_TOOL | fromAi() used outside a tool node |
SUBNODE_NOT_CONNECTED | Subnode not connected to parent |
See references/validation-and-testing.md for the complete list and resolution strategies.
import { NODE_TYPES } from '@n8n/workflow-sdk'
NODE_TYPES.IF // 'n8n-nodes-base.if'
NODE_TYPES.SWITCH // 'n8n-nodes-base.switch'
NODE_TYPES.MERGE // 'n8n-nodes-base.merge'
NODE_TYPES.STICKY_NOTE // 'n8n-nodes-base.stickyNote'
NODE_TYPES.SPLIT_IN_BATCHES // 'n8n-nodes-base.splitInBatches'
NODE_TYPES.HTTP_REQUEST // 'n8n-nodes-base.httpRequest'
NODE_TYPES.WEBHOOK // 'n8n-nodes-base.webhook'
NODE_TYPES.DATA_TABLE // 'n8n-nodes-base.dataTable'
import {
isIfNodeType, isSwitchNodeType, isMergeNodeType,
isStickyNoteType, isSplitInBatchesType, isHttpRequestType,
isWebhookType, isDataTableType, isNodeChain, isNodeInstance
} from '@n8n/workflow-sdk'
This skill is for building and manipulating n8n workflows programmatically using the @n8n/workflow-sdk. It covers workflow creation, JSON import/export, validation, code generation, pin data, and the full SDK API.
This skill is NOT for:
n8n-node-builder skill instead)references/node-registry-official.json (official) or references/node-registry-community.json (community) to get the real type and version. Do NOT copy version numbers from code examples in this skill — they are illustrative and may be outdated. The registry cache is the single source of truth for versions. A wrong node type or version produces a broken workflow.sticky() note listing the npm packages the user must install in their n8n instance.validateWorkflow() before .toJSON(). The SDK catches 23+ error conditions that would silently produce broken workflows in n8n, so skipping validation means shipping bugs.output to node configs, then call generatePinData(). Without output declarations, there's no way to generate test fixtures automatically, and downstream nodes can't be tested with realistic data shapes.generateWorkflowCode() to understand existing workflows — Raw n8n JSON is verbose and hard to follow. The generated TypeScript code reveals the logical flow, branching, and node relationships at a glance.parseWorkflowCode() for round-tripping — Code → JSON → Code round-trips cleanly, making it safe to convert, edit in code, and convert back without data loss.parseWorkflowCodeToBuilder() when you need validation — Unlike parseWorkflowCode() which returns raw JSON, this returns a WorkflowBuilder so you can .validate() before .toJSON(), catching errors before they reach n8n.HARDCODED_CREDENTIALS to prevent this.fromAi() only in tool nodes — fromAi() generates $fromAI expressions that only the AI Agent node can resolve. Using them in regular nodes produces expressions that will fail at runtime. The validator catches FROM_AI_IN_NON_TOOL.expr() for dynamic values — Raw strings with ={{ }} are error-prone (missing = prefix, unbalanced braces). expr() handles the prefix automatically and makes intent clear in code.output on nodes to enable pin data generation — This is how you create test fixtures. Without declared outputs, generatePinData() has nothing to convert.sticky() for workflow documentation — Auto-positions around given nodes, making it easy to annotate sections of complex workflows for other developers.parameters: { options: {} } is broken and does nothing. Every Set node v3.4 MUST have mode: 'manual' with assignments.assignments containing at least one entry, OR mode: 'raw' with a jsonOutput string. Use type: 'string'/'number'/'boolean' and a single value field — NOT the old stringValue/numberValue format. If you don't know what fields to set, ask the user.gpt-4o-mini, claude-sonnet-4-20250514) are illustrative and become outdated. Always select the most appropriate current model for the user's task. Check the provider's latest offerings rather than copying from examples.