Tools: Build an AI Email Assistant with n8n and Telegram - Complete Guide

Tools: Build an AI Email Assistant with n8n and Telegram - Complete Guide

What you need

1. Getting your credentials

DeepSeek API

Telegram Bot

Getting your Chat ID

2. Building the workflow in n8n

Step by step:

3. Testing the workflow

4. Production tweaks

Filter by sender

Rate limiting

Conversation history

Handling attachments

Conclusion Is your inbox flooded with repetitive emails? Support queries, FAQs, information requests… they all eat up time and require manual replies. In this article you'll build an automated email assistant that: Diagram created with https://savnet.co Note: Telegram bots have no usage costs, making them ideal for automations with no message limits. Sign up at platform.deepseek.com, head to the API Keys section and create a new one. You'll need: Creating a Telegram bot is quick and requires no company registration or credit cards. Open Telegram, search for @BotFather, send /newbot and follow the steps. At the end you'll get a token like 4839574812:AAFD39kkdpWt3ywyRZergyOLMaJhac60qc. For more details on bot creation: Telegram Bot Creation Handbook You need to know your Chat ID (your Telegram user ID) so the bot knows where to send messages. Save both values (Bot Token and Chat ID) for later use in n8n. If you're new to n8n, the first step is creating a workflow. At each stage I'll tell you which node to add so you can find it like this: Node 1 — IMAP Trigger Add the Email Trigger (IMAP) node, which will be our workflow's trigger and will check for unread emails in our inbox. By default, once the workflow is published the node will poll periodically (every 60 seconds by default) to detect new emails. Node 2 — Code (Knowledge Base) The "secret sauce" of the assistant lives here. Instead of letting DeepSeek improvise, you define a clear context with the responses you expect. Add a Code node (Code in Javascript), switch the mode to "Run Once for All Items" (top-right of the editor) and connect it to Node 1 (IMAP). The code must preserve the email data we care about and add the knowledgeBase field. You can make this as detailed as you want. The more context, the better the responses. Using ...item.json preserves the original email data so Node 3 (content extraction) receives it without issues. Name this node, for example, CodeKnow. Node 3 — Code (Extract Content) Use another Code node (Code in Javascript) to extract the relevant information from each email and generate the chatInput with the prompt we'll send to our AI. Switch the mode to "Run for Each Item": Node 4 — DeepSeek Chat Model (Configuration) Add a DeepSeek Chat Model node. It will likely auto-generate 3 nodes: When chatmessage received, Basic LLM Chain and DeepSeek Chat Model. Keep only the LLM and DeepSeek — the extra one isn't needed. Connect the code block to the Basic LLM node like this: In the remaining blocks, set up the following: What we've done is send a prompt with our intent and the email data defined in the CodeProcessEmail block, along with a system message containing our knowledge base — the info we defined in the CodeKnow block. What about cost? DeepSeek v4 charges ~$0.14 per million input tokens and ~$0.28 per output. A typical email (~1,000 tokens) costs ~$0.0002. With 1,000 emails a month you'd spend ~$0.20. In the production tweaks section I'll show you how to filter to avoid unnecessary calls. The node takes the chatInput from Node 3 (knowledge base + email) and returns the response generated by DeepSeek. Node 5 — Code (Map Assistant Response) The DeepSeek block only returns the AI's raw response, which means we lose the full email data we'll need later. That's why we add this Code node in "Run Once for All Items" mode to merge the AI response with the original email data and determine whether it can reply or not: This block is key because: Node 6 — IF (Can it reply?) With the AI response and email data at hand, we add an if block. This node evaluates the {{ $json.canReply }} field with the following: Node 7.1 — Email (SMTP) — Reply Configure the Send Email node to send the automatic reply: Node 7.2 — Telegram — Send Notification For emails the AI can't handle, we won't send an automatic reply. Instead, you'll receive an alert via Telegram. However, we first need to make sure we're not sending messages longer than 4,000 characters — Telegram's limit. To handle this: That's it — our workflow is done :D ⚠️ Before going live, run controlled tests. Don't connect this workflow directly to your personal or company inbox. Use a test email account (IMAP + SMTP) that contains no sensitive or confidential information. If something goes wrong — a bad AI interpretation, a misconfigured domain — you could end up auto-replying to real customers with incorrect information. Better safe than sorry. With the workflow ready, do the following: When everything works as expected, just click Publish to enable polling and put it into production. Add an IF node after the trigger to only process emails from specific domains or addresses: DeepSeek has rate limits per minute. If you expect a high volume of emails, add a Wait node of 1-2 seconds between processing batches, or use a queue. If you want DeepSeek to remember previous conversations, you can store the history in a database (PostgreSQL, Redis) and pass it as additional context on each iteration. DeepSeek doesn't process files directly. If you receive emails with attachments, you can use a Code node to extract text from PDFs or images (via OCR) and pass it as context. You now have an automated email assistant that: This doesn't replace a support team, but it absorbs the volume of repetitive questions and frees up time for what really matters. If you want to visually document this workflow or more complex infrastructure diagrams, Savnet helps you design them. And if you need to continuously validate this kind of automation, SavFalconEye lets you keep confidence in every change. Ready to give it a try? Let me know how it went in the comments. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

