AutomationFlowsAI & RAG › AI Email Reply Based on Hubspot Data + Slack Approval

AI Email Reply Based on Hubspot Data + Slack Approval

ByMiha @miha on n8n.io

This n8n template drafts customer-ready email replies using Google Gemini, enriched with HubSpot context (contact, deals, companies, tickets). Each draft is routed to Slack for one-click approval before it’s sent from Gmail—so you move fast without losing control.

Event trigger★★★★☆ complexityAI-powered18 nodesGoogle Gemini ChatGmailGmail TriggerAgentHubSpotHTTP RequestSlack
AI & RAG Trigger: Event Nodes: 18 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #8967 — we link there as the canonical source.

This workflow follows the Agent → Gmail recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "tpXEt8k7YAJ2EC64",
  "name": "AI email reply based on HubSpot data + Slack approval",
  "tags": [],
  "nodes": [
    {
      "id": "6897a614-cd5a-4e76-99fb-7094b4692dd2",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1056,
        928
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "1b011a66-c09a-4a6c-b666-89a5301f54cc",
      "name": "Reply to a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1888,
        704
      ],
      "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": "Filter: 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 Agent)",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        992,
        720
      ],
      "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\u20132 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\u20132 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\u2019s name if available.\n- 2\u20135 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 Email",
      "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": "Set 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"
      },
      "credentials": {
        "hubspotOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "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": [
        2112,
        352
      ],
      "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": "Wait for Response - Approve Auto-Reply",
      "type": "n8n-nodes-base.slack",
      "position": [
        1472,
        720
      ],
      "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"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "464de728-acf5-4664-9469-f4f660d29ec6",
      "name": "If Approved?",
      "type": "n8n-nodes-base.if",
      "position": [
        1680,
        720
      ],
      "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": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 1328,
        "height": 384,
        "content": "## Get CRM information\nFetch contact info and associated deals, tickets and compaines."
      },
      "typeVersion": 1
    },
    {
      "id": "bb2fb3e1-dbcc-468f-9d0b-65e379aad792",
      "name": "Sticky Note1",
      "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": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        576
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 528,
        "content": "## Write draft response"
      },
      "typeVersion": 1
    },
    {
      "id": "39b501df-7fc7-470f-8aa4-6dacc05eb255",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1392,
        576
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 384,
        "content": "## Wait for You to Approve in Slack, and reply in Gmail"
      },
      "typeVersion": 1
    },
    {
      "id": "86267d6e-aa6e-4521-a0fb-c823724b7e7d",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -32
      ],
      "parameters": {
        "color": 5,
        "width": 468,
        "height": 656,
        "content": "## AI email reply with HubSpot context + Slack approval\n\n### How it works\n1. A new Gmail email triggers the workflow.\n2. The sender\u2019s HubSpot info (deals, companies, tickets) is fetched.\n3. Gemini drafts a personalized reply.\n4.The draft appears in Slack for quick review and approval.\n5. After approval, the reply is sent automatically.\n\n### Setup\n- [ ] **Gmail:** Use the same account for the trigger and the send nodes.\n- [ ] **HubSpot:** Connect all the HubSpot nodes.\n- [ ] **Slack:** Connect Slack and choose where to send the draft for approval.\n- [ ] **Gemini:** Add your [Google AI Studio](https://aistudio.google.com/) API key\n- [ ] **Filter:** Tweak or remove the sender rule before going live.\n\n### Customize\n- **Prompt:** Adjust tone, length, and how much CRM detail to include.\n- **Fields:** Pick which deal/company/ticket properties to pull.\n- **Approval:** Skip Slack to auto-send, or add extra reviewers if needed.\n- **Filter** for specific Gmail tags or emails"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5c480d09-d63d-428d-b2e4-d8c75f211c07",
  "connections": {
    "If Approved?": {
      "main": [
        [
          {
            "node": "Reply to a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Record Types": {
      "main": [
        [
          {
            "node": "List Contact Associations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Batch Read Objects": {
      "main": [
        [
          {
            "node": "Normalize CRM Context for LLM",
            "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
          }
        ]
      ]
    },
    "Build Batch Read Requests": {
      "main": [
        [
          {
            "node": "Batch Read Objects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Contact Associations": {
      "main": [
        [
          {
            "node": "Build Batch Read Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Watch Gmail (New Inbound)": {
      "main": [
        [
          {
            "node": "Filter: Allowed Sender",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize CRM Context for LLM": {
      "main": [
        [
          {
            "node": "Draft Reply (AI Agent)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Response - Approve Auto-Reply": {
      "main": [
        [
          {
            "node": "If Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This n8n template drafts customer-ready email replies using Google Gemini, enriched with HubSpot context (contact, deals, companies, tickets). Each draft is routed to Slack for one-click approval before it’s sent from Gmail—so you move fast without losing control.

Source: https://n8n.io/workflows/8967/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

CV → Match → Screen → Decide, all automated

HTTP Request, Information Extractor, Google Sheets +7
AI & RAG

Streamline your HR recruitment process with this intelligent automation that reads candidate emails and resumes, analyzes them using GPT-4, and automatically shortlists or rejects applicants based on

Gmail, Gmail Trigger, HTTP Request +7
AI & RAG

Intelligent email-to-WhatsApp automation that monitors Gmail and Outlook accounts, uses Google Gemini AI to filter important emails, and forwards them to WhatsApp via Evolution API. Multi-account supp

Gmail, Agent, Google Gemini Chat +4
AI & RAG

This template is designed for healthcare providers, sales reps, and medical tourism companies who need to process diagnosis emails efficiently. It automates the full flow from email to report delivery

Gmail Trigger, Google Sheets, Agent +5
AI & RAG

Stop drowning in job applications. This workflow transforms your hiring process from a manual, time-consuming data-entry task into an automated, intelligent screening system.

Jot Form Trigger, Google Gemini Chat, Output Parser Structured +5