n8n + Custom Code

Learn how to integrate custom code with n8n workflows

วิธีการผสมผสาน n8n กับ custom code เพื่อสร้างระบบอัตโนมัติที่ทรงพลังและยืดหยุ่น

Integration Patterns

Pattern 1: Function Node (Simple)

n8n Workflow:
Trigger → Function Node → Action Node

Custom Code:
├─ Data transformation
├─ Validation
├─ Simple calculations
└─ Thai text processing

Pattern 2: HTTP Request Node (External API)

n8n Workflow:
Trigger → HTTP Request → Process Response → Action

Custom Code:
├─ Express.js API
├─ Custom business logic
├─ Database operations
└─ Complex calculations

Pattern 3: Webhook Response (Two-way)

n8n Workflow:
Webhook → Custom API → Response to Webhook

Custom Code:
├─ Real-time processing
├─ Complex workflows
├─ Database transactions
└─ External integrations

Example 1: LINE Chatbot with Thai NLP

n8n Workflow Structure

LINE Webhook → Function (Thai NLP) → IF Node → Function (Response) → LINE Reply

Function Node 1: Thai Intent Detection

// Thai Intent Detection Function
const thaiKeywords = {
  greeting: ['สวัสดี', 'ดี', 'หวัดดี', 'เฮลโล', 'ดีครับ', 'ดีคะ'],
  price: ['ราคา', 'เท่าไหร่', 'กี่บาท', 'ค่าใช้จ่าย', 'บริการ'],
  appointment: ['นัด', 'ว่าง', 'เวลา', 'วัน', 'จอง', 'นัดหมาย', 'ตาราง'],
  contact: ['ติดต่อ', 'โทร', 'อีเมล', 'ที่อยู่', 'เบอร์', 'สาขา'],
  service: ['บริการ', 'ทำ', 'รักษา', 'ตรวจ', 'consult', 'ปรึกษา'],
  emergency: ['ฉุกเฉิน', 'ปวด', 'เจ็บ', 'ฉีด', ' accident', 'บาดเจ็บ']
};

function detectThaiIntent(message) {
  const normalizedMessage = message.toLowerCase().trim();
  let detectedIntent = 'unknown';
  let confidence = 0;
  
  for (const [intent, keywords] of Object.entries(thaiKeywords)) {
    const matches = keywords.filter(keyword => 
      normalizedMessage.includes(keyword)
    ).length;
    
    if (matches > confidence) {
      confidence = matches;
      detectedIntent = intent;
    }
  }
  
  // Extract entities
  const entities = {};
  
  // Phone number extraction
  const phoneMatch = normalizedMessage.match(/0[689]\d{8}/);
  if (phoneMatch) entities.phone = phoneMatch[0];
  
  // Date extraction (Thai format)
  const dateMatch = normalizedMessage.match(/(\d{1,2})[\/\-](\d{1,2})/);
  if (dateMatch) entities.date = dateMatch[0];
  
  // Time extraction
  const timeMatch = normalizedMessage.match(/(\d{1,2})[:.]?(\d{2})/);
  if (timeMatch) entities.time = timeMatch[0];
  
  return {
    intent: detectedIntent,
    confidence: confidence / 3, // Normalize to 0-1
    entities,
    originalMessage: message
  };
}

// Process incoming LINE messages
const events = $input.all()[0].json.events || [];
const processedMessages = [];

for (const event of events) {
  if (event.type === 'message' && event.message.type === 'text') {
    const result = detectThaiIntent(event.message.text);
    processedMessages.push({
      replyToken: event.replyToken,
      userId: event.source.userId,
      intent: result.intent,
      confidence: result.confidence,
      entities: result.entities,
      message: event.message.text
    });
  }
}

return processedMessages.map(msg => ({ json: msg }));

Function Node 2: Response Generation

