ℹ️ TL;DR
One email address, support@locallygrown.net, quietly turned into a pipeline that reads, sorts, and drafts replies before I ever see a message: it skips the noise, runs a spam gate that fails open, untangles forwarded threads to find the real sender, figures out which market they belong to (with a plain-English database as backup), and hands the whole thing to Sage, the same expert agent my market managers already trust. A recent branch even watches DMARC reports for anyone sending mail as me — and caught a “forger” that turned out to be a customer’s security scanner. Every stage ends in a human decision, on purpose. The machine reads and drafts; I decide.

For years, support@locallygrown.net was just an inbox I dreaded. Mail came in — a grower who couldn’t log in, a customer whose eggs never showed up at pickup, a market manager with a policy question, and in between all of it, an unrelenting drizzle of SEO pitches and “I noticed your website could rank higher.” Every message was a context switch, and most of them weren’t even for me.

These days it’s less an inbox than a pipeline. When an email lands, a small machine reads it before I ever do: it throws out the noise, decides whether it’s spam, untangles forwarded threads to find the actual human, figures out which of our markets the person belongs to, drafts a reply, and hands me a tidy little ticket with all of that already done. And lately it does one more thing: it keeps an eye on who’s out there sending mail as me.

None of this replaced me. That distinction matters to me more than the automation does, and I’ll come back to it. But first, the anatomy.

One address, a dozen jobs

The whole thing hangs off a single Mailgun inbound route that posts every message to an n8n workflow. n8n is the connective tissue, a self-hosted automation tool where each step is a node and you can watch data flow through them like water through pipes. That visibility turned out to matter a lot, because the work was never in any one clever step but in the whole chain.

The leftmost section of the support-processor canvas in n8n, a node-graph automation editor. A Mailgun Webhook trigger feeds a Switch node (in Rules mode) with two outputs. Output 0 runs up and to the right into an 'Evaluate DMARC Report' code node, then a 'Send DMARC Report' HTTP node. The 'Fallback' output runs along the bottom into 'Filter Skip List,' then a 'Detect Spam' node backed by Anthropic, then 'Filter Spam,' then 'Lookup Market from Email,' continuing off the right edge.
The front half of the pipeline. Everything enters through one Mailgun webhook, then the Switch forks the flow. DMARC reports peel off along the top to get parsed and watched, while everything else takes the “Fallback” path into the support chain: skip the noise, detect spam, filter it, and find the sender's market.

Here’s what the chain does, in order.

1. Skip the obvious noise

The first node is almost insultingly simple: a short list of senders that never need a human, like mail from myself, PayPal receipts, or anything from a no-reply@ address. If the sender matches, the workflow just stops. No point spending a single CPU cycle thinking hard about a transaction receipt.

2. A spam gate that fails open

What’s left goes through a small, fast language model whose only job is to answer one question: is this unsolicited junk? It returns a confidence score, and only mail it’s quite sure about (80% or higher) gets dropped.

The interesting decision here isn’t the model itself but what happens when the model is unsure, or when its answer comes back malformed: the workflow lets the message through. I made that call on purpose. A spam email that reaches me costs me three seconds and a delete. A real customer’s email that gets silently eaten because a classifier had a bad day costs me a customer. Those are not symmetric mistakes, so the system is biased toward the cheaper one. “When in doubt, bother the human” is a good default for anything that sits between people and me.

3. Untangling the forward

A surprising amount of support mail arrives forwarded, and the single biggest source of those forwards is me. I’ve been online long enough to have accumulated email addresses like barnacles: old personal ones, role accounts, addresses I’d genuinely forgotten I had. People use every one of them. Someone digs an address of mine out of a decade-old order, or I’m still sitting in their contacts from who-knows-when, and they write to me directly instead of to support. So I forward it into the funnel, at which point the message looks like it’s from me, about a problem that has nothing to do with me. Market managers do the same thing, bouncing a customer’s complaint my way. Either way, the sender on the envelope is no longer the person with the problem.

So there’s a node that does a bit of archaeology, hunting for the tell-tale signs (the Begin forwarded message: lines, the Fwd: in the subject, the quoted From: headers buried in the body) and digging out the original sender. It’s unglamorous regex work, the kind that’s never quite finished, but it means the rest of the pipeline reasons about the actual customer instead of the messenger, which matters most when the messenger is me. I’d very much rather the system not go looking me up as the aggrieved party.

4. Whose market is this, anyway?

LocallyGrown is a platform that hosts many local food markets, each with its own manager. To route a message well, I need to know which market the sender belongs to. There’s a lookup table of market managers keyed by email, and most of the time that’s an instant hit.

