{
  "id": "JMLklGiqNKJswtXO",
  "name": "AI support assistant via EmailConnect \u2014 match Notion KB, draft reply or escalate",
  "tags": [],
  "nodes": [
    {
      "id": "f02d973f-9ccf-40bb-adff-a8eaec7a5de9",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1776,
        128
      ],
      "parameters": {
        "width": 720,
        "height": 518,
        "content": "## AI support assistant via EmailConnect \u2014 match Notion KB, draft reply or escalate\n\n### How it works\n\nInbound support emails arrive through an EmailConnect webhook. Obvious spam is dropped by score. For everything else, the workflow loads your Q&A knowledge base from Notion, asks an LLM to pick the single closest matching entry (or return NO_MATCH below a confidence threshold), and then branches:\n\n- **Match found** \u2192 an LLM drafts a tailored reply using the KB answer and sends it to the customer.\n- **No confident match** \u2192 the question is escalated to your team by email, and the customer gets a friendly \"we're on it\" acknowledgement.\n\nBoth drafting agents keep short per-sender memory so follow-ups read naturally.\n\n### Setup steps\n\n- Point your EmailConnect alias webhook at the **When Email Received** trigger and verify it.\n- Connect Notion and set the database in **Get All KB Questions**. The Notion DB needs columns that surface as `question`, `answer`, and `help_url`.\n- Connect your LLM credential on the three chat-model nodes, and an SMTP credential on the three send-email nodes.\n- Set your own from-address, escalation inbox, and BCC in the email nodes.\n\n### Customization\n\nTune the spam score threshold, raise/lower the confidence bar in the **Find Best Match** prompt, or swap the escalation step for a ticket/Slack action."
      },
      "typeVersion": 1
    },
    {
      "id": "e6f4b1a8-77b9-4a0c-82b8-30c3f97bd7c3",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1008,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 424,
        "height": 438,
        "content": "## Receive & spam-gate\n\nStarts on the EmailConnect webhook and drops messages above the spam-score threshold before any AI runs."
      },
      "typeVersion": 1
    },
    {
      "id": "12f82423-a461-40f0-a825-43d28a3bb8b3",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 560,
        "content": "## Match against Notion KB\n\nLoads all Q&A rows from Notion, formats them, and asks the LLM to return the ID of the closest entry (or NO_MATCH below ~85% confidence). The result decides whether to answer or escalate."
      },
      "typeVersion": 1
    },
    {
      "id": "ed58cfda-b664-4ca2-9be4-0cdb2b3b339c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 620,
        "height": 556,
        "content": "## Find KB match for received e-mail\n\nHave an LLM evaluate the original question versus the Notion articles found and calculate a confidence for each. Then have it indicate whether NO_MATCH was found, it came up with a hallucinated index or found a genuine answer."
      },
      "typeVersion": 1
    },
    {
      "id": "fae0e83d-77ca-4492-9568-8ca725817255",
      "name": "When Email Received",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -960,
        384
      ],
      "parameters": {
        "path": "emailconnect-inbound",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "d93c2bc8-ad71-4665-93da-4cfcc28c1962",
      "name": "Check if spam",
      "type": "n8n-nodes-base.if",
      "position": [
        -752,
        384
      ],
      "parameters": {
        "options": {
          "ignoreCase": false
        },
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "32329838-35bf-4de1-ae3d-58816e992f43",
              "operator": {
                "type": "boolean",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.spam.score }}",
              "rightValue": ""
            },
            {
              "id": "3551762c-fad0-4305-838f-6e1c54a59d98",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.body.spam.score }}",
              "rightValue": 10
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "87b14197-a8fd-4522-b67c-bbb0114926b5",
      "name": "Stop processing",
      "type": "n8n-nodes-base.noOp",
      "position": [
        -464,
        288
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "5c0576a7-9fec-44ca-a413-5d6cd80bb86a",
      "name": "Get All KB Questions",
      "type": "n8n-nodes-base.notion",
      "position": [
        -464,
        496
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "operation": "getAll",
        "returnAll": true,
        "databaseId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_NOTION_DATABASE_ID",
          "__regex": "^([0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12})"
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "7adf39c5-d34c-4d6c-b3fa-6a73a61c030f",
      "name": "Format KB data",
      "type": "n8n-nodes-base.code",
      "position": [
        -272,
        496
      ],
      "parameters": {
        "jsCode": "// Extract all Q&A pairs from Notion KB\nconst kbData = $('Get All KB Questions').all().map((item, index) => {\n  return {\n    id: index,\n    question: item.json.property_question || 'No question',\n    answer: item.json.property_answer || 'No answer',\n    help_article: item.json.property_help_url || ''\n  };\n});\n\n// Format for similarity comparison - include both question and context\nconst questionsForComparison = kbData.map(item =>\n  `ID:${item.id} | Q: ${item.question} | Context: ${item.answer.substring(0, 120)}`\n).join('\\n');\n\n// Robust email data extraction - handles both wrapped and unwrapped payloads\nconst emailData = $('When Email Received').first().json;\nconst message = emailData.body?.message || emailData.message;\n\nreturn [{\n  json: {\n    kbData: kbData,\n    questionsText: questionsForComparison,\n    userName: message.sender.name,\n    userEmail: message.sender.email,\n    userQuestion: message.content.text,\n    totalQuestions: kbData.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e8f29f1b-3b2b-44e1-b30d-c4922c536d1e",
      "name": "Find Best Match",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        112,
        288
      ],
      "parameters": {
        "text": "=You are an expert at finding semantic similarity between questions. \n\n# Your task\nYou will be given a user's question and a list of questions from our knowledge base. Your job is to find the question that is most semantically similar to what the user is asking.\n\nUSER QUESTION: {{ $json.userQuestion }}\n\nAVAILABLE KB QUESTIONS:\n{{ $json.questionsText }}\n\nAnalyze the user's question and find the most relevant KB entry. Consider:\n- Exact keyword matches\n- Semantic similarity \n- Intent similarity\n- Context clues\n\nRespond with ONLY the ID number of the best matching question. If no good match exists (confidence < 85%), respond with \"NO_MATCH\".\n\nBest match ID:",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 2
    },
    {
      "id": "c4b438a9-7925-4cef-bb7f-0b74ed2ddcf8",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        64,
        512
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "8900f109-28a1-455f-8978-600f8f844473",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        704,
        288
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "3551762c-fad0-4305-838f-6e1c54a59d98",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.escalate }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6ef3a250-8a47-40b6-bb31-3a0e1ddcbb46",
      "name": "Draft escalation",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        944,
        480
      ],
      "parameters": {
        "text": "=We've received a support request email. You are a supportive, friendly customer success manager, tasked with responding to their question.\n\n# Details\n## Sender\nName: {{ $('Format KB data').item.json.userName }}\nEmail: {{ $('Format KB data').item.json.userEmail }}\n\n## Original e-mail message\n{{ $('Format KB data').item.json.userQuestion }}\n\n## Result\nAs we don't have a direct answer for sender, we've escalated the question for further follow up.\n\n# Task\nDraft an e-mail to the sender that informs them that their questions has been escalated. Explain that for any legitimate e-mail, they can expect a response within a business day. **IF** however, their message is commercial or promotional, explain that that response might not come.\n\n# Guidelines\nReview the user's original e-mail message and make sure you:\n- Check for recent conversations from the same sender ({{ $('Format KB data').item.json.userEmail }}) in the attached memory tool to refer to if relevant.\n- Meet their tone, e.g. formal vs informal \n- Meet their depth, e.g. to the point vs detail\n- Be professional and concise, yet warm",
        "options": {
          "systemMessage": "- Write in normal English sentence case. Do not capitalize every noun. Use standard capitalization rules: sentences begin with a capital, and proper nouns are capitalized.\n- Close your message by picking any gender neutral first name for yourself, and identifying yourself as a customer success agent."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 2
    },
    {
      "id": "cd7935bb-f12f-454a-9543-a0ad63a8d5b3",
      "name": "OpenRouter Chat Model2",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        880,
        704
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "086f66f8-8ff3-47b5-9d03-86546b713019",
      "name": "Simple Memory1",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        1040,
        704
      ],
      "parameters": {
        "sessionKey": "={{ $('Format KB data').item.json.userEmail }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "88134f40-fa39-43e9-916b-c02540e8545e",
      "name": "Structured Output Parser1",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1200,
        704
      ],
      "parameters": {
        "jsonSchemaExample": "{\n\t\"subject\": \"Email subject\",\n\t\"message\": \"Hi there, thanks for reaching out!\"\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "50b197c4-6afe-4046-ae8b-580815feebcf",
      "name": "Draft answer",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        928,
        944
      ],
      "parameters": {
        "text": "=We've received a support request email. You are a supportive, friendly customer success manager, tasked with responding to their question.\n\n# Details\n## Sender\nName: {{ $('Format KB data').item.json.userName }}\nEmail: {{ $('Format KB data').item.json.userEmail }}\n\n## Original e-mail message\n{{ $json.userQuestion }}\n\n## Matched answer in our KB\n{{ $('Get answer').item.json.matchedAnswer }}\n\n## Further reading (include in reply only if relevant and present)\n{{ $('Get answer').item.json.matchedHelpUrl }}\n\n# Task\nDraft an e-mail to the sender that incorporates the answer found in the KB to address their question. \n\n# Guidelines\nReview the user's original e-mail message for their question and make sure you:\n- Tailor the KBs answer to their specific question and remove fluf. Be to the point. \n- Check recent conversations from the same sender ({{ $('Format KB data').item.json.userEmail }}) in the attached memory tool to refer to if relevant.\n- Meet their tone, e.g. formal vs informal \n- Meet their depth, e.g. to the point vs detail\n- Be professional and concise, yet warm",
        "options": {
          "systemMessage": "- Write in normal English sentence case. Do not capitalize every noun. Use standard capitalization rules: sentences begin with a capital, and proper nouns are capitalized.\n- Close your message by picking any gender neutral first name for yourself, and identifying yourself as a customer success agent."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 2
    },
    {
      "id": "04aeb3ad-3b42-4aad-89af-5f8a6401555e",
      "name": "OpenRouter Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        864,
        1184
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "f754fdea-ace4-4813-827a-ca14281c6e87",
      "name": "Simple Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        1024,
        1184
      ],
      "parameters": {
        "sessionKey": "={{ $('Format KB data').item.json.userEmail }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 8
      },
      "typeVersion": 1.3
    },
    {
      "id": "0167af9c-f4ad-4518-afda-ff99afe1fb5b",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1184,
        1184
      ],
      "parameters": {
        "jsonSchemaExample": "{\n\t\"subject\": \"Email subject\",\n\t\"message\": \"Hi there, thanks for reaching out!\"\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "bd3be683-89c0-4e03-8410-c93dba8057bf",
      "name": "Simple Memory3",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        256,
        512
      ],
      "parameters": {
        "sessionKey": "={{ $('Format KB data').item.json.userEmail }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 8
      },
      "typeVersion": 1.3
    },
    {
      "id": "1a68258f-e729-4bb3-8507-97d5e49a3ef9",
      "name": "Get answer",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        288
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json.output?.toString().trim() ?? '';\nconst matchId = /^NO_MATCH$/i.test(raw) ? 'NO_MATCH' : (raw.match(/\\d+/) || ['NO_MATCH'])[0];\nconst kbData = $('Format KB data').first().json.kbData\nconst userEmail = $('Format KB data').first().json.userEmail\nconst userQuestion = $('Format KB data').first().json.userQuestion\n\n// If LLM concluded there was no answer in the KB.\nif (matchId === \"NO_MATCH\") {\n  return [{\n    json: {\n      found: false,\n      escalate: true,\n      message: \"ESCALATE_TO_HUMAN\"\n    }\n  }];\n}\n\n/**\n * A defensive escalation against a bad/hallucinated index. This  \n * makes sure the index exists.\n */ \nconst matchedEntry = kbData[parseInt(matchId)];\nif (!matchedEntry) {\n  return [{\n    json: {\n      found: false,\n      escalate: true, \n      message: \"ESCALATE_TO_HUMAN\"\n    }\n  }];\n}\n\n// If the LLM found an answer with high confidence.\nreturn [{\n  json: {\n    found: true,\n    escalate: false,\n    matchedQuestion: matchedEntry.question,\n    matchedAnswer: matchedEntry.answer,\n    matchedHelpUrl: matchedEntry.help_article,\n    userEmail: userEmail,\n    userQuestion: userQuestion,\n    confidence: \"high\"\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "48b63650-6f4b-4ba2-8666-cc5a6d09af39",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 940,
        "height": 1228,
        "content": "## Draft reply or escalate\n\n**Top branch \u2014 escalate:** notify your team and send the customer an acknowledgement.\n\n**Bottom branch \u2014 answer:** draft a reply grounded in the matched KB answer and send it.\n\nBoth drafting agents use per-sender memory keyed on the customer's email."
      },
      "typeVersion": 1
    },
    {
      "id": "86cb94f8-1e1a-4598-a8d7-f2c295f05c16",
      "name": "Send escalation to human",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        1328,
        272
      ],
      "parameters": {
        "text": "=A question was raised, currently not in the KB. Please review, and answer the user directly.\n\nDetails as per below.\n\nFrom: {{ $('Format KB data').item.json.userName }}\nEmail: {{ $('Format KB data').item.json.userEmail }}\nMessage: {{ $('Format KB data').item.json.userQuestion }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "[!] Escalated question",
        "toEmail": "user@example.com",
        "fromEmail": "Support <support@example.com>",
        "emailFormat": "text"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "56ad2b0e-8b8f-4bb3-9438-5c6d48d92b41",
      "name": "Send answer email to sender",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        1328,
        944
      ],
      "parameters": {
        "text": "={{ $json.output.message }}",
        "options": {
          "bccEmail": "user@example.com",
          "appendAttribution": false
        },
        "subject": "={{ $json.output.subject }}",
        "toEmail": "={{ $('Format KB data').item.json.userName }} <{{ $('Format KB data').item.json.userEmail }}>",
        "fromEmail": "Support <support@example.com>",
        "emailFormat": "text"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "3d4599aa-8296-46e2-8fe4-ce57637a43b1",
      "name": "Inform need for escalation to sender",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        1328,
        480
      ],
      "parameters": {
        "text": "={{ $json.output.message }}",
        "options": {
          "bccEmail": "user@example.com",
          "appendAttribution": false
        },
        "subject": "={{ $json.output.subject }}",
        "toEmail": "={{ $('Format KB data').item.json.userName }} <{{ $('Format KB data').item.json.userEmail }}>",
        "fromEmail": "Support <support@example.com>",
        "emailFormat": "text"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "be48d42a-46f0-4a87-8f58-f47393cb98dc",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Send escalation to human",
            "type": "main",
            "index": 0
          },
          {
            "node": "Draft escalation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Draft answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get answer": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft answer": {
      "main": [
        [
          {
            "node": "Send answer email to sender",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if spam": {
      "main": [
        [
          {
            "node": "Stop processing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get All KB Questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "Draft answer",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Format KB data": {
      "main": [
        [
          {
            "node": "Find Best Match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory1": {
      "ai_memory": [
        [
          {
            "node": "Draft escalation",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory3": {
      "ai_memory": [
        [
          {
            "node": "Find Best Match",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Find Best Match": {
      "main": [
        [
          {
            "node": "Get answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft escalation": {
      "main": [
        [
          {
            "node": "Inform need for escalation to sender",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Email Received": {
      "main": [
        [
          {
            "node": "Check if spam",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All KB Questions": {
      "main": [
        [
          {
            "node": "Format KB data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Find Best Match",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Draft answer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Draft escalation",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Draft answer",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser1": {
      "ai_outputParser": [
        [
          {
            "node": "Draft escalation",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  }
}