n8n ワークフローエンジニア
トリガーするタイミング
- n8n ワークフローJSONの編集または構築
- n8nのデバッグ: 「ノードが実行されない」、「ペアアイテムが利用できない」、「静的データが保持されない」
- n8n REST API を使用したバルク操作(名前変更、再試行、削除)
- Telegram, Gmail, HTTP Request, Code, Switch, Merge ノードのパターン
- n8n でのスケジュールトリガー / タイムゾーン / cron の動作
- n8n v2.x UI の動作(公開/非公開、アーカイブ)
厳格なルール
- 編集前に監査する。 ワークフローJSON全体を読む → 所見を提示する → 確認する → 一括で適用する。決して盲目的にパッチを当てない。
- スケジュールトリガーでテストする。 手動の「実行」は
$workflow.staticDataをリセットする。重複排除の証明には実際のcron実行が必要。 - 実際の結果を確認する。 実行ログと最終状態(メッセージが届いた、行が存在する)を確認する。ノードが
ok: trueを返すのは良い兆候だが、証明ではない。UIの同期ラグが観測者を誤らせる。 - Codeノードのモードと戻り値を一致させる。 間違ったモード + 間違った戻り値 = サイレントな部分的な処理。
クイックリファレンス — 最もよくあるバグ
| 症状 | 根本原因 | 修正方法 |
|---|---|---|
localhost:4000 へのHTTP Requestが拒否/タイムアウトを返す | n8nがDockerブリッジモード | ホストゲートウェイを使用: デフォルトブリッジでは多くの場合 172.17.0.1(docker network inspect bridge で確認)、または extra_hosts に host.docker.internal:host-gateway をマップする |
| Telegramメッセージが届かない、エラーなし | IPv6 DNS解決のタイムアウト | NODE_OPTIONS=--dns-result-order=ipv4first をdocker-composeに設定 |
new Date() がcronがESTなのにUTCを返す | コンテナに TZ 環境変数がない | TZ と GENERIC_TIMEZONE の両方をタイムゾーンに設定する |
parse_mode: Markdown がサイレントに失敗するか文字化けする | AI出力にマッチしない * または _ | parse_mode: HTML + 送信前にサニタイズする |
| LiteLLM HTTP Requestノードで401 | 認証ヘッダーがない | Authorization: Bearer {{ $env.LITELLM_API_KEY }} を追加 |
Claude呼び出しで ECONNABORTED: timeout of 15000ms | デフォルトの15秒タイムアウト | options.timeout: 90000 を設定 |
PUT /api/v1/workflows/:id が400を返す | ボディに余分なフィールドがある | name, nodes, connections, settings, staticData のみに絞る |
| ノードの名前変更がワークフローをサイレントに壊す | 接続参照が古い名前を参照している | 古い名前を nodes[].name と connections{} キーの両方で全て置換する |
| Code集約後にペアアイテムが利用できない | N→1→M でアイテムの系統が失われた | executeOnce: true を設定し、$('UpstreamNode').all() を明示的に反復処理する |
| 手動実行で静的データの重複排除が「機能しない」 | 手動実行が静的データをリセットする | スケジュールされたcronでのみテストする |
Codeノード — モード/戻り値のルール
すべてのCodeノードには mode パラメーターがあります。不一致 = サイレントバグ。
| モード | アクセスパターン | 戻り値 |
|---|---|---|
runOnceForAllItems (デフォルト) | const items = $input.all(); | return items.map(...) — 配列である必要があります |
runOnceForEachItem | $json が現在のアイテム | return { json: ... } — 単一オブジェクト、配列 NOT |
不一致の症状:
runOnceForAllItems+$jsonにアクセス → 最初のアイテムのみがサイレントに処理されるrunOnceForEachItem+ 配列を返す →"A 'json' property isn't an object"バリデーションエラー
重複排除パターン(クロスラン状態)
const store = $getWorkflowStaticData('global');
store.jobIds = store.jobIds || [];
const newItems = items.filter(item => !store.jobIds.includes(item.json.id));
// 新しいIDを保持し、無制限の成長を防ぐために最後の500に制限
store.jobIds = [...store.jobIds, ...newItems.map(i => i.json.id)].slice(-500);
return newItems;
重要: staticData は スケジュールされた 実行間でのみ保持されます。手動実行はそれをリセットします。重複排除を確認するには、「実行」を2回クリックするのではなく、連続する2回のスケジュール実行を監視します。
実行ごとのキャッシュクリーンアップ
// 最初のCodeノードの先頭 — 一時的なキャッシュをクリーンアップするが、クロスラン状態は保持する
const store = $getWorkflowStaticData('global');
for (const k of Object.keys(store)) {
if (k.startsWith('cjob_') || k.startsWith('cprep_') || k === 'pendingJobs') {
delete store[k];
}
}
// jobIdsは絶対に削除しない — クロスランの重複排除はこれに依存する
パイプライン観測可能性パターン
主要な各Codeノードにカウンターを設定します。フォーマットステップで読み取ります。メッセージに追加します。
// ステージ1 — ティッカーを構築
const store = $getWorkflowStaticData('global');
store.pipeline = { ticker_count: tickers.length, fixed_count: fixed.length };
// ステージ2 — シグナルを計算
store.pipeline = store.pipeline || {};
store.pipeline.signal_count = results.length;
// 最終 — メッセージをフォーマット
const pl = store.pipeline || {};
const pf = (n) => (n === undefined || n === null) ? '?' : String(n);
const pipelineBlock =
'\n\n📊 パイプライン\n' +
'ティッカー構築: ' + pf(pl.ticker_count) + '\n' +
'シグナル: ' + pf(pl.signal_count);
ペアアイテムの問題と回避策
症状: 集約ノード(Nアイテム → 1プロンプト → 1 LLM応答 → Mアイテムに分割)の後、下流の $('UpstreamNode').item.json が Paired item data for item from node 'X' is unavailable をスローする。
回避策(優先順位順):
-
下流のCodeノードで
executeOnce: trueを設定し、明示的に反復処理する:const upstream = $('UpstreamNode').all(); const outputs = []; for (let i = 0; i < parsed.length; i++) { outputs.push({ json: { ...parsed[i], original: upstream[i].json } }); } return outputs; -
静的データを介してIDを渡し、下流で明示的に検索する。
-
pairedItem.sourceOverwrite— 壊れやすく、最後の手段。
Telegram パターン
HTMLモード — 許可されているのは7タグのみ
<b>, <i>, <u>, <s>, <a>, <code>, <pre> — これ以外は 400 Bad Request: Unsupported start tag をスローする。
AI生成コンテンツのための3層防御
- プロンプト: どのタグが許可されているかをAIに明示的に伝える
- サニタイズ: 送信前にコード内でサポートされていないタグを削除する
- エスケープ: ユーザー/AIコンテンツを埋め込む前にHTMLエスケープする(
&→&,<→<,>→>)
n8n帰属フッターを非表示にする
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false,
"disable_web_page_preview": true
}
トークンを .env に vs クレデンシャル
ビルトインのTelegramノード+クレデンシャルよりも、環境変数+HTTP Requestパターンを推奨:
- 移行時に移植可能
- ローテーションが容易(
nano .env+ コンテナ再起動) - DB暗号化キーロックアウトのリスクなし
- パターン:
POST https://api.telegram.org/bot{{ $env.BOT_TOKEN }}/sendMessage - トレードオフ: トークンがリクエストURLに含まれるため、実行データ、プロキシログ、エラーメッセージに表示される可能性がある。このワークフローの実行データ保存をオフにするか、他の人がログを読める場合はビルトインのTelegramクレデンシャルを使用する。
Gmail OAuth パターン
| 問題 | 解決方法 |
|---|---|
invalid_grant ... refresh token revoked | 「テスト」モードのOAuthは約7日で期限切れになる。UIから再認証するか、アプリを本番公開する。 |
空の payload.headers(From/Subjectが空白) | デフォルトの simple: true がヘッダーを削除する。options.simple: false を設定する。 |
| セキュリティアラート/領収書が欠落 | -category:promotions -category:social -category:updates を除外しない — Gmailは関連メールの90%をそこにルーティングする。is:unread newer_than:Nd のみを使用する。 |
| 複数アカウント間で既読にする必要がある | アカウントごとにクレデンシャルが必要。1つのGmailクレデンシャルで別のアカウントのメッセージIDを操作することはできない。 |
REST API パターン(バルク操作)
認証: X-N8N-API-KEY: <key> ヘッダー。
| エンドポイント | 注意点 |
|---|---|
GET /api/v1/workflows | すべてを返す(ページネーション)。クライアント側でフィルタリング。 |
GET /api/v1/workflows/:id | 完全なワークフローJSON。 |
PUT /api/v1/workflows/:id | 厳格: name, nodes, connections, settings, staticData のみ。settings 自体にも許可リストがある(下記参照)。GETしてからPUTする往復で、それ以外をすべて削除する。 |
DELETE /api/v1/workflows/:id | 実際に削除する(UIはアーカイブのみ提供)。バルククリーンアップに使用。 |
POST /api/v1/workflows/:id/activate | v2.x UIの公開と同じ。 |
settings フィールドの許可リスト(PUTボディ)
executionOrder, timezone, saveExecutionProgress, saveDataErrorExecution, saveDataSuccessExecution, saveManualExecutions, callerPolicy, callerIds, errorWorkflow, executionTimeout
ノード type は作成後に不変
node.type を変更するPUTは200を返すが、タイプは同じまま。変換するには(例: telegram → httpRequest): ノードオブジェクト全体を置き換える(id に新しいUUID、同じ name)。接続は名前でルーティングされるため、名前を維持した置き換えは安全。
n8n v2.x UI の詳細
| 旧 (v1.x) | 新 (v2.x) |
|---|---|
| アクティブ化トグル | 公開(緑色のボタン、右上)/ 非公開 |
| ワークフロー削除 | アーカイブのみ(実際の削除にはREST APIを使用) |
HTTP Request ノード — LLM呼び出しの必須設定
{
"method": "POST",
"url": "http://172.17.0.1:4000/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{"name": "Content-Type", "value": "application/json"},
{"name": "Authorization", "value": "=Bearer {{ $env.LITELLM_API_KEY }}"}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({model:'<your-model-alias>',messages:[{role:'user',content:$json.prompt}],temperature:0.3}) }}",
"options": {
"timeout": 90000,
"retry": { "tries": 2, "waitBetweenTries": 5000 }
}
}
n8nコンテナには curl がない
コンテナ内でHTTPテストを行うには、docker exec 経由でNode.jsのビルトインhttpsモジュールを使用する:
docker exec n8n node -e "const https=require('https'); /* request */"
SSH事前確認(VPS上のヒアドキュメントの前)
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 <user>@<host>
# プロンプトを確認: [<user>@<host> ...]$
# その後にのみヒアドキュメントを貼り付ける
SSHドロップ後にPowerShellにbashヒアドキュメントを貼り付ける = 数百の解析エラー。
評価スイートパターン(ロジックのみ)
n8nの評価機能を介したモック入力テストは、APIクレジットを消費せずにロジックの回帰をキャッチする。フィルターロジック、キーワードリスト、またはバリデーションルールへの変更の前と後に再実行する。
アンチパターン
| アンチパターン | 代わりに行うこと |
|---|---|
| 「もう一度実行して、うまくいくか見てみる」 | まず実行ログを最初から最後まで読む |
| 多くの往復で一度に1つのノードにパッチを当てる | 完全なファイルを監査 → バンドルされたパッチスクリプト → 一括で確認する |
AI出力に parse_mode: Markdown を使用する | parse_mode: HTML + サニタイザー |
| ワークフローJSONにAPIキー/チャットIDをハードコーディングする | {{ $env.KEY }} を参照し、<secret-file>(chmod 600)に保存する |
| 接続を更新せずにノードの名前を変更する | nodes[].name と connections{} キーの両方でノード名を全て置換する |
| 重複排除をテストするために手動実行する | スケジュールトリガーを待つ |
| LLM呼び出しのデフォルトのhttpRequestタイムアウト | 最低90000msを設定する |
runOnceForAllItems と $json を混在させる | モードをアクセスパターンに一致させる |