n8nで実現する業務自動化事例集:実践的なワークフロー10選
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日