n8nで実現する業務自動化事例集:実践的なワークフロー10選

はじめに

業務の自動化は、企業の生産性向上において最も重要な取り組みの一つです。n8n は、400以上のサービスと連携可能なオープンソースの自動化プラットフォームとして、複雑な業務フローを視覚的に設計・実行できます。本記事では、実際の業務で活用できる n8n のワークフロー事例を詳しく解説します。

n8n の基本概念

n8n とは

n8n(pronounced "n-eight-n")は、ノーコード・ローコードで業務自動化ワークフローを構築できるプラットフォームです。

主な特徴

  • セルフホスティング可能: データを完全に管理
  • 400+ 統合: 主要なサービスと連携
  • カスタムノード: JavaScript で拡張可能
  • 条件分岐: 複雑なロジックの実装

環境構築

Docker を使用したセットアップ

# docker-compose.yml
version: "3.8"

services:
  n8n:
    image: n8nio/n8n:latest
    restart: always
    ports:
      - "5678:5678"
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=admin
      - N8N_BASIC_AUTH_PASSWORD=your-secure-password
      - N8N_HOST=your-domain.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - WEBHOOK_URL=https://your-domain.com/
      - N8N_ENCRYPTION_KEY=your-encryption-key
    volumes:
      - n8n_data:/home/node/.n8n
      - ./custom-nodes:/home/node/.n8n/custom
    networks:
      - n8n-network

  postgres:
    image: postgres:14
    restart: always
    environment:
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=n8n-password
      - POSTGRES_DB=n8n
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - n8n-network

volumes:
  n8n_data:
  postgres_data:

networks:
  n8n-network:

実践的なワークフロー事例

1. 顧客問い合わせ自動対応システム

{
  "name": "顧客問い合わせ自動対応",
  "nodes": [
    {
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [250, 300],
      "parameters": {
        "labelIds": ["INBOX"],
        "filters": {
          "from": "*@customer-domain.com"
        }
      }
    },
    {
      "name": "Classify Email",
      "type": "n8n-nodes-base.openAi",
      "position": [450, 300],
      "parameters": {
        "resource": "chat",
        "model": "gpt-4",
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "メールを以下のカテゴリに分類してください:技術サポート、請求関連、一般問い合わせ、クレーム"
            },
            {
              "role": "user",
              "content": "={{$node['Gmail Trigger'].json['text']}}"
            }
          ]
        }
      }
    },
    {
      "name": "Router",
      "type": "n8n-nodes-base.switch",
      "position": [650, 300],
      "parameters": {
        "dataType": "string",
        "value1": "={{$node['Classify Email'].json['choices'][0]['message']['content']}}",
        "rules": {
          "rules": [
            {
              "value2": "技術サポート",
              "output": 0
            },
            {
              "value2": "請求関連",
              "output": 1
            },
            {
              "value2": "クレーム",
              "output": 2
            }
          ]
        }
      }
    }
  ]
}

2. 売上レポート自動生成・配信

// カスタム Function ノードのコード例
const salesData = items[0].json;
const moment = require('moment');

// 日次売上集計
const dailySales = salesData.reduce((acc, sale) => {
  const date = moment(sale.date).format('YYYY-MM-DD');
  if (!acc[date]) {
    acc[date] = {
      date: date,
      totalAmount: 0,
      orderCount: 0,
      products: {}
    };
  }
  
  acc[date].totalAmount += sale.amount;
  acc[date].orderCount += 1;
  
  // 商品別集計
  sale.items.forEach(item => {
    if (!acc[date].products[item.productId]) {
      acc[date].products[item.productId] = {
        name: item.productName,
        quantity: 0,
        revenue: 0
      };
    }
    acc[date].products[item.productId].quantity += item.quantity;
    acc[date].products[item.productId].revenue += item.price * item.quantity;
  });
  
  return acc;
}, {});

// レポート生成
const report = {
  period: {
    start: moment().subtract(1, 'day').format('YYYY-MM-DD'),
    end: moment().format('YYYY-MM-DD')
  },
  summary: {
    totalRevenue: Object.values(dailySales).reduce((sum, day) => sum + day.totalAmount, 0),
    totalOrders: Object.values(dailySales).reduce((sum, day) => sum + day.orderCount, 0),
    averageOrderValue: 0
  },
  dailyBreakdown: Object.values(dailySales),
  topProducts: []
};

report.summary.averageOrderValue = report.summary.totalRevenue / report.summary.totalOrders;

// トップ商品の抽出
const productTotals = {};
Object.values(dailySales).forEach(day => {
  Object.entries(day.products).forEach(([productId, data]) => {
    if (!productTotals[productId]) {
      productTotals[productId] = {
        name: data.name,
        totalQuantity: 0,
        totalRevenue: 0
      };
    }
    productTotals[productId].totalQuantity += data.quantity;
    productTotals[productId].totalRevenue += data.revenue;
  });
});

report.topProducts = Object.values(productTotals)
  .sort((a, b) => b.totalRevenue - a.totalRevenue)
  .slice(0, 10);

return [{
  json: report
}];

3. SNS 投稿自動化ワークフロー

// TypeScript での実装例
interface SocialMediaPost {
  content: string;
  platforms: ('twitter' | 'facebook' | 'linkedin' | 'instagram')[];
  mediaUrls?: string[];
  scheduledTime?: Date;
  hashtags?: string[];
}

