n8n×Slack×WordPressで記事自動投稿を構築する方法|Webhook受信からYMYL判定まで実践ログ
# n8n×Slack×WordPressで記事自動投稿を構築する方法|Webhook受信からYMYL判定まで実践ログ
n8nでSlack Webhookを使った記事自動投稿フローを作ると、「VerifiedなのにWebhookが発火しない」「Slack返信を受け取れない」といった問題に詰まることがあります。
この記事では、Jogtime向けに実際に構築したn8n×Slack×WordPress自動投稿フローを例に、Webhook受信から自己監査・YMYL判定・WordPress投稿までの構成と解決手順を実践ログとして公開します。
Webhookが発火しなかった原因と解決手順
「Verified」なのに動かない。n8nでSlack Webhookを使うとき、これが最初の難関ではないでしょうか。
私が詰まった原因は3つありました。順番に確認していくと解決できます。
原因1:Event SubscriptionsのRequest URLが間違っていた
n8nのWebhookノードには「Test URL」と「Production URL」の2種類があります。フローをPublishする前はTest URLで試していたのですが、Production URLとは別物です。
Slack AppのEvent Subscriptions → Request URLには必ずProduction URLを設定する必要があります。Test URLを貼り付けていると、フローをアクティブにしても発火しません。
確認手順:
- n8nのWebhookノードを開き「Production URL」をコピーする
- Slack App管理画面 → Event Subscriptions → Request URLに貼り付ける
- 「Verified ✓」になることを確認する
原因2:Subscribe to bot eventsに`message.channels`が追加されていなかった
Slackのスレッド返信を受け取るには、ボットイベントとしてmessage.channels(パブリックチャンネルの場合)を購読している必要があります。
Event Subscriptions画面の「Subscribe to bot events」セクションにmessage.channelsが追加されているかを確認してください。追加後はアプリの再インストールが必要になります。
原因3:n8nアプリがチャンネルに招待されていなかった
Slackアプリはチャンネルに招待しないとイベントを受け取れません。対象チャンネルで以下を実行します。
/invite @アプリ名この3点を順番に確認すると、ほとんどのケースで解決できます。
判断基準:
- Production URLが設定されていない → Slack App管理画面のRequest URLを差し替える
message.channelsがない → Subscribe to bot eventsに追加してアプリを再インストールする- アプリがチャンネルにいない →
/invite @アプリ名を実行する
Slackスレッド返信を正しく受け取るコード設計
Webhookが発火するようになっても、次の問題が待っています。Slackからは様々なイベントが飛んでくるため、「スレッド返信のみ」を正しく受け取るコードが必要です。
以下のコードで、不要なイベントをフィルタリングしています。
const body = $input.first().json.body;
// ボット自身の投稿は無視する
if (body.event?.bot_id) return [];
// Slackのリトライは無視する
if ($input.first().json.headers?.['x-slack-retry-num']) return [];
if ($input.first().json.headers?.['x-slack-retry-reason'] === 'http_timeout') return [];
// スレッド返信以外は無視する(thread_tsがないものはスレッド外の投稿)
if (!body.event?.thread_ts) return [];
// thread_tsとtsが一致する場合は親メッセージ(返信ではない)
if (body.event?.thread_ts === body.event?.ts) return [];特にx-slack-retry-numのチェックは重要です。Slackは3秒以内にレスポンスがないとリトライを送ってくるため、これを無視しないとフローが二重に発火してしまいます。
n8nフロー2の全体構成と各ノードの役割
実際に動いているフロー2の構成はこうなっています。全体を把握していると、どこで詰まっているかが特定しやすくなります。
Webhook(Slack返信受信)
→ site_config(サイト設定の読み込み)
→ If(スレッド返信フィルタ)
→ 回答品質判定(OpenAI)
→ 品質判定分岐
→ NG:Slack再送依頼通知
→ OK:session.json読み込み
→ RAG読み込み
→ 02本文生成(Claude)
→ 03自己監査(Claude)+ 外部監査(OpenAI)← 並列処理
→ 監査結果統合
→ 統合リライト(Claude)
→ YMYLチェック
→ YMYL判定分岐
→ NG:Slack通知(人間確認へ)
→ OK:article.md + meta.json書き込み → publish.mjs実行
→ Google Sheets更新(status → drafted)
→ session削除
→ Slack完了通知各ノードの役割で特に重要なのは以下の点です。
session.jsonによる状態保持: フロー1でSlackインタビューを送信した時点のデータ(キーワード・設計情報)をVPS上のsession.jsonに保存しておき、フロー2でWebhookを受信したタイミングで読み込みます。Webhookをまたぐと前段のデータが消えるため、この設計が必須です。
自己監査と外部監査の並列処理: ClaudeとOpenAIを同時に走らせ、監査観点を分散しています。自己監査だけだと見落としや思考の偏りが残る可能性があるため、別モデルを並列実行して差分を見る構造にしています。2つの監査結果を統合してから最終リライトに渡す構成です。
site_configノードの位置: フロー2ではWebhookの直後にsite_configを置いています。Webhookから始まるフローはCronトリガーと異なり、最初にsite_configが読み込まれないため、Webhookの直後に配置する設計が必要でした。
YMYL記事をAIに自動公開させない理由と設計
フレイル予防・健康系のコンテンツはYMYL(Your Money or Your Life)に該当します。誤った情報が読者の健康判断に影響する可能性があるため、AI単体での自動公開は行わないことを方針として確定しました。
具体的には、YMYLチェックでOKが出てもWordPressへの投稿はstatus: draftで行います。公開ボタンを押すのは人間の判断です。
この設計にした理由は2つあります。
1つ目は責任の所在を明確にするためです。AIが生成した記事でも、公開するという判断をした人間が内容に責任を持つ構造にしたかったためです。
2つ目はYMYLチェック自体の限界を認識しているからです。AIによるYMYLチェックは「明らかにアウトなもの」を弾くことはできますが、「微妙にグレーなもの」の判断は人間が行う必要があります。
n8nがdraftedとして投稿し、人間が確認して公開する。このフローは手間に見えて、実際には記事の品質に対する最後の砦として機能しています。
ただし、このままだと自分のメディアでどこまでAIに任せるかの判断基準は整理できません。
なぜなら、「何をどの順番で設計するか」という判断軸がまだ整理されていないからです。
→ 判断軸の整理はこちら
実際に投稿が成功するまでに直した3つのポイント
最初の記事(elderly-nutrition-overview、WP ID: 2270)が投稿できるまでに直した箇所を振り返っておきます。同じ構成を作る方の参考になれば幸いです。
ポイント1:session.jsonのパスが間違っていた
VPS上のsession.jsonの保存パスが /files/sessions/ になっていましたが、実際のパスは /root/n8n/files/sessions/ でした。フロー1でsessionを保存してもフロー2で読み込めず、データが消えているように見えていた原因がこれでした。
パスは環境によって異なるため、SSH接続でVPS上を実際に確認することをおすすめします。
find /root -name "session_*.json" 2>/dev/nullポイント2:Mergeノード後のデータ参照が壊れた
ノードを追加・削除した後にMergeノード以降のコードが$input.all()を参照していると、ノード順が変わった際にデータが取れなくなります。
解決策は$('ノード名').first()の形式でノード名を直接指定することです。これでノードの順番が変わっても参照が安定します。
ポイント3:Google OAuthの7日トークン切れ
Google SheetsノードのOAuth認証が7日で切れる問題がありました。Google Cloud ConsoleでOAuthアプリのステータスを本番へ変更しました。テスト状態では制限が発生するため、運用環境では本番公開設定を確認することをおすすめします。
判断基準:
- session.jsonが読み込めない → VPS上の実際のパスをSSHで確認する
- Mergeノード後にデータが取れない →
$('ノード名').first()形式に切り替える - Google Sheetsが突然動かなくなる → OAuthアプリのステータスを本番に変更する
よくある質問
Q. フロー1とフロー2は別フローとして作るべきですか?
はい、分けることをおすすめします。フロー1はCronトリガーまたは手動で動き、フロー2はSlack Webhookで動くため、トリガーが異なります。同じフロー内に入れようとするとデータの流れが複雑になります。session.jsonで状態を引き継ぐことで、別フローでも問題なく連携できます。
Q. YMYLチェックはどのノードで行いますか?
ClaudeのAIノードで行っています。システムプロンプトにYMYLの判定基準を記載し、本文を渡してNG/OKを返してもらいます。判定結果をIfノードで分岐させ、NGの場合はSlackに通知して人間の確認待ちにします。
Q. publish.mjsはどこに置きますか?
VPS上の /root/wp-auto-jogtime/ ディレクトリに配置しています。n8nのSSHノードから cd /root/wp-auto-jogtime && node publish.mjs articles/{slug} で実行します。
まとめ
n8n×Slack×WordPressの記事自動投稿フローで詰まるポイントは、大きく「Webhookが発火しない」「データがフローをまたいで消える」「YMYLをどう扱うか」の3つに集約されます。
今日できる一歩として、まずWebhookのProduction URLが正しく設定されているかを確認することをおすすめします。Verifiedになっていても、そのURLがProduction URLでなければ本番では動きません。この確認だけで多くの詰まりが解消できます。
session.jsonによる状態保持の設計と、YMYL記事の手動公開ポリシーは、最初から設計しておくと後から直す手間が省けます。フロー構築の参考になれば幸いです。
ただし、このままだと自分のメディアに合った設計判断を再現するのは難しいです。
なぜなら、「何をAIに任せて・何を人間が判断するか」という役割設計の基準が、まだ整理されていないからです。
→ 判断軸の整理はこちら
bizinets.biz
記事を読んで「もっと知りたい」と思ったあなたへ
bizinets.biz では、ビジネスの仕組みづくりを体系的に学べるコンテンツを用意しています。まずは一度、覗いてみてください。
bizinets.biz へ行く →