Project n8n-kit
Expand description
@vahor/n8n-kit
Build n8n workflows using TypeScript code with full type safety and IDE support.
Installation
npm install @vahor/n8n-kit
Features
- 400+ Nodes: Support for all base n8n nodes plus AI nodes
- Type Safety: Full TypeScript support with intelligent autocomplete
- Expression Builder: Type-safe n8n expressions with IDE support
- Chain API: Fluent API for building workflow chains
- Bundle JS: Can bundle dependencies inside your
Codenodes
Exports
The package provides several export paths for different use cases:
@vahor/n8n-kit
Main export containing core functionality:
App- Application containerChain- Workflow chain builderWorkflow- Workflow definitionCredentials- Credential managementNodejsFunction- Bundle TypeScript/JavaScript code for Code nodes
@vahor/n8n-kit/nodes
Manually implemented nodes with full type definitions:
If- Conditional logic nodeScheduleTrigger- Schedule-based triggers- And more...
@vahor/n8n-kit/nodes/generated
Auto-generated nodes from n8n-nodes-base (without output types):
- All 400+ n8n base nodes and ai nodes
@vahor/n8n-kit/credentials/generated
Auto-generated credentials from n8n-nodes-base:
- All n8n credential types with proper typing
Example Init File
Your entrypoint file should export an app instance:
// src/index.ts
import { App, Workflow, Chain } from "@vahor/n8n-kit";
const app = new App();
new Workflow(app, "my-workflow", {
active: true,
name: "My Workflow",
definition: [
Chain.start(
// ...
),
],
});
export { app };
Core Concepts
App
The App class serves as the main container for your n8n workspace.
Passing an instance of App in the constructor of a Workflow will automatically add the workflow to the list of workflows to deploy.
Workflow
You know what a workflow is, right?
new Workflow(app, "my-workflow", {
active: true,
name: "My Workflow",
tags: ["tag1", "tag2"],
settings: {
executionTimeout: 1200,
},
inputSchema: type({
field: "string",
}),
outputSchema: type({
someField: "string",
otherField: "number",
}),
definition: (workflow) => [
new StickyNote("note", {
position: [0, 0],
content: "Hello World",
height: 120,
width: 600,
}),
anotherChain,
chainWithWorkflowParameter(workflow),
]
});
Parameters:
name- Name of the workflowactive- Whether the workflow is active or nottags- Tags of the workflowsettings- Settings of the workflowinputSchema- Input schema of the workflow. Only used for type validation when usingExecuteWorkflowandExecuteWorkflowTrigger.outputSchema- Output schema of the workflow. Only used for type validation when usingExecuteWorkflowandExecuteWorkflowTrigger.definition- Definition of the workflow. Can be a function that takes the workflow as parameter (required forGroupnodes)
For all type parameters, you'll have to use the type function from arktype to define the schema. The type method is re-exported in @vahor/n8n-kit.
Using an external workflow
Maybe not all your workflows are defined in your current application. If that's the case, you can still reference them by their ID:
const importedWorkflow = Workflow.import(app, {
// Needs either hashId or n8nWorkflowId
hashId: "my-workflow-hash-id", // Id generated by n8n-kit, visible in the CLI output or in the n8n tags
n8nWorkflowId: "my-workflow-n8n-id", // Id of the workflow in n8n
inputSchema: type({
// ...
}),
outputSchema: type({
// ...
}),
});
Then you'll be able to use that workflow in execution nodes:
import { ExecuteWorkflow } from "@vahor/n8n-kit/nodes";
new ExecuteWorkflow("call-to-external-workflow", {
workflow: importedWorkflow,
workflowInputs: {
// type-safe based on the input schema
},
});
Credentials
Credentials must be created in n8n first, then referenced by ID:
const googleDriveApiCredentials = Credentials.byId({
name: "googleDriveOAuth2Api", // Credential type (auto-completed - type derives from it)
id: "abc123", // n8n credential ID
});
To get the id, go on your n8n instance, click on a credential, and copy the id from the URL.
e.g. https://n8n.instance.com/home/credentials/yTwI5ccVwfGll1Kf => yTwI5ccVwfGll1Kf
Chain
Chains represent connected sequences of nodes:
Chain.start(triggerNode)
.next(processNode)
.next(({ $ }) => conditionalNode) // Access to expression builder
.multiple([Chain.start(node1).next(node11), node2]) // Multiple branches
.connect(["node-1", "node-2"], node3) // Connect to multiple nodes by their ids
Note: You can't use next() after multiple(), you'll have to use connect() to join the branches in one chain.
Node
Each node has a label and properties:
new NodeType("unique-id", {
label: "Display Name", // Optional, defaults to node id
position: [0, 0], // Optional, when missing the node is automatically positioned
disabled: true, // Optional, defaults to false
parameters: { // Node-specific parameters
// ... type-safe parameters
},
})
Expression Builder
This feature is experimental and may change in the future.
As a lot of n8n nodes are auto-generated they don't have output types.
The ExpressionBuilder provides type-safe n8n expressions with IDE support:
Basic Usage
// In .next() callback, $ gives access to previous node outputs
// json will be the previous node object. You can also access properties of all past nodes in the chain.
.next(({ $ }) =>
new SomeNode("id", {
parameters: {
value: $("json.propertyName").toExpression(),
},
})
)
$ is of type [$Selector<T>], you can use it as $Selector<typeof myChain>.
Property Access
$("json.user.name").toExpression()
// Results in: "={{ $json.user.name }}"
$("['Node Name'].nested.property").toExpression()
// Results in: "={{ $('Node Name').nested.property }}"
Node IDs are automatically replaced with the node label when the workflow is built:
const node = new Code("code", {
label: "Some very long label",
outputSchema: type({ type: "string" }),
parameters: {
jsCode: "...",
},
});
// and then
$("code.type").toExpression()
// Results in: "={{ $('Some very long label').item.json.type }}"
This avoids typing long names and changing references when the node label changes.
Method Calls
$("json.text")
.call("toUpperCase")
.call("split", " ")
.call("join", "-")
.toExpression()
// Results in: "={{ $json.text.toUpperCase().split(' ').join('-') }}"
Typed Methods
The expression builder includes typed methods for common operations:
Array methods: find, filter, join, first, last
String methods: toLowerCase, toUpperCase, trim, split
// Example with typed array and string methods
$("data").first(d => d.element.name.toLowerCase().startsWith("a")).toExpression()
// Results in: "={{ $('data').first((d) => d.element.name.toLowerCase().startsWith("a")) }}"
These typed methods provide full IDE support and type safety, equivalent to using .call() with the same method names.
Note: Using an array method on a non-array will result in a type error (same with string methods...).
Property Navigation
$("json.config")
.prop("['api.key']")
.toExpression()
// Results in: "={{ $json.config['api.key'] }}"
expr function
When using multiple expressions in one place, calling toExpression() can make it hard to read the code.
To avoid this, you can use the expr function:
import { expr } from "@vahor/n8n-kit";
expr`Hello ${$("json.name")}, you have ${$("json.count")} messages.`
// Results in: "=Hello {{ $json.name }}, you have {{ $json.count }} messages."
Note: You can still use string values directly instead of using the builder.
Code Bundling
The NodejsFunction class allows you to bundle dependencies inside your Code nodes, enabling you to use npm packages and write complex logic in separate files without having to install them on your n8n instance.
A similar class exists for PythonFunction without the bundling functionality. (does not support importing modules)
Basic Usage
import { NodejsFunction } from "@vahor/n8n-kit";
import { Code } from "@vahor/n8n-kit/nodes";
new Code("Bundle JS", {
parameters: {
language: "javaScript",
jsCode: NodejsFunction.from({
projectRoot: path.join(__dirname, "my-function"),
input: {
action: $("json.action"),
user_id: $("json.user_id"),
},
}),
},
})
Project Structure
Your bundled function should be in its own directory with a package.json:
my-function/
├── package.json
├── index.ts # Default entrypoint
└── package-lock.json
package.json example:
{
"name": "my-function",
"dependencies": {
"zod": "^4"
}
}
index.ts example:
import * as z from "zod/mini";
type Input = {
action: string;
user_id: string;
};
const handler = (input: Input) => {
const schema = z.object({
action: z.enum(["create", "update", "delete"]),
user_id: z.string().check(z.length(24)),
});
return schema.safeParse(input);
};
Configuration Options
NodejsFunction.from({
projectRoot: "/path/to/function", // Required: Directory with package.json
entrypoint: "index.ts", // Optional: Default is "index.ts" or "index.js"
mainFunctionName: "handler", // Optional: Default is "handler"
installCommand: ["npm", ["ci"]], // Optional: Install command to run before bundling (default: ["npm", ["ci"]])
input: { // Optional: Parameters to pass to the function
param1: "value",
param2: $("json.field"), // Can use expression builder
},
bundlerOptions: { // Optional: tsdown bundler options
// Advanced bundling configuration
},
})
Input Parameter
Passing input to the NodejsFunction constructor will automatically pass the input parameters to your function.
Example:
NodejsFunction.from({
projectRoot: "/path/to/function",
input: {
param1: "value",
param2: $("json.field"), // Can use expression builder
},
})
will be transformed to:
return handler({
param1: "value",
param2: $json.field
});
How It Works
- Bundle Time: Dependencies are installed with
npm ciand code is bundled usingtsdown - Output: The returned value of your main function becomes the node's output
TypeScript Integration
You need to enable strict mode in your tsconfig.json file