class SocialMediaAutomation {
  private n8nWebhookUrl: string;
  
  constructor(webhookUrl: string) {
    this.n8nWebhookUrl = webhookUrl;
  }
  
  async schedulePost(post: SocialMediaPost): Promise<void> {
    // コンテンツの最適化
    const optimizedContent = this.optimizeForPlatforms(post);
    
    // n8n webhook トリガー
    const response = await fetch(this.n8nWebhookUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ...post,
        optimizedContent,
        timestamp: new Date().toISOString()
      })
    });
    
    if (!response.ok) {
      throw new Error(`Failed to schedule post: ${response.statusText}`);
    }
  }
  
  private optimizeForPlatforms(post: SocialMediaPost): Record<string, string> {
    const optimized: Record<string, string> = {};
    
    post.platforms.forEach(platform => {
      switch (platform) {
        case 'twitter':
          optimized.twitter = this.truncateForTwitter(post.content, post.hashtags);
          break;
        case 'linkedin':
          optimized.linkedin = this.formatForLinkedIn(post.content, post.hashtags);
          break;
        case 'facebook':
          optimized.facebook = post.content + '\n\n' + (post.hashtags?.join(' ') || '');
          break;
        case 'instagram':
          optimized.instagram = this.formatForInstagram(post.content, post.hashtags);
          break;
      }
    });
    
    return optimized;
  }
  
  private truncateForTwitter(content: string, hashtags?: string[]): string {
    const maxLength = 280;
    const hashtagString = hashtags ? '\n\n' + hashtags.join(' ') : '';
    const availableLength = maxLength - hashtagString.length;
    
    if (content.length > availableLength) {
      return content.substring(0, availableLength - 3) + '...' + hashtagString;
    }
    
    return content + hashtagString;
  }
  
  private formatForLinkedIn(content: string, hashtags?: string[]): string {
    // LinkedIn 用のフォーマット
    const formattedContent = content
      .split('\n')
      .map(line => line.trim())
      .filter(line => line.length > 0)
      .join('\n\n');
    
    return formattedContent + '\n\n' + (hashtags?.join(' ') || '');
  }
  
  private formatForInstagram(content: string, hashtags?: string[]): string {
    // Instagram は最初の部分のみ表示されるため、重要な情報を先頭に
    const maxPreviewLength = 125;
    const preview = content.substring(0, maxPreviewLength);
    const fullContent = content + '\n.\n.\n.\n' + (hashtags?.join(' ') || '');
    
    return fullContent;
  }
}

4. 在庫管理自動化システム

// n8n Function ノード: 在庫アラート
const inventoryData = items[0].json.inventory;
const thresholds = {
  critical: 10,
  low: 50,
  reorderPoint: 100
};

const alerts = [];
const reorders = [];

inventoryData.forEach(item => {
  const stockLevel = item.currentStock;
  const dailyUsage = item.averageDailyUsage || 0;
  const leadTime = item.leadTimeDays || 7;
  
  // 在庫レベルの評価
  if (stockLevel <= thresholds.critical) {
    alerts.push({
      level: 'CRITICAL',
      item: item,
      message: `緊急: ${item.name} の在庫が危機的レベル (${stockLevel}個) です`,
      daysRemaining: dailyUsage > 0 ? Math.floor(stockLevel / dailyUsage) : 'N/A'
    });
  } else if (stockLevel <= thresholds.low) {
    alerts.push({
      level: 'LOW',
      item: item,
      message: `警告: ${item.name} の在庫が少なくなっています (${stockLevel}個)`,
      daysRemaining: dailyUsage > 0 ? Math.floor(stockLevel / dailyUsage) : 'N/A'
    });
  }
  
  // 自動発注判定
  const reorderPoint = dailyUsage * leadTime * 1.5; // 安全係数1.5
  if (stockLevel <= reorderPoint && !item.onOrder) {
    const orderQuantity = Math.ceil(dailyUsage * 30); // 30日分を発注
    reorders.push({
      itemId: item.id,
      itemName: item.name,
      currentStock: stockLevel,
      orderQuantity: orderQuantity,
      supplier: item.preferredSupplier,
      estimatedCost: orderQuantity * item.unitCost
    });
  }
});

// Slack 通知用のメッセージ作成
const slackMessage = {
  text: '在庫管理レポート',
  attachments: []
};

if (alerts.length > 0) {
  const criticalAlerts = alerts.filter(a => a.level === 'CRITICAL');
  const lowAlerts = alerts.filter(a => a.level === 'LOW');
  
  if (criticalAlerts.length > 0) {
    slackMessage.attachments.push({
      color: 'danger',
      title: '🚨 緊急在庫アラート',
      fields: criticalAlerts.map(alert => ({
        title: alert.item.name,
        value: `在庫: ${alert.item.currentStock}個 | 残日数: ${alert.daysRemaining}日`,
        short: true
      }))
    });
  }
  
  if (lowAlerts.length > 0) {
    slackMessage.attachments.push({
      color: 'warning',
      title: '⚠️ 在庫低下警告',
      fields: lowAlerts.map(alert => ({
        title: alert.item.name,
        value: `在庫: ${alert.item.currentStock}個 | 残日数: ${alert.daysRemaining}日`,
        short: true
      }))
    });
  }
}

