Automating Email Categorization with n8n and Self-Hosted AI: A Journey in Prompt Engineering

From "simple" idea to working automation in 3 prompt engineering iterations 📈 Attempt 1: AI writes essays about emails 📝 Attempt 2: AI wraps JSON in markdown 🙄 Attempt 3: Perfect single-line JSON! ✨ Sometimes the hardest part isn't the tech—it's teaching AI to follow instructions exactly.

Automating Email Categorization with n8n and Self-Hosted AI: A Journey in Prompt Engineering
Photo by Geoffrey Chevtchenko / Unsplash

I've recently discovered n8n, a powerful self-hosted tool for creating automation workflows. I decided to set up a flow that triggers every time I receive an email—it extracts the email content, sends it to my self-hosted AI, and generates a JSON snippet containing one of my predefined tags. Sounds simple, right?

Wrong! Let me walk you through the challenges I encountered and how I solved them.

Choosing the Right Model

When working with AI, your first decision is selecting the model. I chose Gemma3:1b in Ollama. Gemma is Google's streamlined version of Gemini that can run locally on your own hardware. The "1B" indicates it has 1 billion parameters.

Here's the trade-off: more parameters generally mean better performance, but they also require more memory and processing power. Since I'm running this on a CPU-only machine with 32GB of RAM, I needed to balance capability with resource constraints. The 1B model is about 1GB in size, fits comfortably in memory, and runs quickly on my CPU.

The Prompt Engineering Challenge

The second crucial component is the prompt—the instructions you give the AI. Think of it as a more specific version of what you type into ChatGPT, but with precise requirements like "generate only JSON."

Here was my first attempt:

You are an email categorization assistant. Analyze the email and categorize it into exactly ONE of these categories:

CATEGORIES:
- work: Work-related emails, meetings, projects, colleagues
- personal: Friends, family, personal matters
- shopping: Orders, receipts, shipping, e-commerce
- finance: Bills, banking, payments, invoices, taxes
- newsletters: Newsletters, blogs, marketing emails
- social: Social media notifications, forums
- travel: Flights, hotels, bookings, travel confirmations
- action: Emails requiring immediate response or action
- other: Anything that doesn't clearly fit above

RULES:
1. Return ONLY a valid JSON object 
2. Use lowercase category names exactly as listed 
3. Provide a brief reason (max 50 characters) 
4. Be decisive - pick the best fit even if uncertain  

FORMAT: {"category": "work", "reason": "Meeting invitation from colleague"}

The instructions seemed clear: I wanted ONLY a valid JSON object. But here's what I got:

This email is a **Delivery Notification** from bpost.

Here's a breakdown of why:

* **Subject:** Clearly indicates the purpose of the email – notifying about a delivery.
* **Content:** It details the parcel being delivered, the delivery preferences of the recipient, and the expected delivery process.
* **Attachments:** The inclusion of images (logo, transparent, icon) suggests a visual communication element related to the delivery

Perfect analysis, but completely wrong format! Time for prompt engineering round two:

You are an email categorizer.
You must respond with ONLY valid JSON, nothing else.

Categories: work, personal, shopping, finance, newsletters, social, travel, action, other

Format: {"category": "work", "reason": "brief explanation"}

Rules:
- Return ONLY the JSON object
- No explanations before or after
- No markdown formatting
- Use lowercase category names exactly as listed

I made the JSON requirement even more explicit. The result:

Here's a JSON representation of the email content, formatted for easy parsing:

```json
{
  "subject": "*** has chosen bpost for the delivery of your parcel",
  "from": "bpost",
  "body": "If you aren't in, we will deliver according to your delivery preferences..."
}

Closer, but still wrapped in explanatory text and markdown formatting! My automation couldn't parse this. Back to the drawing board for attempt three:

CRITICAL: You MUST respond with ONLY a single line of valid JSON. No other text allowed.

Categories: work, personal, shopping, finance, newsletters, social, travel, action, other

Required format: {"category":"shopping","reason":"delivery notification"}

FORBIDDEN: explanations, markdown, backticks, formatting, extra text REQUIRED: exactly one JSON object on one line

Email content: {{ $json.cleanContent.subject }} {{ $json.cleanContent.body }}

Finally! The output:

{"category":"action","reason":"delivery notification"}

Perfect! Clean, parseable JSON with no extra fluff.

An Even Better Discovery

While writing this post, I discovered there's an even smaller model: Gemma3:270M. With only 270 million parameters, it has some quirks—it oddly references Trump and Clinton in general conversation—but it excels at this specific email tagging task and runs blazingly fast on my CPU-only hardware.

The Final Result

Here's what my completed n8n workflow accomplishes:

  1. Triggers when a new email arrives
  2. Extracts the email content
  3. Sends it to my self-hosted AI with the refined prompt
  4. Receives clean JSON categorization
  5. Tags the email accordingly

The key lesson? When working with AI automation, prompt engineering is just as important as choosing the right model. Sometimes you need to be almost comically explicit about exactly what you want—and what you don't want—in the output.

n8n flow screenshot

This setup now reliably categorizes my incoming emails without any manual intervention, all running on my own hardware. The combination of n8n's flexibility and a well-tuned local AI model creates a powerful, privacy-respecting automation solution.