
問い合わせメールが届いたらAIが「返信が必要か」を判定し、必要なものだけGmailに返信下書きを作ってSlackにリンクを流す仕組みを、n8nとClaudeで作りました。送信は一切しません。人間がSlackのリンクから下書きを開き、直して送ります。この記事では、13ノードの構成、「返信不要」の理由をSlackに出す設計、Gmail Trigger起点のワークフローをテストするときの回避策、そしてClaudeの応答から本文を取り出すところで踏んだ罠をまとめます。
背景:自動返信ではなく「下書きまで」
問い合わせの一次対応は、早く返したい一方で、AIに勝手に送らせるのは怖い領域です。私は「AIは判定と下書きまで、送信は人間」に線を引きました。また、返信不要と判断したメールを黙って処理すると理由が見えず不安なので、返信不要も失敗も理由付きでSlackに通知し、Gmail側のラベルは「AI下書き済み」の1つだけにしました。
構成:13ノードの流れ
- 新着お問い合わせ(Gmail Trigger):問い合わせ用ラベルの新着を毎分ポーリング。本文込みで取る
- 設定(Set、includeOtherFields):会社名・署名・文体・NGワード・除外ドメイン・モデル・返信字数上限・Slackチャンネル・dryRunを1か所に集約。下流はすべて
$("設定").item.jsonを参照し、元メールの項目もここ経由で読む - 処理済みチェック:「AI下書き済み」が付いていれば何もしない
- 除外判定:差出人が no-reply / noreply / mailer-daemon を含む、または除外ドメインなら「返信不要」の通知へ
- Claude判定・生成(HTTP Request → Messages API):返信不要なら
SKIP:理由、必要なら返信本文だけを返す - 生成成功判定 → 返信要否判定:本文が空なら「処理NG」、
SKIP:で始まれば「返信不要」の通知へ - 下書き作成(Gmail draft)→ ラベル付与 → Slack通知(下書き):差出人・件名・受信時刻・「下書きを開く」「元スレッドを開く」リンク・本文冒頭200字
下書き作成ノードの要点と、Slackに載せるGmailリンクの形はこうです。
resource: draft / operation: create
subject : Re: {{ 元メールの件名 }}
message : {{ Claude の生成本文 }} + 空行 + "--" + 会社名 + 担当名
threadId: 元メールの threadId (元スレッドの中に返信下書きとして並ぶ)
sendTo : Reply-To があればそれ、無ければ差出人
下書き : https://mail.google.com/mail/?authuser=<アカウント>#drafts/<下書きの message.id>
元スレッド: https://mail.google.com/mail/?authuser=<アカウント>#all/<threadId>
判定の線引き:「除外」と「返信不要」は誰が決めたかが違う
結果はどちらも返信不要ですが、除外はルール、返信不要はClaudeの判定です。Slackの理由欄でそれが分かるようにしました。除外なら「除外ルール: 差出人が自動送信アドレス(no-reply)」と当たったルールを列挙し、Claudeの判定なら「AI判定: お見積りへのお礼のみで返信は不要なため」のように SKIP: の後ろの文をそのまま出します。
当初は件名に「メルマガ」「配信停止」が含まれる場合もルールで除外していましたが、廃止しました。お客様とのやり取りの件名に「配信停止」が入ることは普通にあり、ルールで機械的に落とすのは危ないからです。件名の判断はClaudeに任せています。Claudeへの指示(system)の要点は次のとおりです。
あなたは<会社名>の問い合わせメール対応担当です。受信メールを読み、返信の要否を判定してください。
事業内容: ... / 参考情報: ... / 文体: 丁寧かつ簡潔なビジネス日本語 / 使用を避ける表現: 絶対,必ず,保証します,最安
返信本文は400文字以内に収めてください。
メルマガ・自動通知・お礼のみ・営業DM等、返信不要な場合は SKIP: に続けて理由のみを出力してください。
返信が必要な場合は、丁寧な日本語で返信本文のみを出力してください(件名や署名は含めない)。
価格・納期など確約できない質問には、次の文言で受けてください: 恐れ入りますが、詳細につきましては担当者より改めてご連絡いたします。
判定は本文の内容のみに基づいて行ってください。送信元のドメインが自社と同一であることを理由に返信不要と判断しないでください。
出力ルール: 出力は返信本文(または SKIP:)の1文字目から開始してください。判定コメント、前置き、区切り線(---)、説明文は一切出力しないでください。
「参考情報」には会社概要や「初回の問い合わせはフォームから」「料金は個別見積り」といった事実を入れています。営業時間はサイトに書いていないので「断定せず、担当より折り返しご連絡と案内する」と指示しました。知らないことは知らないと言わせる指示が要ります。
テストの方法:Gmail Trigger起点は手動実行できない
n8nのMCP(ワークフローをコードから作る口)からは、Schedule / Webhook / Form / Chat / Manual Trigger 起点のワークフローしか実行できません。回避策は、下流のノードが元メールを $("新着お問い合わせ") ではなく $("設定") から読むように統一しておき、テスト時だけ「Manual Trigger → Gmail(get) → 設定」という一時入口を足して特定のメールを流すことです。設定ノードは includeOtherFields なので、入口が何であれ下流から見える形は同じです。終わったら一時入口を削除します。
自分宛に3種類のメールを送って確認しました。件名「メルマガ」は除外ルールで返信不要の通知、お礼だけの本文はClaudeが SKIP: を返して理由付きの通知、本文が「おい」だけの短いメールには用件を確認する下書きが作られ、ラベルとSlack通知まで通りました。Claudeの1回の生成は入力673トークン・出力222トークン、約3秒でした。
ハマりポイント
- Claudeの応答の先頭に thinking ブロックが付く:本文を
content[0].textで取っていたところ、先頭がthinkingブロックになって text が undefined になり「処理NG」に落ちました。本文を使う5か所(生成成功判定・返信要否判定・下書き本文・Slackの抜粋・返信不要の理由)をすべて、text ブロックだけを連結する式に変えました{{ ($json.content || []).filter(c => c.type === "text").map(c => c.text).join("\n").trim() }} - 壊れたラベルは一覧に出るのに付けられない:以前作ったラベルを addLabels に使うと
400 labelId not foundになりました。一覧には出るのに付与できない状態です。新しいラベルを作り直したら通りました - Slackの資格情報に channels:read が無い:チャンネル一覧の取得がスコープ不足で失敗します。投稿(chat:write)はできるので、チャンネルIDはSlack側で確認して設定ノードに直接入れました。通知先を変えるときは、そのチャンネルにBotを招待するのを忘れると投稿が失敗します
- HTTP Requestノードの認証はMCPの一括更新で付かない:
setNodeCredentialがHTTP Requestの httpHeaderAuth を受け付けず、バッチ全体が巻き戻りました。SDKのコード側でcredentialsを指定しておけば作成時に入ります
まとめ
AIに問い合わせ対応を任せるときの要は、「どこまでをAIに任せ、どこから人が見るか」の線引きです。この構成では、送信は人間、返信不要も失敗も理由付きでSlackに出す、という形にしました。以前書いたSNS自動投稿の品質設計と同じで、「自動で直さず、人が見られる形で止める」ほうが、結局は信頼して任せられます。
問い合わせ対応・SNS投稿・データ集計など、こうした業務自動化の仕組みづくりは、AI自動化代行で構築から運用まで承ります。ご相談はお問い合わせフォームからどうぞ。
← TECH-BLOG一覧へ