// Response Generation Function
function generateResponse(intent, entities, confidence) {
  const responses = {
    greeting: {
      text: 'สวัสดีครับ! 🙏\nยินดีต้อนรับสู่ ShantiLink\nมีอะไรให้ช่วยไหมครับ?',
      quickReply: [
        { type: 'text', text: 'สอบถามราคา' },
        { type: 'text', text: 'นัดหมาย' },
        { type: 'text', text: 'ติดต่อ' }
      ]
    },
    price: {
      text: '📋 ราคาบริการของเรา:\n\n💻 ปรึกษาระบบ: 1,500 บาท/ชั่วโมง\n🤖 ทำ Chatbot: 5,000 บาทขึ้นไป\n📱 LINE LIFF: 8,000 บาทขึ้นไป\n🔧 ระบบอัตโนมัติ: 15,000 บาทขึ้นไป\n\nสอบถามรายละเอียดได้ครับ',
      quickReply: [
        { type: 'text', text: 'นัดหมายปรึกษา' },
        { type: 'text', text: 'ดูผลงาน' }
      ]
    },
    appointment: {
      text: `📅 นัดหมายได้ที่:\n\n🕐 จันทร์-ศุกร์: 9:00-18:00\n📍 กรุงเทพฯ (ออนไลน์ก็ได้)\n\n${entities.date ? `วันที่สนใจ: ${entities.date}\n` : ''}${entities.time ? `เวลา: ${entities.time}\n` : ''}กรุณาระบุ:\n1. ชื่อ-นามสกุล\n2. เบอร์โทรศัพท์\n3. ประเภทที่ปรึกษา`,
      quickReply: [
        { type: 'text', text: 'ปรึกษาระบบ' },
        { type: 'text', text: 'ทำเว็บไซต์' },
        { type: 'text', text: 'ทำแอปพลิเคชัน' }
      ]
    },
    contact: {
      text: '📞 ติดต่อ ShantiLink:\n\n📱 โทร: 081-234-5678\n📧 อีเมล: info@shantilink.com\n🌐 เว็บ: shantilink.com\n💬 LINE: @shantilink\n\nติดต่อได้ 24 ชั่วโมงครับ',
      quickReply: [
        { type: 'text', text: 'ปรึกษาฟรี' },
        { type: 'text', text: 'ดูโปรเจกต์' }
      ]
    },
    service: {
      text: '🛠️ บริการของเรา:\n\n✅ ระบบอัตโนมัติ (n8n)\n✅ Chatbot LINE/Facebook\n✅ เว็บไซต์และแอป\n✅ LINE LIFF Application\n✅ ระบบจอง-นัดหมาย\n✅ Dashboard รายงาน\n\nทุกระบบทำงานได้จริงครับ',
      quickReply: [
        { type: 'text', text: 'ดูผลงาน' },
        { type: 'text', text: 'ปรึกษาฟรี' }
      ]
    },
    emergency: {
      text: '⚠️ กรณีฉุกเฉิน:\n\n🚨 หากเป็นเรื่องแพทย์ฉุกเฉิน\nโทร: 1669 (ฉุกเฉินการแพทย์)\n\n🏥 หากต้องการคลินิกใกล้เคียง\nแจ้งที่อยู่ปัจจุบันได้ครับ\n\n📞 ติดต่อเรา: 081-234-5678',
      quickReply: [
        { type: 'text', text: 'แจ้งที่อยู่' },
        { type: 'text', text: 'โทรหาคลินิก' }
      ]
    },
    unknown: {
      text: '😊 ขอโทษครับ ยังไม่เข้าใจ\n\nลองพิมพ์:\n• "สวัสดี" - เริ่มต้น\n• "ราคา" - ดูบริการ\n• "นัด" - จองคิว\n• "ติดต่อ" - หาเรา\n\nหรือพิมพ์คำถามได้เลยครับ',
      quickReply: [
        { type: 'text', text: 'สวัสดี' },
        { type: 'text', text: 'ราคา' },
        { type: 'text', text: 'นัดหมาย' }
      ]
    }
  };
  
  return responses[intent] || responses.unknown;
}

// Generate response for each message
const responses = [];

for (const message of $input.all()) {
  const data = message.json;
  const response = generateResponse(data.intent, data.entities, data.confidence);
  
  responses.push({
    replyToken: data.replyToken,
    messages: [
      {
        type: 'text',
        text: response.text,
        quickReply: {
          items: response.quickReply.map(reply => ({
            type: 'action',
            action: {
              type: 'message',
              label: reply.text,
              text: reply.text
            }
          }))
        }
      }
    ]
  });
}

return responses.map(resp => ({ json: resp }));

Example 2: Custom API for Order Processing

Express.js API Server

