Facility Maintenance Triage & Alerting
n8n · Claude · RAGThe problem
This one started as a gap I noticed against a job posting, not a personal itch. Roles building AI into real operations kept naming the same category of tool: agent and workflow orchestration platforms like n8n, Make, Copilot Studio. I'd built the trading agent entirely in Python and Claude's API directly, real experience, but not in that specific tool family. So I picked a problem worth solving and built it in the tool I needed to learn, rather than padding a resume with a claim I couldn't back up.
The domain: industrial maintenance triage. A technician reports an issue, an AI classifies it, urgent things get escalated immediately, everything gets logged. It mirrors real operations automation without requiring domain knowledge I don't have, and it's a close cousin to the kind of equipment monitoring a geothermal or industrial operations team actually needs.
Building it as a real pipeline, not a demo
The version that would have been fastest to build: intake form triggers Claude, Claude decides urgency, urgent stuff pings Slack. That's a demo. It's also a bad design, because it has no answer for what happens when the AI gets something wrong, and no record of anything that wasn't urgent.
So the actual pipeline does three things a shortcut version wouldn't: every ticket gets logged permanently regardless of urgency, not just the escalated ones. The AI's classification is grounded in that equipment's real history (RAG) before it reasons about the current issue, so it's not just pattern-matching on the text of the report. And the AI's own output gets checked before anything trusts it, a malformed response gets routed to a human-review queue instead of silently corrupting the log.
The hardest bug in the whole build looked, from every angle I could check, like it shouldn't
exist. The escalation listener's webhook verified correctly with Slack. The right event
(reaction_added)
was subscribed. The right OAuth scope was present. The workflow was published and saving
executions. And still: reacting to a message in Slack produced nothing, not even a failed
execution, just silence.
The actual cause was sitting on a completely different settings page: a small notice reading "Socket Mode is enabled. You won't need to specify a Request URL." Socket Mode is a separate, mutually exclusive delivery method, a persistent connection that had been silently intercepting every event and routing it away from the webhook entirely. The endpoint was correctly built, correctly verified, and was never going to receive anything while that one toggle stayed on.
Disabled Socket Mode, reinstalled the app, reacted again. The request headers on the delivery
that finally arrived showed x-slack-retry-num: 3,
confirming Slack had genuinely been retrying delivery the whole time, correctly, to an endpoint
that could never receive it while the wrong setting was on.
Choosing the harder architecture on purpose
Before building the acknowledgment tracker, there were two honest options: a scheduled workflow polling the tracker sheet every few minutes for stale alerts, roughly a 45-minute build, or a real-time webhook reacting directly to a Slack emoji reaction, architecturally more interesting and genuinely at risk of stalling out on auth. I chose the webhook upfront, time-boxed against falling back to polling if it wasn't working within about 90 minutes. It worked, in that same session, after clearing the Socket Mode issue above and a smaller one before it: n8n's webhook node auto-responds with a generic acknowledgment by default, but Slack's own verification handshake requires echoing back a specific challenge value, which needed an explicit Respond-to-Webhook node before the URL would verify at all.
A failure mode specific to visual workflow tools
The most repeated bug across the whole build wasn't really one bug, it was the same subtle
failure appearing three separate times. In a node-based tool like n8n, a reference to
$json resolves to whatever node
sits immediately upstream, not to a specific named source. After restructuring the workflow so
every ticket logs before the urgency check runs, instead of only urgent ones, three previously
working things broke at once: the urgency switch stopped routing anything, the Slack alert text
came back nearly empty, and later, the same thing happened again when wiring in acknowledgment
fields. Nothing about those expressions had changed, the node sitting in front of them had. Fixed
by replacing the implicit references with explicit ones tied to a named node, so the reference
stays correct regardless of what gets inserted upstream later.
Telling real bugs from test artifacts
A few things that looked broken during testing weren't. The first real test of the malformed- output fallback, deliberately telling the AI to omit a field, didn't trigger the fallback at all, because the prompt itself had two conflicting format blocks left over from earlier edits, and the model correctly followed the more specific one. That was a flawed test, not a working system failing to catch a real problem. Once the prompt was cleaned up to one unambiguous block, the same test correctly routed to human review. Worth being honest about in a build like this: some of the debugging time went to figuring out whether something was actually broken before assuming it was.
The equipment knowledge base got a similar honesty pass after the fact: the specific service records are necessarily fictional, no public source for a real plant's maintenance logs exists, but the underlying failure modes referenced (mineral scaling in heat exchangers, brine corrosion, non-condensable gas handling) were revised to reference real, documented geothermal-industry issues rather than arbitrary invented text.
Where it stands
Two n8n workflows (the main triage pipeline and the escalation listener), a live dashboard reading directly from the underlying sheets with no backend server, and a README that doubles as the design doc, covering the architecture, the decisions above, and the limitations I'm upfront about: this is a documented working prototype, not a production deployment, and the README says so directly rather than overselling it.
The dashboard itself hit one more real issue worth naming: fetching the Google Sheet directly from the browser failed outright, since Google's export endpoint doesn't send the CORS headers browsers require for a cross-origin fetch, true for any site trying to read it, not specific to this setup. Since the dashboard is static and has no server of its own to proxy the request through, the fix was a script-tag injection technique against Google's visualization query endpoint instead of a normal fetch call, the same underlying method Google's own charting library uses, since script tags aren't bound by the same cross-origin restriction.
Planned next: replacing the equipment lookup with real vector-based retrieval grounded in actual failure modes, pulling source documents from SharePoint instead of hand-typed entries, and a conversational agent on top of the pipeline that can decide which tool to call rather than following a fixed path.