bbizinets.life
n8nでWebhook後にデータが消える問題をsession.jsonで解決した
実践ログ

n8nでWebhook後にデータが消える問題をsession.jsonで解決した

n8nでAI記事生成フローを作っていたとき、Webhook後に前段のデータがまるごと消えるという問題に直面しました。Schedule Triggerで記事設計まで進めてSlackに質問を送ったのに、Slackの返信を受け取るWebhookのフローでは「keyword」も「slug」も「記事の設計データ」も一切使えない状態になっていました。

解決策はVPS上にsession.jsonとして状態を保存することでした。この記事では、なぜWebhookをまたぐとデータが消えるのか、どう解決したか、実際に詰まったポイントを記録しています。

この記事でわかること:

  • n8nでWebhook後に前段データが消える理由
  • session.jsonで状態保持する方法
  • Slackのmessage.ts / thread_tsでセッションを対応させる方法
  • Docker環境で/files配下に保存する必要がある理由
  • Extract from Fileノードが必要になるケース

Webhook後に前段データが消えた:n8nで詰まった問題

詰まった状況は、こんな構成でした。

フロー1はSchedule Triggerで起動して、キーワードを取得し、記事の方向性を設計して、Slackにインタビュー質問を3問送信して終わります。フロー2はSlackのスレッド返信を受け取るWebhookで起動して、返信内容と記事設計データを統合して記事を生成します。

問題は、フロー1が「Slackを送信して終了」した時点で、フロー1の実行コンテキストは閉じてしまうことでした。フロー2が起動するのはSlackに返信が来たとき、つまり人間が質問に答えたときです。その間、数分から数十分のタイムラグがあります。

フロー2のWebhookノードにはSlackの返信テキストしか渡ってきません。フロー1で作ったkeyword、slug、記事設計の全データはどこにもない。「あれ、なんで使えないんだろう?」と思ってExecutionsタブを確認して、初めて原因が分かりました。

n8nの1回の実行(Execution)は、トリガーから終了まで1つのコンテキストです。別のトリガーで起動する別の実行は、完全に別のコンテキストになります。フロー間でデータを自然に共有する仕組みはありません。

Webhookを挟んで人間の入力を待つ構成では、前半フローのデータを外部に保存しておく必要があります。

なぜWebhookをまたぐとデータが消えるのか

n8nの実行モデルを理解すると、この問題は設計上当然の動きだと分かります。

n8nのワークフローは、トリガーが発火するたびに1つの「実行」として動きます。Schedule Triggerで動くフロー1と、Webhookで動くフロー2は、それぞれ独立した実行です。フロー1が終了した後にフロー2が起動しても、フロー1のメモリは残っていません。

これはn8nの問題ではなく、ステートレスな実行モデルの仕様です。同じことはAWS LambdaやCloudflare Workersでも起きます。「実行が終わればメモリは消える」のが非同期・イベント駆動型の基本的な動きです。

つまり、Webhookを挟む非同期フローで状態を保持したいなら、実行の外側(ファイル、DB、キャッシュなど)に状態を保存するしかありません。今回はVPS上のJSONファイルを選びました。

今回の構成:前半フローと後半フローを分けた理由

なぜフローを2つに分けたかというと、「人間の一次情報をAI記事に注入したい」という目的があったからです。

AIだけで記事を生成すると、検索上位に並ぶ既存記事の情報を再配置するだけになりがちです。差別化できるのは「その人にしか書けない経験」です。そこで、Slackにインタビュー質問を3問送って、実際に経験した失敗・判断・数値の変化を答えてもらい、その一次情報をAIに渡して記事を生成する設計にしました。

フロー1(Schedule Trigger起点):

  • keyword_queue.csvからキーワードを1件取得
  • slug生成・市場調査・記事設計をLLMで実行
  • インタビュー質問3問を生成してSlackに送信
  • session.jsonを保存して終了

フロー2(Webhook起点):

  • Slackのスレッド返信を受信
  • session.jsonを読み込んでフロー1の状態を復元
  • 返信テキストと記事設計データを統合
  • 記事本文生成・監査・WordPress投稿へ進む