if (reorders.length > 0) {
  slackMessage.attachments.push({
    color: 'good',
    title: '📦 自動発注提案',
    fields: reorders.map(order => ({
      title: order.itemName,
      value: `発注数: ${order.orderQuantity}個 | 推定費用: ¥${order.estimatedCost.toLocaleString()}`,
      short: true
    }))
  });
}

return [
  {
    json: {
      alerts,
      reorders,
      slackMessage,
      timestamp: new Date().toISOString()
    }
  }
];

5. 人事評価プロセス自動化

// 360度評価の集計と分析
const evaluationWorkflow = {
  name: "360度評価自動集計",
  nodes: [
    {
      name: "Collect Evaluations",
      type: "n8n-nodes-base.googleSheets",
      parameters: {
        operation: "read",
        sheetId: "{{$env.EVALUATION_SHEET_ID}}",
        range: "A2:Z1000"
      }
    },
    {
      name: "Process Evaluations",
      type: "n8n-nodes-base.function",
      parameters: {
        functionCode: `
          const evaluations = items[0].json;
          const employeeScores = {};
          
          // 評価の集計
          evaluations.forEach(eval => {
            const employeeId = eval.employeeId;
            if (!employeeScores[employeeId]) {
              employeeScores[employeeId] = {
                name: eval.employeeName,
                department: eval.department,
                selfScore: 0,
                peerScores: [],
                managerScore: 0,
                subordinateScores: [],
                categories: {}
              };
            }
            
            // 評価カテゴリ別のスコア集計
            const categories = ['leadership', 'teamwork', 'technical', 'communication', 'innovation'];
            categories.forEach(category => {
              if (!employeeScores[employeeId].categories[category]) {
                employeeScores[employeeId].categories[category] = [];
              }
              employeeScores[employeeId].categories[category].push(eval[category]);
            });
            
            // 評価者のタイプ別に分類
            switch (eval.evaluatorType) {
              case 'self':
                employeeScores[employeeId].selfScore = eval.overallScore;
                break;
              case 'peer':
                employeeScores[employeeId].peerScores.push(eval.overallScore);
                break;
              case 'manager':
                employeeScores[employeeId].managerScore = eval.overallScore;
                break;
              case 'subordinate':
                employeeScores[employeeId].subordinateScores.push(eval.overallScore);
                break;
            }
          });
          
          // 最終スコアの計算
          const results = Object.entries(employeeScores).map(([id, data]) => {
            const avgPeerScore = data.peerScores.length > 0 
              ? data.peerScores.reduce((a, b) => a + b, 0) / data.peerScores.length 
              : 0;
            const avgSubordinateScore = data.subordinateScores.length > 0
              ? data.subordinateScores.reduce((a, b) => a + b, 0) / data.subordinateScores.length
              : 0;
            
            // 重み付け平均の計算
            const weights = {
              self: 0.1,
              peer: 0.3,
              manager: 0.4,
              subordinate: 0.2
            };
            
            const finalScore = 
              data.selfScore * weights.self +
              avgPeerScore * weights.peer +
              data.managerScore * weights.manager +
              avgSubordinateScore * weights.subordinate;
            
            // カテゴリ別平均の計算
            const categoryAverages = {};
            Object.entries(data.categories).forEach(([category, scores]) => {
              categoryAverages[category] = scores.reduce((a, b) => a + b, 0) / scores.length;
            });
            
            return {
              employeeId: id,
              name: data.name,
              department: data.department,
              finalScore: Math.round(finalScore * 100) / 100,
              selfScore: data.selfScore,
              peerScore: Math.round(avgPeerScore * 100) / 100,
              managerScore: data.managerScore,
              subordinateScore: Math.round(avgSubordinateScore * 100) / 100,
              categoryScores: categoryAverages,
              evaluationCount: {
                peers: data.peerScores.length,
                subordinates: data.subordinateScores.length
              }
            };
          });
          
          return results.map(result => ({ json: result }));
        `
      }
    }
  ]
};

6. 経費精算自動化フロー

# Python カスタムノード: 領収書OCR処理
import json
from datetime import datetime
from decimal import Decimal
import re