// server.js
const express = require('express');
const { createClient } = require('@supabase/supabase-js');
const nodemailer = require('nodemailer');

const app = express();
app.use(express.json());

// Initialize Supabase
const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY
);

// Initialize email transporter
const transporter = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS
  }
});

// Order processing endpoint
app.post('/api/orders', async (req, res) => {
  try {
    const { customerId, items, totalAmount, deliveryAddress } = req.body;
    
    // Validate order
    if (!customerId || !items || items.length === 0) {
      return res.status(400).json({
        success: false,
        error: 'Invalid order data'
      });
    }
    
    // Generate order ID
    const orderId = `ORD${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`;
    
    // Save to database
    const { data: order, error } = await supabase
      .from('orders')
      .insert({
        order_id: orderId,
        customer_id: customerId,
        items: items,
        total_amount: totalAmount,
        delivery_address: deliveryAddress,
        status: 'pending',
        created_at: new Date().toISOString()
      })
      .select()
      .single();
    
    if (error) throw error;
    
    // Update inventory
    for (const item of items) {
      await supabase
        .from('products')
        .update({ stock: supabase.rpc('decrement', { amount: item.quantity }) })
        .eq('product_id', item.productId);
    }
    
    // Send confirmation email
    await transporter.sendMail({
      to: customerEmail,
      subject: `Order Confirmation - ${orderId}`,
      html: `
        <h2>ยืนยันการสั่งซื้อ</h2>
        <p>เลขที่สั่งซื้อ: ${orderId}</p>
        <p>วันที่: ${new Date().toLocaleDateString('th-TH')}</p>
        <h3>รายการสินค้า:</h3>
        <ul>
          ${items.map(item => `
            <li>${item.name} x ${item.quantity} = ${item.price * item.quantity} บาท</li>
          `).join('')}
        </ul>
        <h3>รวม: ${totalAmount} บาท</h3>
        <p>ที่อยู่จัดส่ง: ${deliveryAddress}</p>
      `
    });
    
    // Trigger n8n workflow (if needed)
    await fetch('https://your-n8n-instance.com/webhook/order-processed', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ orderId, status: 'confirmed' })
    });
    
    res.json({
      success: true,
      orderId,
      message: 'Order processed successfully'
    });
    
  } catch (error) {
    console.error('Order processing error:', error);
    res.status(500).json({
      success: false,
      error: 'Internal server error'
    });
  }
});

