Năm 2025 rồi mà vẫn còn rất nhiều người cần tạo custom node vì API nội bộ, tool riêng hoặc chỉ đơn giản là muốn “tùy biến cho đã tay”. Hiện tại cách tốt nhất và nhanh nhất để tạo custom node là dùng CLI chính thức của n8n Package @n8n/node-cli là CLI mới nhất (ra mắt đầu 2025), nhưng lệnh tạo project là npm create @n8n/node (không phải n8n-node@latest). Nếu bạn chạy lệnh cũ, npm sẽ báo “not found”.
Nếu chưa có npm cache, chạy npm cache clean –force trước.
Cách 1: Dùng CLI chính thức (khuyến nghị mạnh mẽ 2025)
Đây là cách n8n chính thức khuyến nghị cho cả custom cá nhân lẫn community node.
Chạy lệnh này trong terminal:
npm create @n8n/node@latest n8n-nodes-ai-agent
CLI sẽ hỏi bạn một loạt câu:
Package name (ví dụ: n8n-nodes-lazada, @toanmin/n8n-nodes-kiotviet, …)
Display name của node (ví dụ: KiotViet, Lazada API, …)
Description
Author name + email
Chọn style: Declarative style (khuyến nghị 99% trường hợp) hoặc Programmatic style
Có cần credentials không? (nếu API cần API key, OAuth thì chọn Yes)
Có cần icon không?
Sau ~30 giây là xong, bạn sẽ có một project cực kỳ sạch với cấu trúc chuẩn.
Các lệnh quan trọng trong project:
npm install # lần đầu
npm run build # build node
npm run dev # CHẠY CÁI NÀY ĐỂ TEST – nó sẽ tự động khởi động n8n instance có node của bạn luôn!
npm run lint # check lỗi chuẩn n8n
npm publish # khi sẵn sàng đăng lên npm (nên publish scoped @yourname/n8n-nodes-abc)
Node của bạn sẽ nằm trong thư mục nodes/TenNode/TenNode.node.ts
Ví dụ node siêu đơn giản (Declarative style) – lấy random quote:
import { INodeType, INodeTypeDescription, IExecuteFunctions } from ‘n8n-workflow’;
export class RandomQuote implements INodeType {
description: INodeTypeDescription = {
displayName: ‘Random Quote’,
name: ‘randomQuote’,
icon: ‘fa:quote-left’,
group: [‘output’],
version: 1,
description: ‘Lấy một câu quote ngẫu nhiên’,
defaults: { name: ‘Random Quote’ },
inputs: [‘main’],
outputs: [‘main’],
properties: [
{
displayName: ‘Category’,
name: ‘category’,
type: ‘options’,
options: [
{ name: ‘Inspire’, value: ‘inspire’ },
{ name: ‘Funny’, value: ‘funny’ },
{ name: ‘Love’, value: ‘love’ },
],
default: ‘inspire’,
},
],
};
async execute(this: IExecuteFunctions) {
const category = this.getNodeParameter(‘category’, 0) as string;
const response = await this.helpers.httpRequest({
method: ‘GET’,
url: `//api.quotable.io/random?tags=${category}`,
json: true,
});
const items = this.getInputData();
return this.prepareOutputItems([
{
json: {
quote: response.content,
author: response.author,
category,
},
},
]);
}
Build lại → vào n8n (do npm run dev chạy) → search “Random Quote” → có ngay!
Cách 2: Dùng starter cũ (nếu bạn thích cấu trúc mono-repo nhiều node)
git clone //github.com/n8n-io/n8n-nodes-starter.git n8n-nodes-ai-agent
cd n8n-nodes-ai-agent
npm install
# Sau đó copy code vào nodes/YourNode/YourNode.node.ts
Check version: npm –version (cần >=10). Update npm nếu cần: npm install -g npm@latest.
Sau đó copy folder packages/nodes-base/nodes/Example thành MySuperNode rồi sửa file .node.ts
Cách test:
Cách dễ nhất: N8N_CUSTOM_EXTENSIONS=/duong/dan/den/my-nodes/dist n8n start
Hoặc load custom node vào n8n thực tế. Thêm vào .env:
environment: – N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom
Declarative vs Programmatic – chọn cái nào?
- Declarative style (khuyến nghị 2025): UI đẹp, dropdown Resource + Operation, ít code execute, dễ maintain dễ, 90% node hiện đại dùng kiểu này (Gmail, Google Sheets, HTTP Request mới đều là declarative).
- Programmatic style: Linh hoạt tuyệt đối, logic phức tạp, nhiều if/else, loop trong execute → dùng khi declarative không đủ mạnh (ví dụ xử lý pagination phức tạp, multi-page, retry logic đặc biệt…).
Ví dụ: Hướng dẫn tạo node AI Agent (Multi Fallback). Cho phép nhập số Model, từ đó hiện ra các add chat model để người dùng add bất cứ model vào.
vào thư mục custom
Bước 1: Tạo node bằng CLI
npm create @n8n/node@latest n8n-nodes-ai-agent-multi-fallback
CLI hỏi:
- Package name: n8n-nodes-ai-agent-multi-fallback
- Display name: AI Agent Multi Fallback
- Description: AI Agent với nhiều LLM fallback theo thứ tự (primary → fallback 1 → 2 → 3…)
- Author: tên bạn
- Declarative style → Yes
- Credentials → No (vì mình dùng API key string cho tối đa tương thích)
Xong → cd vào thư mục → npm install
cd /home/user/customn8n/n8n-nodes-ai-agent-multi-fallback
npm install
TypeScript bắt buộc khi dùng moduleResolution: “Node16” thì module cũng phải là Node16 hoặc NodeNext. n8n CLI mặc định dùng module: “commonjs” → xung đột.
BƯỚC 1: SỬA tsconfig.json (THAY TOÀN BỘ)
{
“compilerOptions”: {
“target”: “es2020”,
“module”: “Node16”,
“moduleResolution”: “Node16”,
“esModuleInterop”: true,
“allowSyntheticDefaultImports”: true,
“strict”: true,
“skipLibCheck”: true,
“forceConsistentCasingInFileNames”: true,
“outDir”: “./dist”,
“rootDir”: “./”,
“resolveJsonModule”: true,
“declaration”: true,
“declarationMap”: true,
“sourceMap”: true
},
“include”: [“nodes/**/*”, “credentials/**/*”],
“exclude”: [“node_modules”, “dist”]
}
Clean + Reinstall (nếu cần):
rm -rf node_modules package-lock.json dist
npm install
npm install @langchain/openai @langchain/anthropic @langchain/google-genai @langchain/core
Bước 2: File chính là nodes/AiAgentMultiFallback/AiAgentMultiFallback.node.ts
import {
INodeType,
INodeTypeDescription,
IExecuteFunctions,
IDataObject,
INodeExecutionData,
} from ‘n8n-workflow’;
import { ChatOpenAI } from ‘@langchain/openai’;
import { ChatAnthropic } from ‘@langchain/anthropic’;
import { ChatGoogleGenerativeAI } from ‘@langchain/google-genai’;
import { HumanMessage, SystemMessage } from ‘@langchain/core/messages’;
import type { BaseChatModel } from ‘@langchain/core/language_models/chat_models’;
export class AiAgentMultiFallback implements INodeType {
description: INodeTypeDescription = {
displayName: ‘AI Agent Multi Fallback’,
name: ‘aiAgentMultiFallback’,
icon: ‘fa:robot’,
group: [‘transform’],
version: 1,
description: ‘AI Agent gọi nhiều model theo thứ tự fallback (max 20). Dùng “Add Model” để thêm.’,
defaults: { name: ‘AI Agent Multi Fallback’, color: ‘#00C4B4’ },
inputs: [‘main’],
outputs: [‘main’],
properties: [
{
displayName: ‘Task / Prompt’,
name: ‘task’,
type: ‘string’,
typeOptions: { rows: 4 },
default: ”,
placeholder: ‘Ví dụ: Tóm tắt tin tức mới nhất về AI tại Việt Nam’,
},
{
displayName: ‘System Prompt (tùy chọn)’,
name: ‘systemPrompt’,
type: ‘string’,
typeOptions: { rows: 3 },
default: ‘Bạn là một trợ lý AI thông minh và chính xác.’,
},
{
displayName: ‘Models (thêm tối đa 20)’,
name: ‘models’,
type: ‘fixedCollection’,
typeOptions: {
multipleValues: true,
sortable: true,
maxValueCount: 20,
},
placeholder: ‘Add Model’,
default: {},
options: [
{
name: ‘modelConfig’,
displayName: ‘Model’,
values: [
{
displayName: ‘Provider’,
name: ‘provider’,
type: ‘options’,
options: [
{ name: ‘OpenAI / Grok / Groq / Ollama / Local’, value: ‘openai’ },
{ name: ‘Anthropic (Claude)’, value: ‘anthropic’ },
{ name: ‘Google Gemini’, value: ‘gemini’ },
],
default: ‘openai’,
},
{
displayName: ‘Model Name’,
name: ‘modelName’,
type: ‘string’,
default: ‘gpt-4o-mini’,
},
{
displayName: ‘Base URL (Ollama, Grok, Local)’,
name: ‘baseUrl’,
type: ‘string’,
default: ”,
displayOptions: { show: { provider: [‘openai’] } },
},
{
displayName: ‘API Key’,
name: ‘apiKey’,
type: ‘string’,
typeOptions: { password: true },
default: ”,
},
{
displayName: ‘Temperature’,
name: ‘temperature’,
type: ‘number’,
default: 0.7,
typeOptions: { minValue: 0, maxValue: 2 },
},
{
displayName: ‘Max Tokens’,
name: ‘maxTokens’,
type: ‘number’,
default: 2048,
},
],
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
// Helper function ĐƯỢC ĐỊNH NGHĨA TRONG execute → KHÔNG DÙNG this.method
const createLlm = (config: IDataObject): BaseChatModel => {
const provider = config.provider as string;
const modelName = config.modelName as string;
const apiKey = config.apiKey as string || undefined;
const baseUrl = config.baseUrl as string || undefined;
const temperature = config.temperature as number;
const maxTokens = config.maxTokens as number | undefined;
if (provider === ‘openai’) {
return new ChatOpenAI({
model: modelName,
temperature,
maxTokens,
apiKey: apiKey,
configuration: { baseURL: baseUrl },
});
}
if (provider === ‘anthropic’) {
return new ChatAnthropic({
model: modelName,
temperature,
maxTokens,
apiKey: apiKey,
});
}
if (provider === ‘gemini’) {
return new ChatGoogleGenerativeAI({
model: modelName.includes(‘gemini’) ? modelName : `models/${modelName}`,
temperature,
maxOutputTokens: maxTokens,
apiKey: apiKey,
});
}
throw new Error(`Provider không hỗ trợ: ${provider}`);
};
for (let i = 0; i < items.length; i++) {
const rawTask = this.getNodeParameter(‘task’, i, ”) as string;
const task = rawTask || (items[i].json.task as string) || (items[i].json.prompt as string) || ”;
if (!task || typeof task !== ‘string’) continue;
const systemPrompt = this.getNodeParameter(‘systemPrompt’, i) as string;
const modelConfigs = this.getNodeParameter(‘models.modelConfig’, i, []) as IDataObject[];
if (modelConfigs.length === 0) throw new Error(‘Phải có ít nhất 1 model’);
let response = ”;
let usedModel = ”;
let lastError = ”;
for (const config of modelConfigs) {
try {
const llm = createLlm(config); // Fix: Gọi trực tiếp hàm trong execute
const messages = [
new SystemMessage(systemPrompt),
new HumanMessage(task),
];
const result = await llm.invoke(messages);
response = result.content.toString();
usedModel = config.modelName as string;
break;
} catch (err: any) {
lastError = err.message;
continue;
}
}
if (!response) {
throw new Error(`Tất cả model đều lỗi. Lỗi cuối: ${lastError}`);
}
returnData.push({
json: {
response,
modelUsed: usedModel,
fallbackCount: modelConfigs.length,
},
pairedItem: { item: i },
});
}
return [returnData];
}
}
BƯỚC 2: Build: npm run build
KẾT QUẢ: Building TypeScript files…
Built successfully!
BƯỚC 3: Chạy dev: npm run dev
Chạy hoàn toàn mới Initializing n8n process
n8n ready on 0.0.0.0, port 5678
Vậy nên stop cái đang chạy.
# Tìm process đang dùng port 5678
lsof -i :5678
# Hoặc
netstat -tulnp | grep 5678
Kết quả ví dụ:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 12345 minhtoan 12u IPv4 123456 0t0 TCP *:5678 (LISTEN)
Ghi lại PID (ví dụ: 12345) để GIẾT PROCESS
kill -9 12345
Cách khác
nano ~/ecosystem.config.js
env: {
// QUAN TRỌNG: Load custom node từ dist N8N_CUSTOM_EXTENSIONS: ‘/home/minhtoan/customn8n/n8n-nodes-ai-agent-multi-fallback/dist’, // Tắt tunnel (dev mode) N8N_DISABLE_PRODUCTION_MAIN_PROCESS: ‘false’,
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS:’false’, //Fix cảnh báo permissions
},
pm2 start ~/n8n-pm2-ecosystem.config.js
Kiểm tra log + status:
# Xem log realtime
pm2 logs n8n-custom-node
# Xem trạng thái
pm2 status
Test trong n8n UI
Mở: //your-server-ip:5678
Tạo workflow:textManual Trigger → AI Agent Multi Fallback
Node hiện ra → Add Model → Run → HOẠT ĐỘNG BÌNH THƯỜNG
Lưu PM2 khi reboot: pm2 save
Cách dùng cực ngon
- npm run build
- npm run dev → n8n chạy local với node của bạn
- Tạo workflow: Trigger → AI Agent Multi Fallback
- Add Model → Grok (base URL //api.x.ai/v1) → Claude → Gemini → Ollama (base URL //localhost:11434/v1, apiKey để trống)
- Input có field prompt là xong → nó sẽ thử lần lượt đến khi có kết quả hoặc hết model thì error.
cd~/customn8n/n8n-nodes-ai-agent-multi-fallback
rm ./nodes/AiAgentMultiFallback/AiAgentMultiFallback.node.ts
nano ./nodes/AiAgentMultiFallback/AiAgentMultiFallback.node.ts
import {
INodeType,
INodeTypeDescription,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from ‘n8n-workflow’;
import { ChatOpenAI } from ‘@langchain/openai’;
import { ChatAnthropic } from ‘@langchain/anthropic’;
import { ChatGoogleGenerativeAI } from ‘@langchain/google-genai’;
import { HumanMessage, SystemMessage } from ‘@langchain/core/messages’;
import type { BaseChatModel } from ‘@langchain/core/language_models/chat_models’;
export class AiAgentMultiFallback implements INodeType {
description:INodeTypeDescription= {
displayName:’AI Agent Multi Fallback (Số lượng Model)’,
name:’aiAgentMultiFallback’,
icon:’fa:robot’,
group: [‘transform’],
version:2,
description:’AI Agent với số lượng Fallback Model tùy chỉnh (1–20). Nhập số → hiện field.’,
defaults: { name:’AI Agent Multi Fallback’, color:’#00C4B4′ },
inputs: [‘main’],
outputs: [‘main’],
properties: [
{
displayName:’Task / Prompt’,
name:’task’,
type:’string’,
typeOptions: { rows:4 },
default:”,
placeholder:’Ví dụ: Tóm tắt tin tức mới nhất về AI tại Việt Nam’,
},
{
displayName:’System Prompt (tùy chọn)’,
name:’systemPrompt’,
type:’string’,
typeOptions: { rows:3 },
default:’Bạn là một trợ lý AI thông minh và chính xác.’,
},
{
displayName:’Số lượng Fallback Model (1–20)’,
name:’modelCount’,
type:’number’,
default:1,
typeOptions: {
minValue:1,
maxValue:20,
},
description:’Nhập số → hiển thị đúng số model để cấu hình’,
},
// FIX: Dùng INodeProperties và ép kiểu đúng
] as INodeProperties[],
// Thêm dynamic properties sau (vì không thể dùng spread trong properties)
};
// Hàm tạo dynamic properties
privategetDynamicModelProperties():INodeProperties[] {
constproperties:INodeProperties[] = [];
for (leti=1; i<=20; i++) {
properties.push({
displayName:`Model ${i}`,
name:`model_${i}`,
type:’collection’,
displayOptions: {
show: {
modelCount:Array.from({ length:i }, (_, j) =>j+1),
},
},
default: {},
placeholder:`Cấu hình Model ${i}`,
options: [
{
displayName:’Provider’,
name:’provider’,
type:’options’,
options: [
{ name: ‘OpenAI / Grok / Groq / Ollama’, value: ‘openai’ },
{ name: ‘Anthropic (Claude)’, value: ‘anthropic’ },
{ name: ‘Google Gemini’, value: ‘gemini’ },
],
default:’openai’,
},
{
displayName:’Model Name’,
name:’modelName’,
type:’string’,
default:’gpt-4o-mini’,
},
{
displayName:’Base URL (Ollama, Grok, Local)’,
name:’baseUrl’,
type:’string’,
default:”,
displayOptions: { show: { provider: [‘openai’] } },
},
{
displayName:’API Key’,
name:’apiKey’,
type:’string’,
typeOptions: { password:true },
default:”,
},
{
displayName:’Temperature’,
name:’temperature’,
type:’number’,
default:0.7,
typeOptions: { minValue:0, maxValue:2 },
},
{
displayName:’Max Tokens’,
name:’maxTokens’,
type:’number’,
default:2048,
},
],
});
}
returnproperties;
}
// Gộp properties trong constructor hoặc init
constructor() {
this.description.properties= [
…this.description.properties,
…this.getDynamicModelProperties(),
];
}
asyncexecute(this:IExecuteFunctions):Promise<INodeExecutionData[][]> {
constitems=this.getInputData();
constreturnData:INodeExecutionData[] = [];
constcreateLlm= (config:any):BaseChatModel=> {
constprovider=config.providerasstring;
constmodelName=config.modelNameasstring||’gpt-4o-mini’;
constapiKey=config.apiKeyasstring||undefined;
constbaseUrl=config.baseUrlasstring||undefined;
consttemperature=Number(config.temperature) ||0.7;
constmaxTokens=config.maxTokens?Number(config.maxTokens) :undefined;
if (provider===’openai’) {
returnnewChatOpenAI({
model:modelName,
temperature,
maxTokens,
apiKey,
configuration:baseUrl? { baseURL:baseUrl } :undefined,
});
}
if (provider===’anthropic’) {
returnnewChatAnthropic({
model:modelName,
temperature,
maxTokens,
apiKey,
});
}
if (provider===’gemini’) {
returnnewChatGoogleGenerativeAI({
model:modelName.includes(‘gemini’) ?modelName:`models/${modelName}`,
temperature,
maxOutputTokens:maxTokens,
apiKey,
});
}
thrownewError(`Provider không hỗ trợ: ${provider}`);
};
for (const [i, item] ofitems.entries()) {
// === FIX TASK (an toàn với mọi kiểu dữ liệu) ===
lettask=”;
try {
constparamTask=this.getNodeParameter(‘task’, i, ”) asany;
if (typeofparamTask===’string’&¶mTask.trim()) {
task=paramTask.trim();
}
} catch { }
if (!task&&item.json) {
task= (item.json.task??item.json.prompt??item.json.message??item.json.text??”) asstring;
}
if (!task) {
returnData.push({ json: { error:’Không có task/prompt’ }, pairedItem: { item:i } });
continue;
}
// === FIX SYSTEM PROMPT (không còn lỗi {} → string) ===
letsystemPrompt=’Bạn là một trợ lý AI thông minh và chính xác.’;
try {
constparam=this.getNodeParameter(‘systemPrompt’, i, ”) asany;
if (typeofparam===’string’&¶m.trim() !==”) {
systemPrompt=param.trim();
}
} catch {
// Dùng mặc định
}
// === LẤY MODEL COUNT ===
letmodelCount=1;
try {
constcount=this.getNodeParameter(‘modelCount’, i, 1) asany;
if (typeofcount===’number’&&count>=1&&count<=20) {
modelCount=count;
}
} catch { }
// === LẤY TẤT CẢ MODEL ===
constmodelConfigs:any[] = [];
for (letm=1; m<=modelCount&&m<=20; m++) {
try {
constconfig=this.getNodeParameter(`model_${m}`, i, null) asany;
if (config&&config.provider&&config.modelName) {
modelConfigs.push(config);
}
} catch { }
}
if (modelConfigs.length===0) {
returnData.push({ json: { error:’Chưa cấu hình model nào’ }, pairedItem: { item:i } });
continue;
}
// === GỌI MODEL THEO THỨ TỰ FALLBACK ===
letresponse=”;
letusedModel=”;
for (constconfigofmodelConfigs) {
try {
constllm=createLlm(config);
constresult=awaitllm.invoke([
newSystemMessage(systemPrompt),
newHumanMessage(task),
]);
response=typeofresult.content===’string’
?result.content
:Array.isArray(result.content)
?result.content.map(c=> (casany).text||”).join(”)
:JSON.stringify(result.content);
usedModel=config.modelName;
break;
} catch (err: any) {
continue;
}
}
if (!response) {
response=’Tất cả model đều lỗi. Vui lòng kiểm tra API key và kết nối.’;
}
returnData.push({
json: {
response,
modelUsed:usedModel||null,
fallbackCount:modelConfigs.length,
success:!!usedModel,
},
pairedItem: { item:i },
});
}
return [returnData];
}
}
npm run build
CẬP NHẬT package.json (để publish npm sau này)
{
“name”: “n8n-nodes-ai-agent-multi-fallback”,
“version”: “1.0.0”,
“description”: “AI Agent với kéo thả model fallback (max 20)”,
“keywords”: [“n8n”, “ai”, “fallback”, “drag-drop”],
“n8n”: {
“n8nNodesApiVersion”: 1,
“nodes”: [
“dist/nodes/AiAgentMultiFallback/AiAgentMultiFallback.node.js”
]
}
}
Sau này: npm publish → ai cũng cài được!
Thay vì Enable Fallback → Bây giờ có ô nhập số: Số lượng Fallback Model (1–20)
→ Tự động hiện đúng số field để nhập model
→ Kéo thả thay đổi thứ tự fallback
→ Không cần “Add Model” – nhập số là xong!
# Bước 1: Thoát n8n dev đúng cách (quan trọng nhất) Ctrl + C ← Nhấn 1 lần → thấy “Terminate batch job (Y/N)?” y ← Gõ y rồi Enter
# Bước 2: Giết sạch mọi process n8n còn sót (đảm bảo port 5678 sạch) killall node ← hoặc dùng lệnh mạnh hơn dưới đây nếu vẫn còn
# Cách mạnh nhất (khuyên dùng) pkill -f n8n pkill -f “node.*n8n”
# 1. Fix lỗi Permissions 0664 (chỉ chạy 1 lần là hết mãi mãi) mkdir -p ~/.n8n-node-cli/.n8n touch ~/.n8n-node-cli/.n8n/config chmod 600 ~/.n8n-node-cli/.n8n/config
# 2. Tạo alias để chạy dev sạch sẽ nhất (dán vào ~/.bashrc hoặc ~/.zshrc) echo “alias n8ndev=’N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true npm run dev — –tunnel'” >> ~/.bashrc source ~/.bashrc # hoặc source ~/.zshrc
Bài viết liên quan: