Your inbox is a time sink. The average professional spends 28% of their workday reading and responding to email — that is roughly 2.5 hours every single day. Most of those emails fall into predictable categories: support questions, sales inquiries, scheduling requests, and spam. What if an AI running entirely on your own machine could handle the triage and draft responses for you?
In this tutorial, you will build a complete email auto-response system using n8n (open-source workflow automation) and Ollama (local AI model runner). No API keys, no monthly fees, no data leaving your network. The entire pipeline — from reading incoming mail to drafting context-aware replies — runs on your hardware for free.
You could use OpenAI or Claude for email automation, but there are strong reasons to keep this workflow self-hosted:
Before diving into the build, here is how all the pieces fit together:
IMAP Inbox
|
v
[n8n: Email Trigger] -- polls every 5 minutes
|
v
[n8n: HTTP Request to Ollama] -- classifies email
|
v
[n8n: Code Node] -- parses AI JSON response
|
v
[n8n: Switch Node] -- routes by category
|
+--> support --> [Ollama: draft support reply] --> [Send/Save draft]
+--> sales --> [Ollama: draft sales reply] --> [Send/Save draft]
+--> meeting --> [Ollama: draft scheduling reply] --> [Send/Save draft]
+--> spam --> [Archive / Delete]
+--> other --> [Forward to team / Flag for review]
The workflow has two AI calls per email: one for classification and one for drafting the reply. On an 8B parameter model, each call takes 10-30 seconds depending on your hardware. That means processing a batch of 20 emails takes roughly 5-10 minutes — well within the polling interval.
If you already have n8n and Ollama running, skip ahead to Step 1. Otherwise, here is the quick setup.
1 Install Ollama and pull a model
# Install Ollama (Linux/macOS)
curl -fsSL https://ollama.ai/install.sh | sh
# Pull the recommended model
ollama pull llama3:8b
# Verify it is running
curl http://localhost:11434/api/tags
You should see a JSON response listing your installed models. If you get a connection error, run ollama serve in a separate terminal.
2 Install n8n via Docker
docker run -d --name n8n \
-p 5678:5678 \
-v n8n_data:/home/node/.n8n \
--add-host=host.docker.internal:host-gateway \
n8nio/n8n
The --add-host flag is critical — it lets the Docker container reach Ollama on your host machine. Open http://localhost:5678 and create your account.
3 Test the connection
In n8n, create a new workflow. Add an HTTP Request node with URL http://host.docker.internal:11434/api/tags and method GET. Execute it. If you see your model list, the connection is working.
Add an IMAP Email trigger node to your workflow. This node polls your inbox at a set interval and passes new emails into the pipeline.
Mailbox: INBOX
Credentials:
Host: imap.gmail.com (or your provider)
Port: 993
User: your-email@gmail.com
Password: your-app-password (NOT your regular password)
SSL/TLS: true
Options:
Poll interval: 5 minutes
Mark as read: true
Download attachments: false
For testing, you can use a Manual Trigger followed by a Set node to simulate incoming emails:
{
"from": "sarah@clientcorp.com",
"subject": "Urgent: Production server down",
"body": "Hi team, our production environment went down about 30 minutes ago. Customers are reporting 500 errors across all API endpoints. We need immediate assistance. This is blocking our launch scheduled for tomorrow morning. Please escalate ASAP.",
"date": "2026-03-24T14:30:00Z"
}
This is where the AI magic happens. Add an HTTP Request node that sends each email to Ollama for classification.
The quality of your classification depends almost entirely on the prompt. Here is one that has been tested across thousands of emails:
URL: http://host.docker.internal:11434/api/generate
Method: POST
Body Type: JSON
Body:
{
"model": "llama3:8b",
"prompt": "You are an email classification assistant. Analyze the following email and return a JSON object with your analysis.\n\nCategories (pick exactly one):\n- support: Technical issues, bugs, account problems, how-to questions\n- sales: Pricing inquiries, purchase interest, demo requests, partnership proposals\n- meeting: Scheduling requests, calendar invites, availability questions\n- feedback: Product feedback, feature requests, complaints, reviews\n- newsletter: Marketing emails, newsletters, promotional content\n- spam: Unsolicited ads, phishing attempts, irrelevant mass mail\n- internal: Team communication, project updates, HR matters\n\nUrgency levels:\n- critical: System down, data loss, security incident, deadline today\n- high: Blocking issue, important client, time-sensitive (24-48h)\n- medium: Standard request, normal business timeline\n- low: FYI, newsletter, non-urgent feedback\n\nSentiment:\n- positive, neutral, negative, angry\n\nEmail:\nFrom: {{ $json.from }}\nSubject: {{ $json.subject }}\nBody: {{ $json.body }}\n\nReturn ONLY valid JSON, no other text:\n{\"category\": \"...\", \"urgency\": \"...\", \"sentiment\": \"...\", \"summary\": \"one sentence summary\", \"requires_reply\": true/false}",
"stream": false,
"options": {
"temperature": 0.1,
"num_predict": 200
}
}
Key decisions in this prompt:
Add a Code node after the HTTP Request to extract the JSON from Ollama's response:
// Parse the classification from Ollama's response
const aiResponse = $input.first().json.response;
// Extract JSON from the response (handles cases where the model
// adds extra text before/after the JSON)
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
// Fallback: if AI didn't return valid JSON, flag for manual review
return [{
json: {
category: 'other',
urgency: 'medium',
sentiment: 'neutral',
summary: 'Could not auto-classify',
requires_reply: false,
parse_error: true,
original_email: $('Set').first().json
}
}];
}
const classification = JSON.parse(jsonMatch[0]);
// Merge classification with original email data
return [{
json: {
...classification,
original_email: $('Set').first().json // Replace 'Set' with your trigger node name
}
}];
Add a Switch node that routes emails based on the classification. Configure these routing rules:
Switch on: {{ $json.category }}
Routes:
"support" --> Draft Support Reply
"sales" --> Draft Sales Reply
"meeting" --> Draft Scheduling Reply
"feedback" --> Log to Spreadsheet + Draft Thank-You
"spam" --> Archive/Delete (no reply)
"newsletter" --> Archive (no reply)
"internal" --> Forward to Slack channel
Default --> Flag for manual review
For critical-urgency emails, add a second routing layer. Before the Switch node, add an IF node:
Condition: {{ $json.urgency }} equals "critical"
True --> Send Slack/SMS alert immediately + Continue to normal routing
False --> Continue to normal routing
This way, a critical support email both triggers an immediate notification and gets a drafted reply.
For each category that requires a response, add another HTTP Request node to Ollama. The key is tailoring the system prompt to each category so the AI generates appropriately toned replies.
{
"model": "llama3:8b",
"prompt": "You are a helpful customer support agent. Draft a professional reply to this support email.\n\nContext:\n- Category: {{ $json.category }}\n- Urgency: {{ $json.urgency }}\n- Sentiment: {{ $json.sentiment }}\n\nOriginal email from {{ $json.original_email.from }}:\nSubject: {{ $json.original_email.subject }}\nBody: {{ $json.original_email.body }}\n\nGuidelines:\n1. Acknowledge the issue specifically (don't be generic)\n2. If the customer sounds frustrated, show empathy first\n3. Provide concrete next steps or troubleshooting instructions\n4. For critical urgency: mention that the team has been alerted and is actively investigating\n5. Keep the reply under 150 words\n6. Sign off as 'Support Team'\n\nWrite ONLY the reply body. No subject line.",
"stream": false,
"options": {
"temperature": 0.5,
"num_predict": 500
}
}
{
"model": "llama3:8b",
"prompt": "You are a friendly sales representative. Draft a reply to this sales inquiry.\n\nOriginal email from {{ $json.original_email.from }}:\nSubject: {{ $json.original_email.subject }}\nBody: {{ $json.original_email.body }}\n\nGuidelines:\n1. Thank them for their interest\n2. Address their specific question or need\n3. Include a soft call to action (schedule a call, try a demo, etc.)\n4. Be warm but concise - under 120 words\n5. Sign off as 'Sales Team'\n\nWrite ONLY the reply body.",
"stream": false,
"options": {
"temperature": 0.6,
"num_predict": 400
}
}
{
"model": "llama3:8b",
"prompt": "Draft a brief scheduling reply to this email.\n\nFrom: {{ $json.original_email.from }}\nSubject: {{ $json.original_email.subject }}\nBody: {{ $json.original_email.body }}\n\nGuidelines:\n1. Confirm interest in meeting\n2. Suggest 2-3 time slots (use placeholder times like [Time Slot 1], [Time Slot 2])\n3. Ask about preferred meeting format (video call, phone, in-person)\n4. Keep it under 80 words\n\nWrite ONLY the reply body.",
"stream": false,
"options": {
"temperature": 0.4,
"num_predict": 300
}
}
Notice the different temperatures: support replies (0.5) need some flexibility for empathetic language, sales replies (0.6) benefit from slightly more creative tone, and scheduling replies (0.4) should be more formulaic and predictable.
Not every email needs a reply. For emails classified as spam or newsletter, skip the reply-drafting step entirely. You can:
For the feedback category, log the feedback to a spreadsheet and send a brief thank-you reply. This creates a searchable feedback database without manual effort:
// Code node: structure feedback for logging
return [{
json: {
date: $json.original_email.date,
from: $json.original_email.from,
subject: $json.original_email.subject,
sentiment: $json.sentiment,
summary: $json.summary,
full_text: $json.original_email.body
}
}];
After testing this workflow across hundreds of real-world emails, here are the patterns that produce the most reliable results:
Always end classification prompts with "Return ONLY valid JSON, no other text." Without this, models frequently add explanatory text around the JSON, which breaks parsing.
Do not just list categories. Briefly describe what each one means. "support: Technical issues, bugs, account problems" gives the model much better classification accuracy than just "support."
Without explicit limits, the AI tends to write 300-500 word replies that feel robotic and over-detailed. "Keep the reply under 150 words" forces concise, natural-sounding responses.
Passing the detected sentiment to the reply-drafting prompt lets the AI adjust its tone. A reply to an "angry" customer leads with empathy; a reply to a "positive" email matches the upbeat tone.
0.1 for classification — deterministic and consistent0.4-0.6 for drafting replies — some natural variation without going off-script0.7 for business email — higher values introduce too much randomnessBefore letting this workflow handle your real inbox, run through these checks:
# Systemd service for Ollama (save to /etc/systemd/system/ollama.service)
[Unit]
Description=Ollama LLM Service
After=network.target
[Service]
ExecStart=/usr/local/bin/ollama serve
Restart=always
RestartSec=5
Environment=OLLAMA_HOST=0.0.0.0
[Install]
WantedBy=multi-user.target
mistral:7b instead of llama3:8b for the classification step. Mistral is faster for short, structured output tasks and uses less RAM, freeing resources for the reply-drafting step where Llama 3 excels.
Email automation is just one piece of the puzzle. If you found this tutorial useful, these related workflows extend the concept:
Building each of these from scratch takes hours of prompt engineering, error handling, and testing. If you want production-ready versions that work out of the box, we have built all of them (and 7 more) as importable n8n templates.
The Self-Hosted AI Workflow Pack includes the email auto-responder from this tutorial plus 10 more production-ready n8n + Ollama workflows: content generation, lead scoring, document processing, competitor monitoring, and more.
$39 one-time — no subscriptions, no API costs, 30-day money-back guarantee
Get the Workflow Pack →Want to try a workflow before buying? Grab these free, open-source templates:
Published by WorkflowForge · Self-Hosted AI Workflow Pack