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
Code
nodes
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 usingExecuteWorkflow
andExecuteWorkflowTrigger
.outputSchema
- Output schema of the workflow. Only used for type validation when usingExecuteWorkflow
andExecuteWorkflowTrigger
.definition
- Definition of the workflow. Can be a function that takes the workflow as parameter (required forGroup
nodes)
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
Note: As these credentials are not deployed using the CLI, you don't need to add them to the app.
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 ci
and 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