But when it misses, the fallback is my favorite part of the whole build: the workflow asks the production database, in plain English, through a separate “chat with a database” sub-workflow. It can take “which market does this person belong to?” and turn it into the actual SQL, run it, and hand back the answer. Take a grower who sells at several markets: a plain lookup just tells me who they are, but the database comes back with every market they belong to and how they use the system, which is the difference between a name and a full picture. The lookup table handles the fast path, and the database conversation is there as a safety net for everything it misses. Belt and suspenders, except the suspenders speak English.

A separate n8n canvas, the 'talk to the database' sub-workflow. A chat-message trigger and a 'When Executed by Another Workflow' trigger both feed into a 'Normalize Input' code node, which connects to a central 'Data Assistant Agent.' Three things hang beneath the agent: an Anthropic chat model, a simple memory store, and an 'Execute a SQL query' tool.
The entire “plain-English database” is this small: an agent, a model, a little memory, and one SQL tool. The support processor reaches it through the “When Executed by Another Workflow” trigger at the lower left.

5. Sage

With the sender and their market now resolved, the pipeline reaches its heart: the hand-off to Sage.

The middle section of the same support-processor canvas. From 'Lookup Market from Email,' the flow runs through 'Get Manager Market' (a data-table lookup), then 'Merge Market Data,' then an 'Unknown Sender?' decision node. Its 'true' branch passes through 'Edit Fields,' a call to the 'Chat with a database using AI' sub-workflow, and 'Merge in DB Response'; its 'false' branch skips straight ahead. Both paths converge on 'Prepare Sage Call.'
Resolving the sender's market, then handing off to Sage. If the manager table already knows who they are, the flow runs straight to “Prepare Sage Call” along the “false” path. If it doesn't, the “true” path detours through the plain-English database to work out the market first. Either way, everything funnels into the call to Sage.

By now the message has everything it needs around it: who actually sent it, whether it arrived through a forward, which market they belong to. The pipeline could hand all of that to a general-purpose model with a clever prompt and get a passable guess back, but it doesn’t. It hands it to Sage instead.

Sage is honestly the most useful thing in this entire post, and I nearly forgot to mention them, which tells you how thoroughly I’ve come to take them for granted. They’re an agent I built specifically for LocallyGrown, not an off-the-shelf model wearing a support hat. They have our actual documentation at their fingertips and a real working knowledge of how the platform behaves: markets, growers, orders, pickups, payments, the whole tangle. The things a general model would have to guess at, Sage simply knows.

And here’s what makes them more than a triage gadget: Sage isn’t only a behind-the-scenes step in this pipeline. They’re the same agent market managers talk to directly inside the application, and those managers have been leaning on them hard, to genuinely good effect. So when a support email lands, it isn’t getting handed to some bespoke email-only classifier I bolted on but routed to the exact same expert the managers already trust, just arriving through a different door. The inbox and the in-app assistant share one brain.

For triage, Sage hands back a small, strict packet of JSON: a category (grower issue, login problem, technical bug, payment, missing items, or a market-specific question), an urgency from 1 to 5, whether it’s something a market manager should handle rather than me, and the part I lean on most, a drafted reply, written in a warm, plain voice.

Here’s the honest version of how I use that draft, though, because it isn’t what you’d guess: I have never once copied it into a reply and hit send. I could; the drafts are almost always spot-on. But that was never the point. Reading the suggested answer turns out to be nearly as valuable as reading the original question. Seeing the problem and a plausible response side by side keeps me from skimming past a detail or jumping to the wrong conclusion; the two together hand my brain a far better starting point than the bare email does. Then my own fingers write the actual reply from there. The draft works less as the output than as a second pair of eyes on the situation before I commit to an interpretation.

Sage is told never to ask for information they already have. If they know the market, they use the name. If there’s an attachment, they acknowledge it instead of asking for one. These are little things, but they’re what make the draft worth reading. A generic template would tell me nothing, while a draft that wrestles with this person’s actual problem is what sharpens my own read of it.

6. Assembling the ticket

The rightmost section of the same support-processor canvas. 'Prepare Sage Call' connects to a 'Talk to Sage' HTTP node, then a 'Code in JavaScript' node that parses Sage's reply, then a 'Send to Eric' HTTP node that posts the finished ticket out through Mailgun.
The hand-off. “Prepare Sage Call” packages up the message and everything we've learned about it, “Talk to Sage” asks the agent, the code node unpacks their reply, and “Send to Eric” drops the finished ticket (analysis, draft, and original thread) into my inbox.

