n8nとSlack連携:通知・投稿・ボタンで承認・スラッシュコマンド

公開 書いた人 大林 諒

この記事では、「問い合わせ受付」のSlack通知を、読んで動ける形に育てます。Block Kitで見やすくし、種類ごとに担当者へメンションし、続報はスレッドに返し、見積もりには受付メールを送る前にボタンで承認を挟みます。逆向きに、Slackのリアクションで「対応済み」にする流れと、スラッシュコマンドで未対応の一覧を出す流れも作ります。前提は「n8nの使い方入門:最初のワークフローを作って動かす」の3ノード(n8n Form Trigger「問い合わせフォーム」→Google Sheets→Slack)が動いていることです。n8nの版は2026年10月時点の安定版2.44.7で書いています。

この記事の内容
  1. 先に決めること
  2. 作るものとスコープの一覧
  3. 手順
  4. 応用
  5. できないことと、その代わり
  6. よく詰まるところ
  7. 次にやること

先に決めること

ボットのトークンでつなぐか、OAuth2でつなぐか

SlackのCredential(認証情報)は「Access Token」と「OAuth2」の2種類で、Slackの上で誰として動くかが違います。

つなぎ方n8nが使うトークン投稿の名前Slack Trigger
Access Token(Bot User OAuth Token)ボットのトークン(xoxb-)アプリの名前使える(これしか使えない)
OAuth2(セルフホストで自分のアプリ)許可した人のユーザートークン許可した人の名前使えない
OAuth2(n8n Cloudの「Connect my account」)許可した人のユーザートークン許可した人の名前使えない

公式ドキュメントはSlackノードにOAuth2を勧めていますが、n8nのOAuth2は許可した人のユーザートークンで動きます。担当者が許可すると通知がその人の名前で出て、その人のアカウントが止まった日に通知も止まります。業務の通知はボットのトークン(Access Token)でつなぎます。Slack Triggerもこれでしか動かないので、Credentialが1つにまとまります。OAuth2が要るのは、メッセージの検索のようにユーザートークンでしか呼べない操作だけです。

アプリを何個作るか

Slackのアプリは、イベントの送り先(Request URL)を1つしか持てません。試すときのTest URLと本番のProduction URLを同じアプリで行き来すると、試している間は本番にイベントが届きません。運用しているn8nでは、本番用と試験用でアプリを分けています。この記事は本番用の1つで書きます。

承認ボタンをどちらの方式にするか

「Send and Wait for Response」(送信して返事を待つ)のボタンは、初期設定だとリンクで、押すとブラウザでn8nのページが開き、誰が押したかは残りません。「Capture Who Responded」(押した人を記録)をオンにすると、Slackの中で1回押せば決まり、押した人が残り、押せる人も絞れます。その代わり、n8nがインターネットから届くHTTPSで動いていることと、SlackアプリにInteractivity(クリックの送り先)の設定が要ります。社外への送信に関わる承認は、押した人が残る方式にします。

新着は #問い合わせ、承認は同じ投稿のスレッド、n8nのエラーは #n8n-監視 に分けます。エラー通知の作り方は「n8nを止めずに運用する:アップデート・バックアップ・エラー通知・脆弱性」のとおりで、同じアプリを使えます。

作るものとスコープの一覧

ワークフローきっかけSlackでやることSlackアプリ側の設定
問い合わせ受付(育てる)フォームの送信Block Kitで通知、メンション、スレッドに続報、ボタンで承認Bot Token Scopes、Interactivity
対応済みにする通知に ✅ のリアクションシートを「対応済み」にし、スレッドに返すEvent Subscriptions
未対応を答える/toiawase と打つ未対応の一覧を、打った人にだけ返すSlash Commands

アプリに付けるボットのスコープ(Bot Token Scopes)です。足りないとn8nは「Your Slack credential is missing required Oauth Scopes」と出し、足りないスコープ名を添えます。

スコープ何に使うか
chat:write投稿・スレッドへの返信・承認の依頼・投稿の書き換え
channels:read / groups:read公開/非公開チャンネルを一覧から選ぶ
users:read / users:read.emailユーザーの一覧、承認できる人の指定、押した人の名前とメール
usergroups:readユーザーグループ(@営業 など)のIDを調べる
reactions:readリアクションのイベントを受ける
commandsスラッシュコマンドを作る
channels:history新しい投稿のイベントを受ける(使うときだけ)
app_mentions:readアプリへのメンションを受ける(使うときだけ)

