HubSpotコンテキストに基づくAIメール返信とSlack承認
上級
これはCRM, AI Summarization分野の自動化ワークフローで、19個のノードを含みます。主にIf, Code, Gmail, Slack, Filterなどのノードを使用。 HubSpotコンテキストに基づくAIメール返信とSlack承認
前提条件
- •Googleアカウント + Gmail API認証情報
- •Slack Bot Token または Webhook URL
- •HubSpot API Key
- •ターゲットAPIの認証情報が必要な場合あり
- •Google Gemini API Key
ワークフロープレビュー
ノード接続関係を可視化、ズームとパンをサポート
ワークフローをエクスポート
以下のJSON設定をn8nにインポートして、このワークフローを使用できます
{
"meta": {
"instanceId": "09423a3357ff64bdcc082268b9d577001317edbe377a3ccfb0b131ffb9554b30"
},
"nodes": [
{
"id": "6897a614-cd5a-4e76-99fb-7094b4692dd2",
"name": "Google Gemini チャットモデル",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
2352,
464
],
"parameters": {
"options": {}
},
"typeVersion": 1
},
{
"id": "1b011a66-c09a-4a6c-b666-89a5301f54cc",
"name": "Reply to a message",
"type": "n8n-nodes-base.gmail",
"position": [
3184,
240
],
"webhookId": "597ba693-ad1e-4a19-9c41-8f8a82e7849f",
"parameters": {
"message": "={{ $('Draft Reply (AI Agent)').item.json.output }}",
"options": {
"appendAttribution": false
},
"emailType": "text",
"messageId": "={{ $('Watch Gmail (New Inbound)').first().json.threadId }}",
"operation": "reply"
},
"typeVersion": 2.1
},
{
"id": "287d6e80-9e28-4188-87a8-c46def152c1e",
"name": "Watch Gmail (New Inbound)",
"type": "n8n-nodes-base.gmailTrigger",
"position": [
544,
256
],
"parameters": {
"filters": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
}
},
"typeVersion": 1.3
},
{
"id": "4b25d17f-ce32-47d2-a27d-ca52c68a5e46",
"name": "フィルター: Allowed Sender",
"type": "n8n-nodes-base.filter",
"position": [
752,
256
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c0511cfb-d540-4b31-b665-f6038d6f8bbe",
"operator": {
"type": "string",
"operation": "notContains"
},
"leftValue": "={{ $json.From }}",
"rightValue": "n8n.io"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "61b5b40e-988e-4482-b729-0669e9081fb2",
"name": "Draft Reply (AI エージェント)",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2352,
256
],
"parameters": {
"text": "=You are a helpful, concise customer support/sales assistant. Draft a ready-to-send email reply.\n\nDO NOT output JSON, arrays, or anything under CONTEXT. Only output the email.\n\n# INPUTS\n\nMy name (for signature): John Bolton\nFrom: {{$('Watch Gmail (New Inbound)').first().json.From}}\nSubject: {{$('Watch Gmail (New Inbound)').first().json.Subject}}\nCustomer message:\n{{$('Watch Gmail (New Inbound)').first().json.snippet}}\n\n# CONTEXT (do not quote or restate; summarize only if helpful)\nContact (HubSpot JSON):\n{{ JSON.stringify($('Find Contact by Email').first().json.properties || {}, null, 2) }}\n\nCompanies (JSON, may be empty):\n{{ JSON.stringify($json.companies || []) }}\n\nDeals (JSON, may be empty):\n{{ JSON.stringify($json.deals || []) }}\n\nTickets (JSON, may be empty):\n{{ JSON.stringify($json.tickets || []) }}\n\n# WHAT TO DO\n- Acknowledge the sender and the exact topic in the Subject/body.\n- Answer their request directly and succinctly.\n- Offer 1–2 clear next steps or a single CTA.\n- Personalize using safe context only:\n - Use contact name/company if present.\n - If deals exist, mention at most the 1–2 most relevant (name, stage, amount, close date). Ignore IDs/owner/pipeline/internal fields.\n - If tickets exist, reference subject/status briefly if relevant.\n- If context is missing, write a generic but professional reply (do not invent facts).\n\n# TONE\nFriendly, professional, plain language. Short paragraphs or brief bullets.\n\n# OUTPUT FORMAT (no extra commentary, no subject, just the email body)\n- Greeting with the person’s name if available.\n- 2–5 sentences answering the question; bullets allowed for steps.\n- Optional one-line context (deal/ticket) if helpful.\n- One clear CTA.\n- Polite sign-off with a sender name placeholder.\n\n# CONSTRAINTS\n- Never expose IDs, raw JSON, or internal property names.\n- Keep under ~150 words unless necessary.\n- If anything is unclear, end with exactly one clarifying question.\n\nGenerate the reply now.\n",
"options": {},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "81616dff-2202-4e66-9c2f-7e93403bf909",
"name": "Find Contact by メール",
"type": "n8n-nodes-base.hubspot",
"position": [
1040,
256
],
"parameters": {
"operation": "search",
"authentication": "oAuth2",
"filterGroupsUi": {
"filterGroupsValues": [
{
"filtersUi": {
"filterValues": [
{
"value": "={{ String($json.From || '').match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i)?.[0] || '' }}",
"propertyName": "email|string"
}
]
}
}
]
},
"additionalFields": {
"properties": [
"email",
"firstname",
"lastname",
"jobtitle",
"company",
"country",
"state",
"city",
"hs_language",
"phone",
"mobilephone",
"lifecyclestage",
"hs_lead_status",
"hubspot_owner_id",
"hs_email_last_open_date",
"hs_email_last_reply_date",
"hs_latest_meeting_activity",
"hs_sequences_is_enrolled",
"hs_sequences_enrolled_count",
"createdate",
"hs_lastmodifieddate",
"hs_timezone",
"notes_last_contacted",
"hs_object_id"
]
}
},
"typeVersion": 2.1
},
{
"id": "6e5e8866-223c-4cdc-b201-7e804d47b01d",
"name": "設定 Record Types",
"type": "n8n-nodes-base.code",
"position": [
1248,
256
],
"parameters": {
"jsCode": "const input = $input.first();\nlet records = Array.isArray(input?.json?.records)\n ? input.json.records\n : [\"deals\",\"companies\",\"tickets\"];\n\nreturn records.map(name => ({ json: { record: name } }));"
},
"typeVersion": 2
},
{
"id": "94281ea8-a451-42a9-9fb6-b90e4b5dc42a",
"name": "List Contact Associations",
"type": "n8n-nodes-base.httpRequest",
"position": [
1456,
256
],
"parameters": {
"url": "=https://api.hubapi.com/crm/v4/objects/contacts/{{ $('Find Contact by Email').item.json.id }}/associations/{{ $json.record }}",
"options": {
"response": {}
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "hubspotOAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "70d7c8e3-bbc5-4be2-bc65-c5c168bcba84",
"name": "Build Batch Read Requests",
"type": "n8n-nodes-base.code",
"position": [
1664,
256
],
"parameters": {
"jsCode": "// Build batch/read requests for only: deals, companies, tickets\n\nconst PROPS = {\n deals: [\n \"dealname\",\n \"amount\",\n \"dealstage\",\n \"pipeline\",\n \"closedate\",\n \"hubspot_owner_id\",\n \"hs_lastmodifieddate\",\n ],\n companies: [\n \"name\",\n \"domain\",\n \"industry\",\n \"numberofemployees\",\n \"annualrevenue\",\n \"website\",\n \"phone\",\n \"city\",\n \"state\",\n \"country\",\n \"hubspot_owner_id\",\n \"createdate\",\n \"hs_lastmodifieddate\",\n ],\n tickets: [\n \"hs_ticket_id\",\n \"subject\",\n \"content\",\n \"hs_pipeline\",\n \"hs_pipeline_stage\",\n \"hs_ticket_priority\",\n \"hs_lastmodifieddate\",\n \"createdate\",\n \"closed_date\",\n ],\n};\n\n// If the upstream node emits these three in order, this helps infer the object when not provided\nconst ORDER = [\"deals\", \"companies\", \"tickets\"];\n\nfunction toBatchRead(item, idx) {\n const object = item.json.object || item.json.record || ORDER[idx];\n\n const results = Array.isArray(item.json.results) ? item.json.results : [];\n const ids = results.map(r => String(r.toObjectId)).filter(Boolean);\n\n return {\n json: {\n object,\n url: `https://api.hubapi.com/crm/v3/objects/${object}/batch/read`,\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: {\n properties: PROPS[object] || [],\n archived: false,\n inputs: ids.map(id => ({ id })),\n },\n hasInputs: ids.length > 0,\n count: ids.length,\n },\n };\n}\n\nreturn $input.all().map(toBatchRead);\n"
},
"typeVersion": 2
},
{
"id": "ee30292a-fce0-4e18-a422-3d7a58b82e4e",
"name": "Batch Read Objects",
"type": "n8n-nodes-base.httpRequest",
"position": [
1888,
256
],
"parameters": {
"url": "={{ $json.url }}",
"body": "={{ $json.body }}",
"method": "POST",
"options": {
"response": {}
},
"sendBody": true,
"contentType": "raw",
"authentication": "predefinedCredentialType",
"rawContentType": "={{ $json.headers['content-type'] }}",
"nodeCredentialType": "hubspotOAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "e588dad3-5cdc-47f8-b180-d97d8e0bbb0a",
"name": "Normalize CRM Context for LLM",
"type": "n8n-nodes-base.code",
"position": [
2096,
256
],
"parameters": {
"jsCode": "// n8n Code node (JavaScript)\n// Input: three items (HubSpot batch/read responses) for deals, companies, tickets (order unknown)\n// Output: a single consolidated item with cleaned, LLM-ready fields\n\nconst items = $input.all().map(i => i.json);\n\n// --- helpers ---\nconst isNonEmpty = v => v !== null && v !== undefined && v !== '';\nconst stripNulls = obj =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => isNonEmpty(v)));\n\nfunction detectType(block) {\n const first = block?.results?.[0]?.properties || {};\n if ('dealname' in first || 'dealstage' in first) return 'deals';\n if ('hs_ticket_id' in first || 'hs_pipeline' in first) return 'tickets';\n if ('name' in first || 'industry' in first) return 'companies';\n return 'unknown';\n}\n\nfunction mapDeal(p) {\n return stripNulls({\n id: p.hs_object_id || p.id,\n name: p.dealname,\n stage: p.dealstage,\n amount: isNonEmpty(p.amount) ? Number(p.amount) : undefined,\n pipeline: p.pipeline,\n closeDate: p.closedate,\n ownerId: p.hubspot_owner_id,\n createdAt: p.createdate,\n lastUpdatedAt: p.hs_lastmodifieddate,\n });\n}\n\nfunction mapCompany(p) {\n // Derive a simple location string when possible\n const parts = [p.city, p.state, p.country].filter(isNonEmpty);\n const hq = parts.length ? parts.join(', ') : undefined;\n\n return stripNulls({\n id: p.hs_object_id || p.id,\n name: p.name,\n domain: p.domain,\n website: p.website,\n phone: p.phone,\n industry: p.industry,\n employees: isNonEmpty(p.numberofemployees) ? Number(p.numberofemployees) : undefined,\n annualRevenue: isNonEmpty(p.annualrevenue) ? Number(p.annualrevenue) : undefined,\n headquarters: hq,\n ownerId: p.hubspot_owner_id,\n createdAt: p.createdate,\n lastUpdatedAt: p.hs_lastmodifieddate,\n });\n}\n\nfunction mapTicket(p) {\n return stripNulls({\n id: p.hs_ticket_id || p.hs_object_id || p.id,\n subject: p.subject,\n description: p.content,\n pipelineId: p.hs_pipeline,\n stageId: p.hs_pipeline_stage,\n priority: p.hs_ticket_priority,\n createdAt: p.createdate,\n lastUpdatedAt: p.hs_lastmodifieddate,\n closedDate: p.closed_date,\n });\n}\n\n// --- collect ---\nconst out = { deals: [], companies: [], tickets: [] };\n\nfor (const block of items) {\n const t = detectType(block);\n const rows = Array.isArray(block.results) ? block.results : [];\n if (t === 'deals') {\n out.deals = rows.map(r => mapDeal(r.properties || {})).filter(o => Object.keys(o).length);\n } else if (t === 'companies') {\n out.companies = rows.map(r => mapCompany(r.properties || {})).filter(o => Object.keys(o).length);\n } else if (t === 'tickets') {\n out.tickets = rows.map(r => mapTicket(r.properties || {})).filter(o => Object.keys(o).length);\n }\n}\n\n// Optional high-level summary for the LLM\nout.summary = {\n dealCount: out.deals.length,\n companyCount: out.companies.length,\n ticketCount: out.tickets.length,\n};\n\n// Emit a single consolidated item\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "04448b5d-ac9c-417b-9246-3853e94303f0",
"name": "待機 for Response - Approve Auto-Reply",
"type": "n8n-nodes-base.slack",
"position": [
2768,
256
],
"webhookId": "ffb81691-54b1-43da-8b71-a4c45362901b",
"parameters": {
"select": "channel",
"message": "={{ $('Watch Gmail (New Inbound)').first().json.From }} sent you the following message:\n\n{{ $('Watch Gmail (New Inbound)').first().json.snippet }}\n\n\nHere is an auto-generated reply (press \"Approve\" to send it):\n\n{{ $json.output }}",
"options": {
"limitWaitTime": {
"values": {
"resumeUnit": "days"
}
}
},
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09H7HTHRMG",
"cachedResultName": "all-n8n-slack-test"
},
"operation": "sendAndWait",
"authentication": "oAuth2"
},
"typeVersion": 2.3
},
{
"id": "464de728-acf5-4664-9469-f4f660d29ec6",
"name": "If Approved?",
"type": "n8n-nodes-base.if",
"position": [
2976,
256
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9313939d-ed39-4a91-b0c6-18512a9c4676",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.data.approved }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "0eb866f6-f229-48da-bf53-a19b0439c7a9",
"name": "付箋",
"type": "n8n-nodes-base.stickyNote",
"position": [
976,
112
],
"parameters": {
"color": 7,
"width": 1280,
"height": 384,
"content": "## Get CRM information\nFetch contact info and associated deals, tickets and compaines."
},
"typeVersion": 1
},
{
"id": "bb2fb3e1-dbcc-468f-9d0b-65e379aad792",
"name": "付箋1",
"type": "n8n-nodes-base.stickyNote",
"position": [
496,
112
],
"parameters": {
"color": 7,
"width": 464,
"height": 384,
"content": "## Get incoming email"
},
"typeVersion": 1
},
{
"id": "53796f4b-13c6-4af0-ad1b-199ac49ac096",
"name": "付箋2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2272,
112
],
"parameters": {
"color": 7,
"width": 400,
"height": 528,
"content": "## Write draft response"
},
"typeVersion": 1
},
{
"id": "39b501df-7fc7-470f-8aa4-6dacc05eb255",
"name": "付箋3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2688,
112
],
"parameters": {
"color": 7,
"width": 704,
"height": 384,
"content": "## Approve and reply"
},
"typeVersion": 1
},
{
"id": "86267d6e-aa6e-4521-a0fb-c823724b7e7d",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"color": 5,
"width": 468,
"height": 624,
"content": "## AI email reply with HubSpot context + Slack approval\n\n### How it works\n1. A new Gmail message arrives.\n2. Look up the sender in HubSpot and fetch related deals, companies, and tickets.\n3. Draft a reply with Gemini.\n4. Post the draft to Slack for approval.\n5. If approved, send a reply\n\n### Setup\n1. **Gmail:** Use the same account for the trigger and the send nodes.\n2. **HubSpot:** Connect all the HubSpot nodes.\n3. **Slack:** Connect Slack and choose where to send the draft for approval.\n4. **Gemini:** Add your [Google AI Studio](https://aistudio.google.com/) API key\n5. **Filter:** Tweak or remove the sender rule before going live.\n\n### Customize\n1. **Prompt:** Adjust tone, length, and how much CRM detail to include.\n2. **Fields:** Pick which deal/company/ticket properties to pull.\n3. **Approval:** Skip Slack to auto-send, or add extra reviewers if needed."
},
"typeVersion": 1
},
{
"id": "f7cb3860-8006-401b-9c4a-2fbb3ca0aa68",
"name": "付箋6",
"type": "n8n-nodes-base.stickyNote",
"position": [
3184,
432
],
"parameters": {
"color": 7,
"width": 376,
"height": 232,
"content": "### 💡 Customizing this workflow\n\n* Be sure to update your name in the Agent prompt, so it get added to the email signature\n* Add a HubSpot form as the trigger and send customers a personalized followup email\n* Instead of replying to the message, create a draft in your Gmail inbox instead. That way, you'll be able to edit the message before sending."
},
"typeVersion": 1
}
],
"pinData": {
"Watch Gmail (New Inbound)": [
{
"To": "\"miha.ambroz@n8n.io\" <miha.ambroz@n8n.io>",
"id": "199823d41f5aa56f",
"From": "Miha Ambroz <miha.ambroz@pm.me>",
"labels": [
{
"id": "INBOX",
"name": "INBOX"
},
{
"id": "IMPORTANT",
"name": "IMPORTANT"
},
{
"id": "CATEGORY_PERSONAL",
"name": "CATEGORY_PERSONAL"
},
{
"id": "UNREAD",
"name": "UNREAD"
}
],
"Subject": "Hey",
"payload": {
"mimeType": "text/plain"
},
"snippet": "I forgot what I last ordered. Can you help me? Sent from Proton Mail Android",
"threadId": "199823d41f5aa56f",
"historyId": "585196",
"internalDate": "1758826671000",
"sizeEstimate": 4059
}
]
},
"connections": {
"464de728-acf5-4664-9469-f4f660d29ec6": {
"main": [
[
{
"node": "1b011a66-c09a-4a6c-b666-89a5301f54cc",
"type": "main",
"index": 0
}
]
]
},
"Set Record Types": {
"main": [
[
{
"node": "94281ea8-a451-42a9-9fb6-b90e4b5dc42a",
"type": "main",
"index": 0
}
]
]
},
"ee30292a-fce0-4e18-a422-3d7a58b82e4e": {
"main": [
[
{
"node": "e588dad3-5cdc-47f8-b180-d97d8e0bbb0a",
"type": "main",
"index": 0
}
]
]
},
"Find Contact by Email": {
"main": [
[
{
"node": "Set Record Types",
"type": "main",
"index": 0
}
]
]
},
"Draft Reply (AI Agent)": {
"main": [
[
{
"node": "Wait for Response - Approve Auto-Reply",
"type": "main",
"index": 0
}
]
]
},
"Filter: Allowed Sender": {
"main": [
[
{
"node": "Find Contact by Email",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "Draft Reply (AI Agent)",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"70d7c8e3-bbc5-4be2-bc65-c5c168bcba84": {
"main": [
[
{
"node": "ee30292a-fce0-4e18-a422-3d7a58b82e4e",
"type": "main",
"index": 0
}
]
]
},
"94281ea8-a451-42a9-9fb6-b90e4b5dc42a": {
"main": [
[
{
"node": "70d7c8e3-bbc5-4be2-bc65-c5c168bcba84",
"type": "main",
"index": 0
}
]
]
},
"287d6e80-9e28-4188-87a8-c46def152c1e": {
"main": [
[
{
"node": "Filter: Allowed Sender",
"type": "main",
"index": 0
}
]
]
},
"e588dad3-5cdc-47f8-b180-d97d8e0bbb0a": {
"main": [
[
{
"node": "Draft Reply (AI Agent)",
"type": "main",
"index": 0
}
]
]
},
"Wait for Response - Approve Auto-Reply": {
"main": [
[
{
"node": "464de728-acf5-4664-9469-f4f660d29ec6",
"type": "main",
"index": 0
}
]
]
}
}
}よくある質問
このワークフローの使い方は?
上記のJSON設定コードをコピーし、n8nインスタンスで新しいワークフローを作成して「JSONからインポート」を選択、設定を貼り付けて認証情報を必要に応じて変更してください。
このワークフローはどんな場面に適していますか?
上級 - 顧客管理, AI要約
有料ですか?
このワークフローは完全無料です。ただし、ワークフローで使用するサードパーティサービス(OpenAI APIなど)は別途料金が発生する場合があります。
関連ワークフロー
HubSpot 連絡先 AI エンハンスメント
HubSpot は AI 強化で最初に接続します
Filter
Hubspot
Agent
+
Filter
Hubspot
Agent
12 ノードMiha
顧客管理
n8nノードの探索(可視化リファレンスライブラリ内)
n8nノードを可視化リファレンスライブラリで探索
If
Ftp
Set
+
If
Ftp
Set
113 ノードI versus AI
その他
AI通話要約 + HubSpotフォローアップタスク
AI通話要約 + HubSpotフォローアップタスク
Hubspot
Form Trigger
Hubspot Tool
+
Hubspot
Form Trigger
Hubspot Tool
12 ノードMiha
顧客管理
メールからWhatsAppへ - AIマルチアカウントブリッジ
AI駆動によるメールのWhatsApp転送、Gmail、Outlook、Google Geminiの統合
Code
Gmail
Http Request
+
Code
Gmail
Http Request
22 ノードiTzJok3r
個人の生産性
AIを活用した会議調査とデイリーアジェンダ(Googleカレンダー、Attio CRM、Slack)
AIを活用した会議調査とデイリーアジェンダ:Googleカレンダー、Attio CRM、Slackを使用
If
Set
Code
+
If
Set
Code
30 ノードHarry Siggins
AI要約
競合他社コンテンツギャップ分析ツール:構題マッピングの自動化
Gemini AI、Apify、Google Sheetsを使用して競合企業のコンテンツギャップを分析
If
Set
Code
+
If
Set
Code
30 ノードMychel Garzon
その他