Finally, the workflow assembles all of that into a single enriched email and drops it in my actual inbox. The subject line alone does a lot of work (something like [Athens][MISSING ITEMS][P4] customer didn't receive eggs at pickup), so I can triage from the subject before I even open it. Inside, I find Sage’s read on the issue, the customer’s address with a one-click copy button, the market (linked), the suggested reply in a quotable block, and the original thread underneath.

What used to be a raw, contextless email is now a ticket that has already been read, classified, routed, and answered in draft. I open it, glance, usually tweak a sentence, and send.

7. Watching who sends mail as me

The most recent addition has nothing to do with support, but it’s the reason I started writing this post, because it turned into a small detective story.

Every day, the big mailbox providers send DMARC aggregate reports, gzipped XML files that tell you who’s been sending email claiming to be from your domain, and whether it passed authentication. Almost nobody reads them, because they’re unreadable. They were piling up in support@ like everyone else’s, getting triaged as if they were customer tickets.

So I gave them their own branch. A switch peels off anything that looks like a DMARC report before it touches the support machinery. From there it gets decompressed and parsed inside the workflow, which sounds trivial and absolutely was not. The off-the-shelf decompression node kept quietly eating the file, so I ended up writing a tiny gzip inflater by hand, in plain JavaScript, with no dependencies. There’s a particular kind of joy, for someone who started on an Apple ][+, in implementing DEFLATE from first principles at midnight because a black-box node wouldn’t cooperate. (It works. I validated it byte-for-byte against real reports before I trusted it.)

Once parsed, the branch applies a few rules about what’s actually worth waking me for: anything getting rejected, anything failing authentication while claiming to be my domain, but not the enormous volume of mail that simply passes, and not the legitimate forwarding that fails for boring reasons. Only genuine surprises send an email.

The first surprise it caught was an AWS server I didn’t recognize, sending as my domain, getting five messages rejected. For about an hour I was convinced someone was impersonating me. I host everything on DigitalOcean and have nothing in AWS. It wasn’t my mail provider either. The thing that cracked it was a single reverse-DNS lookup: the IP belonged to cloud-sec-av.com, which is Check Point’s Harmony email security (formerly Avanan).

The “attacker” was a bodyguard. One of my recipients runs their inbox behind a security scanner that intercepts incoming mail, inspects it, and re-injects it. When it passes my message through untouched, my signature survives and everything’s fine. When it rewrites the message (adds a banner, mangles a link), my cryptographic signature breaks, and from the outside that looks exactly like forgery. My “reject” policy was correctly refusing to vouch for a tampered copy of my own email. Nothing was wrong. A security product was doing its job, and so was mine, and they happened to disagree.

That was the real lesson: in DMARC data alone, a recipient’s spam filter and an actual impersonator look nearly identical. The only thing that told them apart was the reverse-DNS name, which the reports don’t include. So the branch now does that lookup itself, recognizes the big email-security vendors by name, and files their forwarding under “informational” instead of paging me. Real strangers still ring the bell. Bodyguards don’t.

What runs through all of it

One thing keeps pulling me back. Every single stage of this pipeline ends in a human decision, on purpose.

The spam filter fails open. Sage writes a draft rather than a sent reply, and I never send that draft anyway; I read it to think more clearly, then write my own. The market routing only suggests, and I’m the one who confirms it. The DMARC monitor surfaces a judgment call and hands it to me with enough context to make it, without quarantining anyone or touching a DNS record. The machine handles the reading and the sorting and the first-draft thinking, all the repetitive, attention-shredding work. I do the deciding.

I’m not apologizing for that limitation. It’s the only shape of AI automation I actually trust myself to run on a system real people depend on for their groceries and their livelihoods. Automation that acts on its own has to be right. Automation that drafts and defers only has to be helpful, and helpful is a much more forgiving target. A bad suggestion costs nothing more than the second it takes to ignore it. A bad autonomous action means a customer never got their reply, or a legitimate sender got blocked.

There’s a pleasant recursion in how it got built, too. I built this with an AI assistant, pairing with it the way I’d pair with a sharp colleague. The rote stuff got handed off: write me a dependency-free gzip inflater, validate this parser against four real reports, run the whole node the way the platform will run it and show me it works. The judgment stayed mine: what counts as an anomaly, what to suppress, when to fail open, whether that AWS IP was a threat or a misunderstanding. It’s the same philosophy, one level up. The tool amplified what I was doing without deciding anything that mattered.

support@locallygrown.net is still only an email address. It just has better instincts now, and the good sense to ask me before it does anything it can’t take back.