// Order status endpoint
app.get('/api/orders/:orderId', async (req, res) => {
  try {
    const { orderId } = req.params;
    
    const { data: order, error } = await supabase
      .from('orders')
      .select('*')
      .eq('order_id', orderId)
      .single();
    
    if (error || !order) {
      return res.status(404).json({
        success: false,
        error: 'Order not found'
      });
    }
    
    res.json({
      success: true,
      order: {
        orderId: order.order_id,
        status: order.status,
        totalAmount: order.total_amount,
        createdAt: order.created_at,
        estimatedDelivery: order.estimated_delivery
      }
    });
    
  } catch (error) {
    console.error('Order status error:', error);
    res.status(500).json({
      success: false,
      error: 'Internal server error'
    });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Order API server running on port ${PORT}`);
});

n8n Integration

n8n Workflow:
Order Webhook → HTTP Request (Custom API) → Function (Process Response) → Google Sheets → LINE Notification

Example 3: Data Processing Pipeline

Custom Data Processor

// data-processor.js
class SalesDataProcessor {
  constructor() {
    this.holidays = this.getThaiHolidays();
    this.workingHours = { start: 9, end: 18 };
  }
  
  processSalesData(rawData) {
    return rawData.map(record => this.processSingleRecord(record));
  }
  
  processSingleRecord(record) {
    return {
      ...record,
      processedAt: new Date().toISOString(),
      isWorkingHour: this.isWorkingHour(new Date(record.timestamp)),
      isHoliday: this.isHoliday(new Date(record.timestamp)),
      dayOfWeek: new Date(record.timestamp).toLocaleDateString('th-TH', { weekday: 'long' }),
      revenueCategory: this.categorizeRevenue(record.amount),
      customerSegment: this.segmentCustomer(record.customerId, record.amount)
    };
  }
  
  isWorkingHour(date) {
    const hour = date.getHours();
    const day = date.getDay();
    return day >= 1 && day <= 5 && hour >= this.workingHours.start && hour < this.workingHours.end;
  }
  
  isHoliday(date) {
    const dateStr = date.toISOString().split('T')[0];
    return this.holidays.includes(dateStr);
  }
  
  categorizeRevenue(amount) {
    if (amount < 1000) return 'low';
    if (amount < 5000) return 'medium';
    if (amount < 20000) return 'high';
    return 'premium';
  }
  
  segmentCustomer(customerId, amount) {
    // Simple segmentation based on purchase amount
    if (amount > 10000) return 'vip';
    if (amount > 5000) return 'regular';
    return 'new';
  }
  
  generateReport(processedData) {
    const report = {
      summary: {
        totalRevenue: processedData.reduce((sum, r) => sum + r.amount, 0),
        totalOrders: processedData.length,
        averageOrderValue: processedData.reduce((sum, r) => sum + r.amount, 0) / processedData.length,
        workingHourOrders: processedData.filter(r => r.isWorkingHour).length,
        holidayOrders: processedData.filter(r => r.isHoliday).length
      },
      byCategory: this.groupBy(processedData, 'revenueCategory'),
      bySegment: this.groupBy(processedData, 'customerSegment'),
      byDayOfWeek: this.groupBy(processedData, 'dayOfWeek')
    };
    
    return report;
  }
  
  groupBy(data, field) {
    return data.reduce((acc, record) => {
      const key = record[field];
      if (!acc[key]) {
        acc[key] = { count: 0, revenue: 0 };
      }
      acc[key].count++;
      acc[key].revenue += record.amount;
      return acc;
    }, {});
  }
  
  getThaiHolidays() {
    // Simplified Thai holidays list
    return [
      '2024-01-01', // New Year
      '2024-02-12', // Makha Bucha Day
      '2024-04-06', // Chakri Day
      '2024-04-13', // Songkran
      '2024-04-14', // Songkran
      '2024-04-15', // Songkran
      '2024-05-01', // Labor Day
      '2024-05-22', // Visakha Bucha Day
      // ... more holidays
    ];
  }
}

module.exports = SalesDataProcessor;

Best Practices

1. Error Handling

// Always wrap custom code in try-catch
try {
  // Your custom logic here
  const result = processData(inputData);
  return result;
} catch (error) {
  console.error('Custom code error:', error);
  // Return error response that n8n can handle
  return {
    error: true,
    message: error.message,
    timestamp: new Date().toISOString()
  };
}

2. Input Validation

function validateInput(data) {
  if (!data || typeof data !== 'object') {
    throw new Error('Invalid input data');
  }
  
  if (!data.message || typeof data.message !== 'string') {
    throw new Error('Message is required and must be a string');
  }
  
  if (data.message.length > 1000) {
    throw new Error('Message too long');
  }
  
  return true;
}

3. Performance Optimization

// Use async/await for I/O operations
async function processLargeDataset(data) {
  const batchSize = 100;
  const results = [];
  
  for (let i = 0; i < data.length; i += batchSize) {
    const batch = data.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(item => processItem(item))
    );
    results.push(...batchResults);
    
    // Prevent blocking
    if (i % (batchSize * 10) === 0) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
  
  return results;
}

Deployment Considerations

1. Environment Setup

# Package.json dependencies
{
  "dependencies": {
    "express": "^4.18.0",
    "@supabase/supabase-js": "^2.0.0",
    "nodemailer": "^6.9.0",
    "dotenv": "^16.0.0"
  },
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
}

2. Docker Configuration

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

3. Monitoring

// Add monitoring to your custom code
const monitoring = {
  logExecution: (functionName, duration, success) => {
    console.log({
      function: functionName,
      duration,
      success,
      timestamp: new Date().toISOString()
    });
  },
  
  logError: (functionName, error) => {
    console.error({
      function: functionName,
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString()
    });
  }
};

Next Steps

  • Cloud Services - การ deploy และจัดการระบบ
  • Client Examples - ดูตัวอย่างโปรเจกต์จริง
  • Supabase Basics - ฐานข้อมูลและ backend

ต้องการความช่วยเหลือ? ติดต่อเราได้ที่ ShantiLink.com 💬