Id: 1234567890 <<< This is your Chat ID First: YourName Lang: en .... Id: 1234567890 <<< This is your Chat ID First: YourName Lang: en .... Id: 1234567890 <<< This is your Chat ID First: YourName Lang: en .... const knowledgeBase = ` Eres un asistente de soporte técnico de [Tu Empresa]. Responde solo si encuentras una coincidencia clara en las políticas siguientes. Si no estás seguro, responde exactamente: NO_PUEDO_RESPONDER POLÍTICAS: 1. Horario de atención: Lunes a viernes de 9:00 a 18:00 (GMT-5). 2. Tiempo de respuesta estimado: 24 horas hábiles. 3. Contraseñas: Nunca solicites ni compartas contraseñas por correo. 4. Reembolsos: Se procesan dentro de los primeros 30 días. El usuario debe enviar un comprobante. 5. Problemas técnicos comunes: - Error 403: El usuario debe limpiar caché y cookies. - Error 500: Reportar al equipo interno, pedir al usuario que espere 1 hora. - Olvido de contraseña: Enviar enlace de restablecimiento a su correo registrado. 6. Facturación: Para cambios de plan o facturación, responder que el usuario ingrese al panel de pago. INSTRUCCIONES: - Responde de forma amable y profesional. - Usa el mismo idioma del correo recibido. - Si el correo contiene más de una pregunta, responde cada punto. - Si el correo es grosero o agresivo, responde: NO_PUEDO_RESPONDER `; const items = $input.all(); return items.map(item => ({ json: { ...item.json, knowledgeBase } })); const knowledgeBase = ` Eres un asistente de soporte técnico de [Tu Empresa]. Responde solo si encuentras una coincidencia clara en las políticas siguientes. Si no estás seguro, responde exactamente: NO_PUEDO_RESPONDER POLÍTICAS: 1. Horario de atención: Lunes a viernes de 9:00 a 18:00 (GMT-5). 2. Tiempo de respuesta estimado: 24 horas hábiles. 3. Contraseñas: Nunca solicites ni compartas contraseñas por correo. 4. Reembolsos: Se procesan dentro de los primeros 30 días. El usuario debe enviar un comprobante. 5. Problemas técnicos comunes: - Error 403: El usuario debe limpiar caché y cookies. - Error 500: Reportar al equipo interno, pedir al usuario que espere 1 hora. - Olvido de contraseña: Enviar enlace de restablecimiento a su correo registrado. 6. Facturación: Para cambios de plan o facturación, responder que el usuario ingrese al panel de pago. INSTRUCCIONES: - Responde de forma amable y profesional. - Usa el mismo idioma del correo recibido. - Si el correo contiene más de una pregunta, responde cada punto. - Si el correo es grosero o agresivo, responde: NO_PUEDO_RESPONDER `; const items = $input.all(); return items.map(item => ({ json: { ...item.json, knowledgeBase } })); const knowledgeBase = ` Eres un asistente de soporte técnico de [Tu Empresa]. Responde solo si encuentras una coincidencia clara en las políticas siguientes. Si no estás seguro, responde exactamente: NO_PUEDO_RESPONDER POLÍTICAS: 1. Horario de atención: Lunes a viernes de 9:00 a 18:00 (GMT-5). 2. Tiempo de respuesta estimado: 24 horas hábiles. 3. Contraseñas: Nunca solicites ni compartas contraseñas por correo. 4. Reembolsos: Se procesan dentro de los primeros 30 días. El usuario debe enviar un comprobante. 5. Problemas técnicos comunes: - Error 403: El usuario debe limpiar caché y cookies. - Error 500: Reportar al equipo interno, pedir al usuario que espere 1 hora. - Olvido de contraseña: Enviar enlace de restablecimiento a su correo registrado. 6. Facturación: Para cambios de plan o facturación, responder que el usuario ingrese al panel de pago. INSTRUCCIONES: - Responde de forma amable y profesional. - Usa el mismo idioma del correo recibido. - Si el correo contiene más de una pregunta, responde cada punto. - Si el correo es grosero o agresivo, responde: NO_PUEDO_RESPONDER `; const items = $input.all(); return items.map(item => ({ json: { ...item.json, knowledgeBase } })); const email = $input.item.json; const subject = email.subject || ''; const body = (email.textPlain && email.textPlain.trim()) || (email.textHtml && email.textHtml.trim()) || ''; return { subject, body: body.substring(0, 5000), from: email.from, to: email.to, uid: email.uid || email.id, chatInput: `From: ${email.from}\nAsunto: ${subject}\nCuerpo: ${body.substring(0, 5000)}` }; const email = $input.item.json; const subject = email.subject || ''; const body = (email.textPlain && email.textPlain.trim()) || (email.textHtml && email.textHtml.trim()) || ''; return { subject, body: body.substring(0, 5000), from: email.from, to: email.to, uid: email.uid || email.id, chatInput: `From: ${email.from}\nAsunto: ${subject}\nCuerpo: ${body.substring(0, 5000)}` }; const email = $input.item.json; const subject = email.subject || ''; const body = (email.textPlain && email.textPlain.trim()) || (email.textHtml && email.textHtml.trim()) || ''; return { subject, body: body.substring(0, 5000), from: email.from, to: email.to, uid: email.uid || email.id, chatInput: `From: ${email.from}\nAsunto: ${subject}\nCuerpo: ${body.substring(0, 5000)}` }; const aiItems = $input.all(); const emailItems = $('CodeProcessEmail').all(); const output = aiItems.map((item, index) => { const originalEmail = emailItems[index]?.json || {}; const aiText = item.json.text || ''; return { json: { ...originalEmail, aiResponse: aiText.trim(), canReply: !aiText.includes('NO_PUEDO_RESPONDER') } }; }); return output; const aiItems = $input.all(); const emailItems = $('CodeProcessEmail').all(); const output = aiItems.map((item, index) => { const originalEmail = emailItems[index]?.json || {}; const aiText = item.json.text || ''; return { json: { ...originalEmail, aiResponse: aiText.trim(), canReply: !aiText.includes('NO_PUEDO_RESPONDER') } }; }); return output; const aiItems = $input.all(); const emailItems = $('CodeProcessEmail').all(); const output = aiItems.map((item, index) => { const originalEmail = emailItems[index]?.json || {}; const aiText = item.json.text || ''; return { json: { ...originalEmail, aiResponse: aiText.trim(), canReply: !aiText.includes('NO_PUEDO_RESPONDER') } }; }); return output; const escapeHtml = (value) => { return String(value || '') .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); }; const items = $input.all(); let text = ''; items.forEach((item, index) => { const subject = escapeHtml(item.json.subject || 'Sin asunto'); const aiResponse = escapeHtml(item.json.aiResponse || ''); text += `${index + 1}) ${subject}: ${aiResponse}\n━━━━━━━━━━━━━━━━━━━━\n`; }); // ---- split into chunks ---- const chunkSize = 3900; const header = `━━━━━━━━━━━━━━━━━━━━\n<b>Recibiste los siguientes correos:</b>\n\n`; const fullText = header + text; const totalParts = Math.ceil(fullText.length / chunkSize) || 1; const output = []; for (let i = 0; i < fullText.length; i += chunkSize) { const part = Math.floor(i / chunkSize) + 1; output.push({ json: { text: `[${part}/${totalParts}]\n${fullText.slice(i, i + chunkSize)}` } }); } return output; const escapeHtml = (value) => { return String(value || '') .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); }; const items = $input.all(); let text = ''; items.forEach((item, index) => { const subject = escapeHtml(item.json.subject || 'Sin asunto'); const aiResponse = escapeHtml(item.json.aiResponse || ''); text += `${index + 1}) ${subject}: ${aiResponse}\n━━━━━━━━━━━━━━━━━━━━\n`; }); // ---- split into chunks ---- const chunkSize = 3900; const header = `━━━━━━━━━━━━━━━━━━━━\n<b>Recibiste los siguientes correos:</b>\n\n`; const fullText = header + text; const totalParts = Math.ceil(fullText.length / chunkSize) || 1; const output = []; for (let i = 0; i < fullText.length; i += chunkSize) { const part = Math.floor(i / chunkSize) + 1; output.push({ json: { text: `[${part}/${totalParts}]\n${fullText.slice(i, i + chunkSize)}` } }); } return output; const escapeHtml = (value) => { return String(value || '') .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); }; const items = $input.all(); let text = ''; items.forEach((item, index) => { const subject = escapeHtml(item.json.subject || 'Sin asunto'); const aiResponse = escapeHtml(item.json.aiResponse || ''); text += `${index + 1}) ${subject}: ${aiResponse}\n━━━━━━━━━━━━━━━━━━━━\n`; }); // ---- split into chunks ---- const chunkSize = 3900; const header = `━━━━━━━━━━━━━━━━━━━━\n<b>Recibiste los siguientes correos:</b>\n\n`; const fullText = header + text; const totalParts = Math.ceil(fullText.length / chunkSize) || 1; const output = []; for (let i = 0; i < fullText.length; i += chunkSize) { const part = Math.floor(i / chunkSize) + 1; output.push({ json: { text: `[${part}/${totalParts}]\n${fullText.slice(i, i + chunkSize)}` } }); } return output; const from = $input.item.json.from; const allowedDomains = ['yourdomain.com', 'client.com']; const domain = from.split('@')[1]; return allowedDomains.includes(domain); const from = $input.item.json.from; const allowedDomains = ['yourdomain.com', 'client.com']; const domain = from.split('@')[1]; return allowedDomains.includes(domain); const from = $input.item.json.from; const allowedDomains = ['yourdomain.com', 'client.com']; const domain = from.split('@')[1]; return allowedDomains.includes(domain); - Reads incoming emails via IMAP - Analyzes them with DeepSeek using a custom knowledge base - Replies automatically when the model has enough context - Notifies you via Telegram when it needs human review - A server with n8n up and running (if you don't have one, follow this installation guide) - An email account with IMAP access (ZohoMail, Gmail, Outlook, or any provider that supports it) - A DeepSeek API key (platform.deepseek.com) - A Telegram bot created via https://t.me/BotFather - DEEPSEEK_API_KEY - Endpoint URL: https://api.deepseek.com/v1/chat/completions - Recommended model: deepseek-chat - Open Telegram and search for @userinfobot - Start the conversation with Start - The bot replies automatically with your info. Look for the line that says Id: — that number is your Chat ID - In the Credential field, configure the following (values and setup vary by email provider): - User: your email - Password: your password or app password - Host: your IMAP server (e.g. imap.gmail.com) - Secure: true - In the rest of the form, fill in: - Mailbox Name: the mailbox to pull emails from, defaults to INBOX - Action: Mark as Read — this way every processed email gets marked as read. - In DeepSeek Chat Model, just configure the credentials: - Credential: select or create a DeepSeek account credential API Key: your DeepSeek key - API Key: your DeepSeek key - Model: deepseek-v4-flash - API Key: your DeepSeek key - In Basic LLM Chain (processor): - Source for Prompt: Define below - Prompt (User Message): Analyze this email and provide a short, clear response to send via Telegram. {{ $json.chatInput }} - Chat Messages: Type: System → Message: {{ $json.knowledgeBase }} - Type: System → Message: {{ $json.knowledgeBase }} - Require Specific Output Format: disabled - Type: System → Message: {{ $json.knowledgeBase }} - It grabs the original email data (subject, from, body, etc.) - It adds aiResponse with the clean response from DeepSeek - It adds canReply (true/false) to decide whether to auto-reply - Yes (true) → goes to SMTP send - No (false) → goes to Telegram notification - Credential: a window will open for you to register your SMTP credentials: User Password Host Port SSL - From Email: we'll tell it to reply from the same address they wrote to: {{ $json.to }} - To: the person who wrote in: {{ $json.from }} - Subject: Re: {{ $('CodeProcessEmail').item.json.subject }} - Email Format: HTML - Text: {{ $json.aiResponse }} - Add a Code node (JavaScript) in Run Once for All Items mode with the following code: - Add an HTTP Request node to send each of the messages we defined: - Method: POST - URL: https://api.telegram.org/bot{YourTelegramToken,format:0000:xxxxxx}/sendMessage - Authentication: none - SendBody: true - Body Content Type: JSON - Specify Body: Using Fields Below chat_id: your Telegram conversation ID text: {{ $json.text }} parse_mode: HTML disable_web_page_preview: TRUE - chat_id: your Telegram conversation ID - text: {{ $json.text }} - parse_mode: HTML - disable_web_page_preview: TRUE - chat_id: your Telegram conversation ID - text: {{ $json.text }} - parse_mode: HTML - disable_web_page_preview: TRUE - Run it manually by clicking the Execute Workflow button (top right). This triggers the workflow immediately without waiting for automatic polling. - Send yourself a test email with a question that's covered in your knowledge base. - Check that you receive the automatic reply in the test sender's inbox. - Send another email with something outside your knowledge base. - You should receive a Telegram notification with the email content. - Reads emails via IMAP without missing any - Analyzes them with DeepSeek against your own knowledge base - Replies automatically when it can - Notifies you via Telegram when it needs your input