アプリが入っていない公開チャンネルに送れる chat:write.public は付けません。送り先のチャンネルにだけアプリを追加する運用のほうが、どこに流れているかを後から追えます。

手順

1. Slackアプリを作る

画面で1つずつ足すより、マニフェスト(アプリの設定をまとめたJSON)で作るほうが漏れがありません。公式ドキュメントもこちらを勧めています。

  1. api.slack.com/apps を開き、「Create New App」から「From a manifest」を選ぶ
  2. ワークスペースを選んで「Next」
  3. 「JSON」タブの中身を消して下のマニフェストを貼り、n8n.example.co.jp を自社のドメインに直す
  4. 「Next」で内容を確かめ、「Create」
  5. 「OAuth & Permissions」の「Install to Workspace」を押し、「Allow」(ワークスペースの管理者が押す)
  6. 同じ画面の「Bot User OAuth Token」(xoxb- で始まる)を控える
  7. 「Basic Information」の「App Credentials」にある「Signing Secret」を控える
  8. Slackで #問い合わせ チャンネルの詳細を開き、作ったアプリ(問い合わせ通知)をチャンネルに追加してメンバーにする
{
  "display_information": { "name": "問い合わせ通知" },
  "features": {
    "bot_user": { "display_name": "toiawase", "always_online": true },
    "slash_commands": [
      {
        "command": "/toiawase",
        "url": "https://n8n.example.co.jp/webhook/slack-toiawase",
        "description": "未対応の問い合わせを表示します",
        "should_escape": true
      }
    ]
  },
  "oauth_config": {
    "scopes": {
      "bot": ["chat:write", "channels:read", "groups:read", "users:read", "users:read.email", "usergroups:read", "reactions:read", "commands"]
    }
  },
  "settings": {
    "interactivity": {
      "is_enabled": true,
      "request_url": "https://n8n.example.co.jp/webhook-waiting-slack"
    },
    "token_rotation_enabled": false,
    "socket_mode_enabled": false,
    "org_deploy_enabled": false
  }
}

アプリ名は日本語にできますが、ボットの表示名(display_name)は英小文字・数字・-・_・. だけです。token_rotation_enabled は必ず false にします。トークンの自動更新を有効にすると12時間でトークンが切れ、一度有効にすると戻せないので、アプリを作り直すことになります。イベントの送り先はここでは入れず、手順6でワークフローを公開してから足します。Slackは登録の瞬間にそのURLへ確認を送るので、n8n側が待ち受けていないと登録できないからです。

2. n8nにCredentialを作る

  1. 「問い合わせ受付」のSlackノードを開き、「Authentication」が「Access Token」(初期値)であることを確かめる
  2. Credentialの欄で「Create new credential」を選ぶ
  3. 「Access Token」に xoxb- のトークン、「Signature Secret」にSigning Secretを貼って保存する
  4. 名前を「Slack 問い合わせ通知(bot)」のように、どのアプリのどのトークンか分かる名前にする

Signature Secretは空でも投稿はできますが、Slack Triggerが要求の送り主を確かめるのにも、押した人を記録する承認にも使います。無いと承認のボタンを押してもワークフローが進みません。n8n Cloudでも、Slack Triggerを使うならこの形で作ります。Credentialの共有や権限は「n8nの認証情報(Credential):OAuth2・APIキー・共有と権限」に書きました。

n8nの「Slack API」Credential画面。Access TokenとSignature Secretの欄
n8nの「Slack API」Credential画面。Access TokenとSignature Secretの欄

3. 通知をBlock Kitで組む

社外の人が書いた文をそのままSlackに流すと、本文に <!channel> とあっただけでチャンネル全員への通知になります。Slackの書式では &・<・> の3文字を置き換えれば済むので、Slackノードの前に整えるノードを挟みます。

  1. 「問い合わせ一覧に追加」とSlackの間の + から「Edit Fields (Set)」を足し、名前を「Slack用に整える」にする
  2. 「Mode」は「Manual Mapping」のまま、「Add Field」で次の項目を作る(型はすべてString)
