# @vahor/n8n-kit

Build n8n workflows using TypeScript code with full type safety and IDE support.

## Installation

```sh
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 container
- `Chain` - Workflow chain builder
- `Workflow` - Workflow definition
- `Credentials` - Credential management
- `NodejsFunction` - Bundle TypeScript/JavaScript code for Code nodes

### `@vahor/n8n-kit/nodes`
Manually implemented nodes with full type definitions:
- `If` - Conditional logic node
- `ScheduleTrigger` - 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:

```typescript
// 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?

```ts
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 workflow
- `active` - Whether the workflow is active or not
- `tags` - Tags of the workflow
- `settings` - Settings of the workflow
- `inputSchema` - Input schema of the workflow. Only used for type validation when using `ExecuteWorkflow` and `ExecuteWorkflowTrigger`.
- `outputSchema` - Output schema of the workflow. Only used for type validation when using 
`ExecuteWorkflow` and `ExecuteWorkflowTrigger`.
- `definition` - Definition of the workflow. Can be a function that takes the workflow as parameter (required for `Group` nodes)

For all type parameters, you'll have to use the `type` function from [`arktype`](https://arktype.io/docs/primitives) 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:

```ts
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:

```ts
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:

```typescript
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:

```typescript
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:

```typescript
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

> [!WARNING]
> 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

```typescript
// 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

```typescript
$("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:

```typescript
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

```typescript
$("json.text")
	.call("toUpperCase")
	.call("split", " ")
	.call("join", "-")
	.toExpression()
// Results in: "={{ $json.text.toUpperCase().split(' ').join('-') }}"
```

### Apply

If you need to run an arbitrary transform that is easier to write as a lambda, use `apply`:

```ts
$("json.text").apply((text) => text.toUpperCase().split(" ").join("-"))
// Results in: "={{ $json.text.toUpperCase().split(' ').join('-') }}"
```

The function body is parsed and emitted as a direct method chain. Type inference works as expected:
- The function parameter is typed as the current field type
- The output type is inferred from the function's return type (e.g., `string`, `string[]` after `split()`)
- Using condition in the lambda function to change the method call is supported at compile time only.


Notes:
- Not all JS helpers or globals are guaranteed to be available in n8n expressions.


### Typed Methods

The expression builder includes typed methods for common operations:

**Array methods**: `find`, `filter`, `join`, `first`, `last`\
**String methods**: `toLowerCase`, `toUpperCase`, `trim`, `split`

```typescript
// 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

```typescript
$("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:

```ts
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

```typescript
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:**
```json
{
	"name": "my-function",
	"dependencies": {
		"zod": "^4"
	}
}
```

**index.ts example:**
```typescript
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

```typescript
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:
```typescript
NodejsFunction.from({
	projectRoot: "/path/to/function",
	input: {
		param1: "value",
		param2: $("json.field"),         // Can use expression builder
	},
})
```

will be transformed to:
```js
return handler({
	param1: "value",
	param2: $json.field 
});
```

### How It Works

1. **Bundle Time**: Dependencies are installed with `npm ci` and code is bundled using `tsdown`
2. **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