class ExpenseProcessor:
    def __init__(self):
        self.tax_rate = Decimal('0.10')  # 消費税率10%
        self.categories = {
            'タクシー': 'transportation',
            'JR': 'transportation',
            '食事': 'meal',
            'ホテル': 'accommodation',
            '会議': 'meeting',
            '備品': 'supplies'
        }
    
    def process_receipt(self, ocr_text: str, metadata: dict) -> dict:
        """OCRテキストから経費情報を抽出"""
        expense = {
            'id': metadata.get('receipt_id'),
            'employee_id': metadata.get('employee_id'),
            'date': self.extract_date(ocr_text) or datetime.now().isoformat(),
            'vendor': self.extract_vendor(ocr_text),
            'amount': self.extract_amount(ocr_text),
            'category': self.categorize_expense(ocr_text),
            'tax_amount': Decimal('0'),
            'description': '',
            'status': 'pending',
            'created_at': datetime.now().isoformat()
        }
        
        # 消費税の計算
        if expense['amount']:
            expense['tax_amount'] = round(expense['amount'] * self.tax_rate / (1 + self.tax_rate), 2)
        
        # 検証ルールの適用
        expense['validation'] = self.validate_expense(expense)
        
        return expense
    
    def extract_date(self, text: str) -> str:
        """日付の抽出"""
        date_patterns = [
            r'(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})日?',
            r'(\d{1,2})[月/-](\d{1,2})日?'
        ]
        
        for pattern in date_patterns:
            match = re.search(pattern, text)
            if match:
                if len(match.groups()) == 3:
                    return f"{match.group(1)}-{match.group(2).zfill(2)}-{match.group(3).zfill(2)}"
                else:
                    year = datetime.now().year
                    return f"{year}-{match.group(1).zfill(2)}-{match.group(2).zfill(2)}"
        
        return None
    
    def extract_amount(self, text: str) -> Decimal:
        """金額の抽出"""
        amount_patterns = [
            r'合計[::\s]*¥?([0-9,]+)',
            r'総額[::\s]*¥?([0-9,]+)',
            r'¥([0-9,]+)',
            r'([0-9,]+)円'
        ]
        
        for pattern in amount_patterns:
            match = re.search(pattern, text)
            if match:
                amount_str = match.group(1).replace(',', '')
                return Decimal(amount_str)
        
        return Decimal('0')
    
    def extract_vendor(self, text: str) -> str:
        """店舗名の抽出"""
        lines = text.split('\n')
        # 通常、最初の数行に店舗名が含まれる
        for line in lines[:5]:
            if len(line) > 3 and not re.match(r'^[\d\s\-:]+$', line):
                return line.strip()
        
        return '不明'
    
    def categorize_expense(self, text: str) -> str:
        """経費カテゴリの自動分類"""
        text_lower = text.lower()
        
        for keyword, category in self.categories.items():
            if keyword.lower() in text_lower:
                return category
        
        return 'other'
    
    def validate_expense(self, expense: dict) -> dict:
        """経費の妥当性検証"""
        validation = {
            'is_valid': True,
            'errors': [],
            'warnings': []
        }
        
        # 金額チェック
        if expense['amount'] <= 0:
            validation['is_valid'] = False
            validation['errors'].append('金額が無効です')
        elif expense['amount'] > 100000:
            validation['warnings'].append('高額経費のため、追加承認が必要です')
        
        # 日付チェック
        expense_date = datetime.fromisoformat(expense['date'])
        if expense_date > datetime.now():
            validation['is_valid'] = False
            validation['errors'].append('未来の日付は無効です')
        elif (datetime.now() - expense_date).days > 60:
            validation['warnings'].append('60日以上前の経費です')
        
        # カテゴリ別の上限チェック
        category_limits = {
            'meal': 5000,
            'transportation': 50000
        }
        
        if expense['category'] in category_limits:
            limit = category_limits[expense['category']]
            if expense['amount'] > limit:
                validation['warnings'].append(
                    f"{expense['category']}の上限(¥{limit})を超えています"
                )
        
        return validation

# n8n での使用例
def main(items):
    processor = ExpenseProcessor()
    results = []
    
    for item in items:
        ocr_text = item['json']['ocr_text']
        metadata = item['json']['metadata']
        
        expense = processor.process_receipt(ocr_text, metadata)
        results.append({'json': expense})
    
    return results

7. カスタマーサクセス自動化