この構成では、Slack返信を待つ間にフロー1の実行は完全に終了しています。だからこそ、session.jsonによる状態保持が必要になります。

解決策:session.jsonにフロー1の状態を保存する

解決策はシンプルで、フロー1の最後にsession.jsonを保存するノードを追加することでした。

保存場所はVPS上の /root/n8n/files/sessions/ です。ファイル名はSlackにメッセージを送ったときに返ってくる message.ts(タイムスタンプ)を使って session_{ts}.json としました。

保存するデータは以下のとおりです。

{
  "ts": "1710000000.000000",
  "keyword": "n8n session.json Webhook 状態保持",
  "slug": "n8n-webhook-session-json",
  "context_01": {
    "intent": "Webhookを挟んだ後に前段データを保持したい読者向けの記事",
    "design_output": "記事設計の全データ(H2構造・内部リンク候補・市場調査結果など)"
  }
}

ts はSlack側の thread_ts と対応させるための識別子として使います。フロー2で thread_ts を受け取ったとき、同じ値を持つsession.jsonを読み込む設計です。

n8nのExecute CommandノードまたはWrite Binary Fileノードを使って書き込みます。今回はJSONをbase64エンコードして printf '%s' でデコードしながらファイルに書き込む方法を採用しました。

Execute CommandノードにCommandとして記述するコマンドの例です。

mkdir -p /files/sessions && printf '%s' "{{ $json.encoded_session }}" | base64 -d > /files/sessions/session_{{ $json.ts }}.json

encoded_session は前のノードでJSONをbase64エンコードした値、ts はSlackの message.ts から取得したタイムスタンプです。パスや変数名は環境によって異なるため、自分の構成に合わせて調整してください。

Slackのmessage.tsとthread_tsで対応させる設計

この設計で地味に大事なのが、「どのsession.jsonを読み込むか」の対応付けです。

Slackにメッセージを送信したとき、そのメッセージには message.ts というタイムスタンプが返ってきます。このタイムスタンプはメッセージごとに一意です。フロー1ではこの値をsession.jsonのファイル名に使います。

session_1710000000.000000.json

ユーザーがスレッドに返信すると、Webhookで受け取るイベントデータに thread_ts が含まれます。この thread_ts は、返信先のメッセージの message.ts と同じ値になります。

フロー2では thread_ts を取り出して、対応するsession.jsonを読み込みます。読み込んだあと、session.jsonの ts と thread_ts が一致しているか確認するコードを挟むと、誤ったスレッド返信やセッション混入を防げます。

const replyData = $('スレッド返答受信').first().json;
const session = $input.first().json.fileContent;

if (replyData.thread_ts !== session.ts) {
  throw new Error(`thread_ts不一致: ${replyData.thread_ts} !== ${session.ts}`);
}

これがないと、別の記事のセッションを誤って読み込んでしまう可能性があります。複数のフローが並行して動いているときは特に重要です。

詰まりポイント1:コンテナから/root直下に書き込めなかった

ここが最初に詰まったポイントです。

VPS上に /root/n8n/files/sessions/ というパスを作ってsession.jsonを保存する設計にしていました。ところが、n8nのExecute CommandノードからVPSのファイルシステムを操作しようとしたとき、/root/ 直下に自由に書き込めませんでした。

原因はDockerのボリューム設定でした。n8nはDockerコンテナ内で動いており、コンテナから書き込めるのは docker-compose.yml でマウントされた /files/ 配下だけです。コンテナの外側(VPSのファイルシステム)には直接アクセスできません。

対応関係はこうなります。

n8nコンテナ内のパス | VPS上の実際のパス
--- | ---
`/files/sessions/` | `/root/n8n/files/sessions/`

n8n側では /files/sessions/session_{ts}.json として扱い、VPS側では /root/n8n/files/sessions/session_{ts}.json として保存される、という対応関係を意識する必要がありました。