名前値
company{{ $('問い合わせフォーム').item.json.company.replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' })[c]) }}
name・messagecompanyと同じ式で company を name・message に変える。message は末尾に .slice(0, 2900) を足す
category{{ $('問い合わせフォーム').item.json.category }}
mention{{ ({ 'お見積もり': '<!subteam^S0123ABCD>', 'サービスについて': '<@U0123ABCD>' })[$('問い合わせフォーム').item.json.category] ?? '' }}

本文を2,900字で切るのは、Block Kitの文の区画(section)が3,000字までで、超えると投稿ごと受け付けられないからです。

メンションは @山田 と書いても通知されません。人は <@メンバーID>、ユーザーグループは <!subteam^グループID> と書きます。IDはSlackノードで調べられます。「Resource」を「User」、「Operation」を「Get Many」にして一度実行すると、OUTPUTの id に各メンバーのIDが並びます。グループは「Resource」を「User Group」にすれば同じように出ます。Slackの説明では、人へのメンションはその人がチャンネルのメンバーのときだけ通知されるので、担当者は #問い合わせ に入れておきます。

次にSlackノードを直します。

  1. 「Message Type」を「Blocks」にする
  2. 「Blocks」の欄を「Expression」に切り替え、下のJSONを貼る
  3. 「Notification Text」に 新しい問い合わせ: {{ $json.company }}({{ $json.category }}) と入れる
  4. ノード名を「新着を知らせる」に変える
{
  "blocks": [
    { "type": "header", "text": { "type": "plain_text", "text": "新しい問い合わせ({{ $json.category }})" } },
    {
      "type": "section",
      "fields": [
        { "type": "mrkdwn", "text": {{ JSON.stringify('*会社名*\n' + $json.company) }} },
        { "type": "mrkdwn", "text": {{ JSON.stringify('*お名前*\n' + $json.name) }} },
        { "type": "mrkdwn", "text": {{ JSON.stringify('*担当*\n' + ($json.mention || '未定')) }} }
      ]
    },
    { "type": "section", "text": { "type": "mrkdwn", "text": {{ JSON.stringify($json.message) }} } },
    { "type": "context", "elements": [ { "type": "mrkdwn", "text": "受付番号 {{ $execution.id }} 対応したら :white_check_mark: を付けてください" } ] }
  ]
}

欄を「Expression」にしないと {{ }} がそのままの文字で送られます。値を "{{ $json.message }}" のように引用符で囲むと、本文に " や改行があった瞬間にJSONが壊れるので、JSON.stringify() で包みます。n8nが受け付けるのは { "blocks": [ ... ] } の形で、[ ... ] だけではエラーです。見た目はSlackのBlock Kit Builderで作り、値の部分だけ式に差し替えると早いです。

「Notification Text」はスマートフォンの通知に出る文です。Slackの説明では、スマートフォンの通知はBlocksの中身を使わずこの文だけを出すので、空だと何の問い合わせか分かりません。

Slackノードの設定。Message TypeがBlocks、Blocksの欄がExpressionになっている
Slackノードの設定。Message TypeがBlocks、Blocksの欄がExpressionになっている

4. 投稿IDを残して、続報をスレッドに返す

Slackの投稿には ts という番号が付き、これを指定するとその投稿のスレッドに返信できます。n8nのSlackノードは、送った投稿の ts を message_timestamp という名前でOUTPUTに出すので、シートに残せば別のワークフローからも同じスレッドに返せます。

  1. 「問い合わせ一覧」のH列に「受付番号」、I列に「Slack投稿ID」の見出しを足す
  2. 「問い合わせ一覧に追加」で「Mapping Column Mode」を選び直して列を読み込み直し、「受付番号」に {{ $execution.id }} を入れる
  3. 「新着を知らせる」の後ろにGoogle Sheetsの「Update Row」を足し、名前を「投稿IDを記録」にする
  4. 「Column to match on」を「受付番号」、値を {{ $execution.id }}、「Slack投稿ID」を {{ $json.message_timestamp }} にする
  5. 「Add option」から「Cell Format」を足し、「Let n8n format」を選ぶ

$execution.id は実行ごとに変わる番号です。行を探すキーに受付日時を使うと、同じ時刻に2件来たときに別の行を書き換えます。「Cell Format」を変えたのは、初期値の「Let Google Sheets format」だと 1729912345.123456 のような投稿IDが数値になり、桁が丸められて後で照合できなくなるからです。シートの扱いは「n8nとGoogleスプレッドシート連携:読み書き・更新・重複を防ぐ」に分けました。

スレッドに返すときは、Slackの「Send」で「Add option」から「Reply to a Message」を足し、「Message Timestamp to Reply To」に投稿IDを入れます。「Also Send to Channel」をオンにすると返信がチャンネルにも出ますが、続報のたびにオンにするとスレッドに分けた意味がなくなるので、全員が知るべき結果だけにします。

5. 送信の前に承認を挟む

見積もりの問い合わせには受付メールを自動で返します。ただ、競合の偵察や取引できない相手も混ざるので、送る前に営業の責任者が確かめます。

  1. 「投稿IDを記録」の後ろに「If」を足し、{{ $('問い合わせフォーム').item.json.category }} が「お見積もり」と等しい、にする
  2. true の出口にSlackを足し、操作を「Send and Wait for Response」、名前を「送信の承認」にする
  3. 「Send Message To」を「Channel」(初期値は「User」)にし、#問い合わせ を選ぶ
  4. 「Message」に下の文を入れる
  5. 「Response Type」は「Approval」のまま、「Approval Options」で「Type of Approval」を「Approve and Disapprove」、ボタンの文字を「送る」「送らない」にする
  6. 「Capture Who Responded」をオンにし、「Restrict Who Can Approve」で営業の責任者2名を選ぶ
  7. 「Options」で「Reply to a Message」を足し、{{ $('新着を知らせる').item.json.message_timestamp }} を入れる
  8. 「Options」で「Limit Wait Time」を足して1日にし、「Append n8n Attribution」を足してオフにする
  9. 後ろに「If」を足し、{{ $json.data.approved }} が true のときにする
  10. true の出口にGmailで受付メールを送るノード、false の出口に「送らずに止めました」とスレッドに返すSlackノードをつなぐ
<!subteam^S0123ABCD> 受付メールを送ってよいか確認してください
宛先: {{ $('問い合わせフォーム').item.json.email }}
会社名: {{ $('Slack用に整える').item.json.company }}

出力には approved(承認したか)と respondedAt(押した日時)に加え、responder に押した人のID・名前・メールアドレスが入ります。リスト外の人が押すと、その人にだけ断りの文が見えてワークフローは待ち続けます。文は「Unauthorized Reply」で日本語に変えられます。押されたあとは、初期値の「Show Outcome and Remove Buttons」でボタンが消え、誰が決めたかの1行が付くので、二重に押されません。

「Limit Wait Time」を入れないと、誰も押さない限り実行は「Waiting」のまま待ち続けます。2.44.7のSlackノードのソースを読むと、待ちに入るときこのノードは受け取ったデータをそのまま出力として持っておき、ボタンが押されたときだけ data.approved 付きの結果に置き換えます。そのため1日で切れたときは data.approved が付かず、「承認されたか」の false 側に流れてメールは送られない作りです。時間切れの動きは、本番の前に「Amount」を数分にして一度確かめておきます。Gmailでの送り方は「n8nとGmail連携:受信をきっかけに動かす、送る、振り分ける」に書きました。

Send and Wait for Response の設定。Response Type が Approval、Approval Options に「送る」「送らない」のボタン名、Capture Who Responded がオン
Send and Wait for Response の設定。Response Type が Approval、Approval Options に「送る」「送らない」のボタン名、Capture Who Responded がオン

AIのエージェントが道具を使う前に挟む「Human review」も承認先にSlackを選べます。組み方は「n8nでAIエージェントを作る:ChatGPT・Claude・Geminiをつなぐ」にあります。

6. リアクションで「対応済み」にする(Slack Trigger)

対応した人が通知に ✅ を付けたら、シートの「対応状況」を「対応済み」にします。シートを開かずに済むので、実際に本番で動かしていると更新漏れがかなり減ります。

  1. 新しいワークフロー「対応済みにする」を作り、最初に「Slack Trigger」を置いて手順2のCredentialを選ぶ
  2. 「Trigger On」を「Reaction Added」、「Channel to Watch」を #問い合わせ にする
  3. 「Options」から「Emoji Names to Filter」を足し、white_check_mark と入れる(コロンは付けない)
  4. 後ろにGoogle Sheetsの「Update Row」を足し、「Column to match on」を「Slack投稿ID」、値を {{ $json.item.ts }}、「対応状況」を「対応済み」にする
  5. 後ろにSlackの「Send」を足し、「Reply to a Message」に {{ $('Slack Trigger').item.json.item.ts }}、本文に <@{{ $('Slack Trigger').item.json.user }}> さんが対応済みにしました と入れる
  6. 「Publish」を押し、Slack Triggerの「Webhook URLs」から「Production URL」をコピーする
  7. api.slack.com/apps でアプリの「Event Subscriptions」を開き、「Enable Events」をオンにして「Request URL」に貼る。「Verified」と出れば登録済み
  8. 「Subscribe to bot events」に reaction_added を足して「Save Changes」を押し、再インストールを求められたら「Allow」まで進む

絵文字とチャンネルに合わないイベントは、n8nが受け取った時点で捨て、実行も作りません。「Watch Whole Workspace」をオンにすると全チャンネルのイベント1つごとに1実行を使うので、n8n Cloudでは必ず絞ります。✅ は親の通知に付けてもらいます。返信に付けると別の ts が届き、シートの行と一致しません。

7. スラッシュコマンドで未対応を答える

/toiawase と打つと、未対応の一覧が打った人にだけ出るようにします。スラッシュコマンドはSlack Triggerでは受けられないので、「Webhook」ノードで受けます。

Slackはスラッシュコマンドに3秒(3,000ミリ秒)以内の返事を求め、遅れると打った人に operation_timeout が出ます。シートを読んでいると3秒を超えることがあるので、先に「集計しています」と返し、一覧は要求に付いてくる一時的なURL(response_url)へ後から送ります。

  1. 新しいワークフロー「未対応を答える」を作り、「Webhook」を置いて名前を「Slackコマンド」にする
  2. 「HTTP Method」を「POST」、「Path」を slack-toiawase、「Respond」を「Using ‘Respond to Webhook’ Node」にする
  3. すぐ後ろに「Respond to Webhook」を足し、「Respond With」を「JSON」、「Response Body」を {"response_type": "ephemeral", "text": "集計しています"} にする
  4. 後ろにGoogle Sheetsの「Get Row(s)」を足し、「Filters」で「対応状況」が「未対応」の行にする。「Settings」で「Always Output Data」をオンにする
  5. 後ろに「Code」を足し、「Mode」を「Run Once for All Items」にして下のコードを入れる
  6. 後ろに「HTTP Request」を足し、「Method」を「POST」、「URL」を {{ $('Slackコマンド').item.json.body.response_url }} にする
  7. 「Send Body」をオン、「Body Content Type」を「JSON」、「Specify Body」を「Using JSON」にし、「JSON」に {{ JSON.stringify({ response_type: 'ephemeral', text: $json.text }) }} と入れて「Publish」
const rows = $input.all().filter(i => i.json['会社名']);
const text = rows.length === 0
  ? '未対応の問い合わせはありません'
  : `未対応は${rows.length}件です\n` +
    rows.map(i => `・${i.json['受付日時']} ${i.json['会社名']}(${i.json['種類']})`).join('\n');
return [{ json: { text } }];

「Always Output Data」をオンにしないと、0件のときに後ろが動かず「集計しています」で終わります。オンにすると空の item が1件流れるので、コードでは会社名がある行だけを数えています。Codeノードは「n8nのCodeノード:JavaScriptとPythonで足りないところを書く」で詳しく書きます。ephemeral は打った人にだけ見える返事で、in_channel にするとチャンネル全員に見えます。Slackからはフォームの形式で届き、$json.body.text(コマンドの後ろの文字)や $json.body.user_id(打った人)で取れます。

「未対応を答える」のキャンバス。Webhook→Respond to Webhook→Google Sheets→Code→HTTP Request
「未対応を答える」のキャンバス。Webhook→Respond to Webhook→Google Sheets→Code→HTTP Request

Webhookノードには、Slack Triggerのような署名の確認がありません。最低限、「Options」の「Only Run If」に {{ $json.body.team_id === 'T0123ABCD' && $json.body.api_app_id === 'A0123ABCD' }} を入れ、自社のワークスペースとアプリ以外からの要求では実行を作らないようにします。この2つは秘密ではないので、書き込みをするコマンドならSlackの署名(v0:タイムスタンプ:本文 をSigning SecretでHMAC-SHA256したもの)を確かめます。考え方は「n8nで外部サービスとつなぐ:Webhook・HTTP Request・API」と同じです。

応用

  • /toiawase 見積 と打ったらその種類だけに絞る。Codeノードで $('Slackコマンド').first().json.body.text を読んで絞り込みに足す
  • 手順7のCodeノードを「Schedule Trigger」につなぎ、毎朝9時に未対応を1通にまとめて #問い合わせ に送る
  • 「送らない」が押されたら、Slackの「Update」でもとの通知の見出しに「見送り」と付ける

できないことと、その代わり

できないこと代わりの手
Slack Triggerでスラッシュコマンドやボタンのクリックを受ける「Webhook」ノードで受ける。Slack Triggerが受けるのは投稿・リアクション・メンションなどのイベントだけ
Slack TriggerをOAuth2で動かすボットのトークン(Access Token)のCredentialを作る
1つのアプリで試験用と本番用のイベントの送り先を持つ試験用のアプリを別に作る
押した人を記録する承認と、自作のボタンを同じアプリで受けるInteractivityの送り先は1つなので、アプリを分ける
スレッドの中でスラッシュコマンドを使うアプリへのメンション(app_mention)をSlack Triggerで拾う
Slackノードにない操作(モーダルを開くなど)「HTTP Request」の「Authentication」でSlackのCredentialを選び、SlackのAPIを直接呼ぶ

よく詰まるところ

症状原因直し方
「Your Slack credential is missing required Oauth Scopes」スコープが足りないエラーに出たスコープを「OAuth & Permissions」に足し、アプリを再インストールする
not_in_channel で投稿できないアプリがチャンネルのメンバーでないチャンネルの詳細からアプリを追加する
ratelimited で止まる同じチャンネルへの投稿がおよそ1秒1件を超えた1通にまとめる。「Settings」の「Retry On Fail」をオンにし「Wait Between Tries (ms)」を長めにする
スラッシュコマンドで operation_timeout3秒以内に返事をしていないWebhookの直後に「Respond to Webhook」を置いて先に返す
「Request URL」がVerifiedにならない公開していない、URLが localhost、Signature Secretが違うPublishしてから貼る。外向きのURLとSigning Secretを確かめる
同じリアクションで2回、3回動くn8nが3秒以内に答えられず、Slackがやり直しを送った負荷を見直し、重複が困る処理の前に「Remove Duplicates」の「Remove Items Processed in Previous Executions」を置く
「New Message Posted to Channel」で、ワークフローが投稿するたびにまた動く動いた結果の投稿を、同じトリガーがもう一度拾っている「Options」の「Usernames or IDs to Ignore」に、その投稿をしているユーザー(アプリのボット)を入れる
12時間ほどでつながらなくなるトークンの自動更新が有効自動更新を無効にしたアプリを作り直し、トークンを入れ直す
Blocksでエラー、{{ }} がそのまま届くExpressionになっていない、値の " や改行でJSONが壊れたExpressionに切り替え、値は JSON.stringify() で包む
承認のボタンを押しても進まないSignature Secretが無いか違う、Interactivityの送り先が違うSigning Secretを入れ直し、送り先を /webhook-waiting-slack にする
リンクのボタンを押すと localhost が開くn8nが外向きのURLを知らない環境変数 N8N_WEBHOOK_URL に https://n8n.example.co.jp/ を入れて再起動する

次にやること

  • n8nで外部サービスとつなぐ:Webhook・HTTP Request・API
  • n8nの認証情報(Credential):OAuth2・APIキー・共有と権限
  • n8nを止めずに運用する:アップデート・バックアップ・エラー通知・脆弱性
  • n8nの使い方入門:最初のワークフローを作って動かす

あわせて読む