// 顧客の健全性スコア計算とアクション
const customerHealthWorkflow = {
  calculateHealthScore: function(customer) {
    const weights = {
      usage: 0.3,
      engagement: 0.2,
      support: 0.2,
      payment: 0.15,
      feedback: 0.15
    };
    
    // 使用率スコア
    const usageScore = this.calculateUsageScore(customer.usage);
    
    // エンゲージメントスコア
    const engagementScore = this.calculateEngagementScore(customer.engagement);
    
    // サポートスコア(チケット数が少ないほど高スコア)
    const supportScore = this.calculateSupportScore(customer.supportTickets);
    
    // 支払いスコア
    const paymentScore = this.calculatePaymentScore(customer.payments);
    
    // フィードバックスコア
    const feedbackScore = this.calculateFeedbackScore(customer.nps, customer.satisfaction);
    
    // 総合スコアの計算
    const totalScore = 
      usageScore * weights.usage +
      engagementScore * weights.engagement +
      supportScore * weights.support +
      paymentScore * weights.payment +
      feedbackScore * weights.feedback;
    
    return {
      totalScore: Math.round(totalScore),
      breakdown: {
        usage: usageScore,
        engagement: engagementScore,
        support: supportScore,
        payment: paymentScore,
        feedback: feedbackScore
      },
      status: this.getHealthStatus(totalScore),
      recommendations: this.getRecommendations(totalScore, {
        usage: usageScore,
        engagement: engagementScore,
        support: supportScore,
        payment: paymentScore,
        feedback: feedbackScore
      })
    };
  },
  
  calculateUsageScore: function(usage) {
    const {
      monthlyActiveUsers,
      featuresUsed,
      apiCalls,
      dataVolume
    } = usage;
    
    // 各指標を0-100のスコアに正規化
    const mauScore = Math.min(monthlyActiveUsers / usage.totalUsers * 100, 100);
    const featureScore = featuresUsed.length / 10 * 100; // 10機能を基準
    const apiScore = Math.min(apiCalls / 10000 * 100, 100); // 月1万回を基準
    const dataScore = Math.min(dataVolume / 1000 * 100, 100); // 1GB を基準
    
    return (mauScore + featureScore + apiScore + dataScore) / 4;
  },
  
  calculateEngagementScore: function(engagement) {
    const {
      lastLoginDays,
      weeklyActiveUsers,
      featureAdoptionRate,
      trainingCompletion
    } = engagement;
    
    // ログイン頻度スコア
    const loginScore = Math.max(0, 100 - lastLoginDays * 10);
    
    // 週次アクティブ率
    const wauScore = weeklyActiveUsers / engagement.totalUsers * 100;
    
    // 機能採用率
    const adoptionScore = featureAdoptionRate * 100;
    
    // トレーニング完了率
    const trainingScore = trainingCompletion * 100;
    
    return (loginScore + wauScore + adoptionScore + trainingScore) / 4;
  },
  
  getHealthStatus: function(score) {
    if (score >= 80) return 'healthy';
    if (score >= 60) return 'at-risk';
    if (score >= 40) return 'critical';
    return 'churning';
  },
  
  getRecommendations: function(totalScore, breakdown) {
    const recommendations = [];
    
    // スコアが低い領域に対する推奨アクション
    if (breakdown.usage < 50) {
      recommendations.push({
        priority: 'high',
        area: 'usage',
        action: 'オンボーディングセッションの実施',
        description: '主要機能の使い方についてトレーニングを実施'
      });
    }
    
    if (breakdown.engagement < 60) {
      recommendations.push({
        priority: 'medium',
        area: 'engagement',
        action: 'エグゼクティブビジネスレビューの開催',
        description: 'ビジネス価値の確認と今後の活用計画の策定'
      });
    }
    
    if (breakdown.support < 70) {
      recommendations.push({
        priority: 'high',
        area: 'support',
        action: 'テクニカルヘルスチェック',
        description: '技術的な問題の早期発見と解決'
      });
    }
    
    if (totalScore < 60) {
      recommendations.push({
        priority: 'urgent',
        area: 'retention',
        action: 'カスタマーサクセスマネージャーによる直接対応',
        description: '解約リスクを軽減するための個別対応プラン策定'
      });
    }
    
    return recommendations.sort((a, b) => {
      const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 };
      return priorityOrder[a.priority] - priorityOrder[b.priority];
    });
  }
};

8. データ同期・ETLパイプライン