ここを間違えると、「書き込んだつもりなのにファイルが存在しない」というエラーになります。実際に私はこれで一度ハマって、VPSにSSHで入ってファイルを探し回りました。

確認方法:

ls /root/n8n/files/sessions/

このコマンドでVPS上にsession.jsonが作られているか確認できます。

詰まりポイント2:読み込み後にExtract from Fileノードが必要だった

フロー2でsession.jsonを読み込んだとき、もう一つ詰まりがありました。

環境によっては、読み込んだファイルがそのままJSONオブジェクトではなく、バイナリデータとして扱われることがあります。私の環境では、後続ノードで直接 session.keyword を参照できず、Extract from Fileノードを挟む必要がありました。

設定:

  • Operation:Extract From Text File(または Convert to JSON)
  • Source Key:data

このノードを通すことで、session.jsonの中身がJavaScriptオブジェクトとして扱えるようになります。Extract from Fileを飛ばすと、後続のCodeノードで session.keyword や session.context_01 を参照しようとしてもundefinedになります。

判断基準:

  • $input.first().json.fileContent がundefinedになる → Extract from Fileが抜けている
  • $input.first().binary.data が参照文字列のような値になっている → バイナリ問題、Extract from Fileを挟む

session.jsonが向いているケース・向いていないケース

session.jsonは便利ですが、万能ではありません。使い分けの判断基準として整理しておきます。

向いているケース:

  • 1記事分、1処理分の一時状態を保持したい
  • Slack返信やWebhookを待つだけで済む
  • 処理完了後に削除してよいデータ
  • DBを用意するほどではない小規模な非同期フロー

向いていないケース:

  • 長期保存したいマスターデータ
  • 複数ユーザーや複数フローが同時に読み書きする
  • 検索・集計・参照が必要なデータ
  • 永続的な業務データとして使いたい場合
  • セッション数が大量になる運用

今回は「1記事分の一時記憶」として使い、WordPress投稿が完了した時点でsession.jsonを削除する設計にしています。この用途に限れば、シンプルで壊れにくい方法だと感じています。

この設計で得られたこと・注意点

得られたこと:

session.jsonを導入してから、非同期フローが安定して動くようになりました。Slackに返信があった時点で、フロー1で作った記事設計データがそのまま復元されて記事生成に進めます。デバッグのときも、VPS上にsession.jsonが残っているため「どこまで処理が進んでいたか」を確認できます。

また、フロー1が異常終了したときにsession.jsonが残らないため、「session.jsonがあれば前半フローは正常完了している」という判断基準として使えるようになりました。

注意点:

複数の記事が並行して処理される場合は、ファイル名の衝突に注意が必要です。今回は message.ts をファイル名に使うことで衝突を防いでいますが、秒単位で複数のスケジュールが動く構成では別途対策が必要になります。

まとめ

Webhook後にデータが消えるのは、n8nの設計上自然な動きです。Schedule TriggerとWebhook Triggerは別々の実行コンテキストで動くため、前半フローのデータは後半フローには届きません。

session.jsonはこの問題に対する現実的な解決策でした。VPS上のJSONファイルに状態を保存し、Slackのmessage.tsをキーとして対応するセッションを読み込む設計にすることで、人間の返信を待つ非同期フローが成立しました。

詰まりポイントをまとめると:

  • n8nコンテナから書き込めるのは /files/ 配下のみ(Docker volumeの対応を確認する)
  • session.json読み込み後はExtract from Fileノードが必要なケースがある
  • thread_tsとtsの一致チェックを入れるとセッション混入を防げる

今回のsession.json設計で重要だったのは、単にファイルへ保存することではなく、どこで止めて、どこで待ち、どこから再開するかを決めることでした。こうした自動化の判断軸は、bizinets.biz側で整理しています。

bizinets.bizの判断軸ページはこちら

bizinets.biz

記事を読んで「もっと知りたい」と思ったあなたへ

bizinets.biz では、ビジネスの仕組みづくりを体系的に学べるコンテンツを用意しています。まずは一度、覗いてみてください。

bizinets.biz へ行く →

関連記事