// 複数システム間のデータ同期
const etlPipeline = {
  name: "マルチシステムデータ同期",
  schedule: "0 2 * * *", // 毎日午前2時実行
  
  extractData: async function() {
    const sources = [
      {
        name: 'salesforce',
        endpoint: process.env.SALESFORCE_API_URL,
        auth: {
          type: 'oauth2',
          token: process.env.SALESFORCE_TOKEN
        },
        query: `
          SELECT Id, Name, Amount, CloseDate, StageName, AccountId
          FROM Opportunity
          WHERE LastModifiedDate >= YESTERDAY
        `
      },
      {
        name: 'mysql_erp',
        connection: {
          host: process.env.MYSQL_HOST,
          database: process.env.MYSQL_DB,
          user: process.env.MYSQL_USER,
          password: process.env.MYSQL_PASSWORD
        },
        query: `
          SELECT 
            order_id,
            customer_id,
            total_amount,
            order_date,
            status,
            updated_at
          FROM orders
          WHERE updated_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)
        `
      },
      {
        name: 'mongodb_analytics',
        connection: process.env.MONGODB_URI,
        collection: 'user_events',
        query: {
          timestamp: { $gte: new Date(Date.now() - 24*60*60*1000) }
        }
      }
    ];
    
    const extractedData = {};
    
    for (const source of sources) {
      try {
        const data = await this.extractFromSource(source);
        extractedData[source.name] = data;
      } catch (error) {
        console.error(`Failed to extract from ${source.name}:`, error);
        // エラー通知を送信
        await this.sendAlert({
          level: 'error',
          source: source.name,
          message: error.message
        });
      }
    }
    
    return extractedData;
  },
  
  transformData: function(extractedData) {
    const transformed = {
      unified_customers: [],
      unified_orders: [],
      metrics: {}
    };
    
    // Salesforce の商談データを注文データに変換
    if (extractedData.salesforce) {
      const sfOrders = extractedData.salesforce.map(opp => ({
        id: `SF_${opp.Id}`,
        customer_id: opp.AccountId,
        amount: opp.Amount,
        date: opp.CloseDate,
        status: this.mapSalesforceStatus(opp.StageName),
        source: 'salesforce',
        original_data: opp
      }));
      transformed.unified_orders.push(...sfOrders);
    }
    
    // MySQL ERP データの変換
    if (extractedData.mysql_erp) {
      const erpOrders = extractedData.mysql_erp.map(order => ({
        id: `ERP_${order.order_id}`,
        customer_id: order.customer_id,
        amount: order.total_amount,
        date: order.order_date,
        status: this.mapERPStatus(order.status),
        source: 'erp',
        original_data: order
      }));
      transformed.unified_orders.push(...erpOrders);
    }
    
    // MongoDB イベントデータの集計
    if (extractedData.mongodb_analytics) {
      const eventMetrics = this.aggregateEvents(extractedData.mongodb_analytics);
      transformed.metrics = eventMetrics;
    }
    
    // データ品質チェック
    transformed.data_quality = this.checkDataQuality(transformed);
    
    return transformed;
  },
  
  loadData: async function(transformedData) {
    const targets = [
      {
        name: 'data_warehouse',
        type: 'bigquery',
        dataset: 'unified_data',
        tables: {
          orders: transformedData.unified_orders,
          customers: transformedData.unified_customers,
          metrics: [transformedData.metrics]
        }
      },
      {
        name: 'reporting_db',
        type: 'postgresql',
        schema: 'analytics',
        tables: {
          daily_orders: transformedData.unified_orders,
          data_quality_log: [transformedData.data_quality]
        }
      }
    ];
    
    const loadResults = [];
    
    for (const target of targets) {
      try {
        const result = await this.loadToTarget(target);
        loadResults.push({
          target: target.name,
          status: 'success',
          records_loaded: result.recordCount,
          timestamp: new Date().toISOString()
        });
      } catch (error) {
        loadResults.push({
          target: target.name,
          status: 'failed',
          error: error.message,
          timestamp: new Date().toISOString()
        });
      }
    }
    
    // 同期結果のサマリー作成
    const summary = {
      execution_time: new Date().toISOString(),
      sources_processed: Object.keys(transformedData).length,
      total_records: transformedData.unified_orders.length,
      load_results: loadResults,
      data_quality: transformedData.data_quality
    };
    
    // レポート送信
    await this.sendSyncReport(summary);
    
    return summary;
  },
  
  checkDataQuality: function(data) {
    const quality = {
      completeness: {},
      accuracy: {},
      consistency: {},
      issues: []
    };
    
    // 完全性チェック
    const requiredFields = ['id', 'customer_id', 'amount', 'date', 'status'];
    let missingFields = 0;
    
    data.unified_orders.forEach(order => {
      requiredFields.forEach(field => {
        if (!order[field]) {
          missingFields++;
          quality.issues.push({
            type: 'missing_field',
            record_id: order.id,
            field: field
          });
        }
      });
    });
    
    quality.completeness.score = 
      ((data.unified_orders.length * requiredFields.length - missingFields) / 
       (data.unified_orders.length * requiredFields.length)) * 100;
    
    // 正確性チェック
    const invalidAmounts = data.unified_orders.filter(o => o.amount < 0).length;
    const futureDates = data.unified_orders.filter(o => new Date(o.date) > new Date()).length;
    
    quality.accuracy.score = 
      ((data.unified_orders.length - invalidAmounts - futureDates) / 
       data.unified_orders.length) * 100;
    
    // 一貫性チェック(重複チェック)
    const duplicates = this.findDuplicates(data.unified_orders);
    quality.consistency.duplicates = duplicates.length;
    quality.consistency.score = 
      ((data.unified_orders.length - duplicates.length) / 
       data.unified_orders.length) * 100;
    
    quality.overall_score = 
      (quality.completeness.score + quality.accuracy.score + quality.consistency.score) / 3;
    
    return quality;
  }
};

9. コンプライアンス監視自動化

// GDPR/個人情報保護法コンプライアンスチェック
const complianceMonitor = {
  name: "個人情報コンプライアンス監視",
  
  scanForPII: function(data) {
    const piiPatterns = {
      email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
      phone_jp: /0\d{1,4}-\d{1,4}-\d{4}/g,
      credit_card: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
      mynumber: /\d{4}\s?\d{4}\s?\d{4}/g, // マイナンバー
      passport: /[A-Z]{2}\d{7}/g
    };
    
    const findings = [];
    
    Object.entries(piiPatterns).forEach(([type, pattern]) => {
      const matches = data.match(pattern);
      if (matches) {
        findings.push({
          type: type,
          count: matches.length,
          samples: matches.slice(0, 3).map(m => this.maskPII(m, type))
        });
      }
    });
    
    return findings;
  },
  
  checkDataRetention: async function(database) {
    const retentionPolicies = {
      customer_data: 730, // 2年
      transaction_logs: 2555, // 7年(税法)
      access_logs: 365, // 1年
      marketing_data: 1095 // 3年
    };
    
    const violations = [];
    
    for (const [table, maxDays] of Object.entries(retentionPolicies)) {
      const query = `
        SELECT COUNT(*) as count, MIN(created_at) as oldest_record
        FROM ${table}
        WHERE created_at < DATE_SUB(NOW(), INTERVAL ${maxDays} DAY)
      `;
      
      const result = await database.query(query);
      
      if (result[0].count > 0) {
        violations.push({
          table: table,
          violation_count: result[0].count,
          oldest_record: result[0].oldest_record,
          max_retention_days: maxDays,
          action_required: 'データ削除またはアーカイブが必要'
        });
      }
    }
    
    return violations;
  },
  
  auditDataAccess: async function(accessLogs) {
    const suspiciousPatterns = [
      {
        name: '大量データエクスポート',
        check: (log) => log.action === 'export' && log.record_count > 1000
      },
      {
        name: '営業時間外アクセス',
        check: (log) => {
          const hour = new Date(log.timestamp).getHours();
          return hour < 6 || hour > 22;
        }
      },
      {
        name: '異常な頻度のアクセス',
        check: (logs, userId) => {
          const userLogs = logs.filter(l => l.user_id === userId);
          const hourlyAccess = {};
          userLogs.forEach(log => {
            const hour = new Date(log.timestamp).toISOString().slice(0, 13);
            hourlyAccess[hour] = (hourlyAccess[hour] || 0) + 1;
          });
          return Object.values(hourlyAccess).some(count => count > 100);
        }
      }
    ];
    
    const alerts = [];
    
    suspiciousPatterns.forEach(pattern => {
      accessLogs.forEach(log => {
        if (pattern.check(log)) {
          alerts.push({
            pattern: pattern.name,
            user_id: log.user_id,
            timestamp: log.timestamp,
            details: log,
            severity: 'high'
          });
        }
      });
    });
    
    return alerts;
  },
  
  generateComplianceReport: function(scanResults) {
    const report = {
      scan_date: new Date().toISOString(),
      summary: {
        total_issues: 0,
        critical_issues: 0,
        warnings: 0
      },
      findings: {
        pii_exposure: scanResults.pii,
        retention_violations: scanResults.retention,
        access_anomalies: scanResults.access
      },
      recommendations: [],
      compliance_score: 100
    };
    
    // スコア計算とレコメンデーション生成
    if (scanResults.pii.length > 0) {
      report.summary.critical_issues += scanResults.pii.length;
      report.compliance_score -= scanResults.pii.length * 10;
      report.recommendations.push({
        area: 'データ保護',
        action: 'PII データの暗号化またはマスキング実装',
        priority: 'critical'
      });
    }
    
    if (scanResults.retention.length > 0) {
      report.summary.warnings += scanResults.retention.length;
      report.compliance_score -= scanResults.retention.length * 5;
      report.recommendations.push({
        area: 'データ保持',
        action: '保持期限を超過したデータの削除プロセス実行',
        priority: 'high'
      });
    }
    
    report.summary.total_issues = 
      report.summary.critical_issues + report.summary.warnings;
    
    return report;
  }
};

10. マーケティング自動化ワークフロー

// リードスコアリングとナーチャリング
const marketingAutomation = {
  name: "リードスコアリング&ナーチャリング",
  
  scoreLeads: function(leads) {
    return leads.map(lead => {
      let score = 0;
      const scoringFactors = [];
      
      // デモグラフィックスコア
      if (lead.company_size > 100) {
        score += 20;
        scoringFactors.push({ factor: '企業規模', points: 20 });
      }
      
      if (lead.industry === 'technology' || lead.industry === 'finance') {
        score += 15;
        scoringFactors.push({ factor: '業界適合性', points: 15 });
      }
      
      if (lead.job_title.includes('部長') || lead.job_title.includes('マネージャー')) {
        score += 10;
        scoringFactors.push({ factor: '役職レベル', points: 10 });
      }
      
      // 行動スコア
      const behaviorScore = this.calculateBehaviorScore(lead.activities);
      score += behaviorScore.total;
      scoringFactors.push(...behaviorScore.factors);
      
      // エンゲージメントスコア
      const engagementScore = this.calculateEngagementScore(lead.engagement);
      score += engagementScore.total;
      scoringFactors.push(...engagementScore.factors);
      
      return {
        ...lead,
        lead_score: score,
        scoring_factors: scoringFactors,
        stage: this.determineStage(score),
        recommended_actions: this.getRecommendedActions(score, lead)
      };
    });
  },
  
  calculateBehaviorScore: function(activities) {
    const scoreMap = {
      'website_visit': 1,
      'download_whitepaper': 10,
      'webinar_attendance': 15,
      'demo_request': 25,
      'pricing_page_view': 20,
      'contact_form_submission': 30
    };
    
    let total = 0;
    const factors = [];
    
    activities.forEach(activity => {
      const points = scoreMap[activity.type] || 0;
      total += points;
      
      if (points > 0) {
        factors.push({
          factor: activity.type,
          points: points,
          date: activity.date
        });
      }
    });
    
    return { total, factors };
  },
  
  determineStage: function(score) {
    if (score >= 80) return 'sales_qualified';
    if (score >= 60) return 'marketing_qualified';
    if (score >= 40) return 'engaged';
    if (score >= 20) return 'aware';
    return 'cold';
  },
  
  createNurturingCampaign: function(leads) {
    const campaigns = {
      cold: {
        name: '認知度向上キャンペーン',
        emails: [
          {
            day: 0,
            subject: '{{company}}様の課題を解決する5つの方法',
            template: 'awareness_1'
          },
          {
            day: 7,
            subject: '業界トレンドレポート2024',
            template: 'awareness_2'
          },
          {
            day: 14,
            subject: '無料ウェビナーのご案内',
            template: 'webinar_invite'
          }
        ]
      },
      aware: {
        name: '興味喚起キャンペーン',
        emails: [
          {
            day: 0,
            subject: '{{first_name}}様だけの特別オファー',
            template: 'interest_1'
          },
          {
            day: 5,
            subject: '導入事例:{{industry}}業界での成功事例',
            template: 'case_study'
          },
          {
            day: 10,
            subject: 'ROI計算ツールを無料プレゼント',
            template: 'roi_calculator'
          }
        ]
      },
      engaged: {
        name: '検討促進キャンペーン',
        emails: [
          {
            day: 0,
            subject: '個別デモのご提案',
            template: 'demo_offer'
          },
          {
            day: 3,
            subject: '他社比較資料をご用意しました',
            template: 'comparison_guide'
          },
          {
            day: 7,
            subject: '期間限定:特別価格のご案内',
            template: 'special_pricing'
          }
        ]
      }
    };
    
    const campaignAssignments = [];
    
    leads.forEach(lead => {
      const campaign = campaigns[lead.stage];
      if (campaign) {
        campaignAssignments.push({
          lead_id: lead.id,
          campaign_name: campaign.name,
          start_date: new Date().toISOString(),
          emails: campaign.emails.map(email => ({
            ...email,
            scheduled_date: this.addBusinessDays(new Date(), email.day),
            personalization: {
              company: lead.company,
              first_name: lead.first_name,
              industry: lead.industry
            }
          }))
        });
      }
    });
    
    return campaignAssignments;
  },
  
  trackCampaignPerformance: function(campaignData) {
    const metrics = {
      by_campaign: {},
      by_email: {},
      overall: {
        sent: 0,
        opened: 0,
        clicked: 0,
        converted: 0
      }
    };
    
    campaignData.forEach(record => {
      // キャンペーン別集計
      if (!metrics.by_campaign[record.campaign_name]) {
        metrics.by_campaign[record.campaign_name] = {
          sent: 0,
          opened: 0,
          clicked: 0,
          converted: 0,
          open_rate: 0,
          click_rate: 0,
          conversion_rate: 0
        };
      }
      
      const campaign = metrics.by_campaign[record.campaign_name];
      campaign.sent++;
      if (record.opened) campaign.opened++;
      if (record.clicked) campaign.clicked++;
      if (record.converted) campaign.converted++;
      
      // 全体集計
      metrics.overall.sent++;
      if (record.opened) metrics.overall.opened++;
      if (record.clicked) metrics.overall.clicked++;
      if (record.converted) metrics.overall.converted++;
    });
    
    // レート計算
    Object.values(metrics.by_campaign).forEach(campaign => {
      campaign.open_rate = campaign.sent > 0 ? (campaign.opened / campaign.sent * 100).toFixed(2) : 0;
      campaign.click_rate = campaign.opened > 0 ? (campaign.clicked / campaign.opened * 100).toFixed(2) : 0;
      campaign.conversion_rate = campaign.sent > 0 ? (campaign.converted / campaign.sent * 100).toFixed(2) : 0;
    });
    
    metrics.overall.open_rate = (metrics.overall.opened / metrics.overall.sent * 100).toFixed(2);
    metrics.overall.click_rate = (metrics.overall.clicked / metrics.overall.opened * 100).toFixed(2);
    metrics.overall.conversion_rate = (metrics.overall.converted / metrics.overall.sent * 100).toFixed(2);
    
    return metrics;
  }
};

ベストプラクティス

1. エラーハンドリング

// グローバルエラーハンドラー
const errorHandler = {
  name: "Error Handler",
  type: "n8n-nodes-base.function",
  parameters: {
    functionCode: `
      try {
        // メインロジック
        return items;
      } catch (error) {
        // エラーログ
        const errorLog = {
          timestamp: new Date().toISOString(),
          workflow: $workflow.name,
          node: $node.name,
          error: {
            message: error.message,
            stack: error.stack,
            type: error.constructor.name
          },
          context: {
            itemIndex: $itemIndex,
            runIndex: $runIndex,
            mode: $mode
          }
        };
        
        // Slack通知
        await $item(0).helpers.httpRequest({
          method: 'POST',
          url: process.env.SLACK_WEBHOOK_URL,
          body: {
            text: \`:warning: ワークフローエラー: \${$workflow.name}\`,
            attachments: [{
              color: 'danger',
              fields: [
                { title: 'エラーメッセージ', value: error.message },
                { title: 'ノード', value: $node.name },
                { title: '時刻', value: new Date().toISOString() }
              ]
            }]
          }
        });
        
        // エラーを再スロー
        throw error;
      }
    `
  }
};

2. パフォーマンス最適化

// バッチ処理の実装
const batchProcessor = {
  processBatch: async function(items, batchSize = 100) {
    const results = [];
    
    for (let i = 0; i < items.length; i += batchSize) {
      const batch = items.slice(i, i + batchSize);
      
      // 並列処理
      const batchResults = await Promise.all(
        batch.map(item => this.processItem(item))
      );
      
      results.push(...batchResults);
      
      // レート制限対策
      if (i + batchSize < items.length) {
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }
    
    return results;
  }
};

まとめ

n8n を活用することで、複雑な業務プロセスを効率的に自動化できます。本記事で紹介したワークフローは、実際の業務で即座に活用できる実践的な例です。

重要なポイント:

  • 小さく始めて段階的に拡張
  • エラーハンドリングとログ記録の徹底
  • パフォーマンスとスケーラビリティを考慮
  • セキュリティとコンプライアンスの確保

エンハンスド株式会社では、n8n を活用した業務自動化の導入支援を行っています。お客様の業務に最適なワークフローの設計から実装まで、トータルでサポートいたします。


タグ: #n8n #業務自動化 #ワークフロー #ノーコード #ローコード #RPA

執筆者: エンハンスド株式会社 自動化ソリューション部

公開日: 2024年12月20日