From bug whack-a-mole to self-healing machine: building an autonomous bug pipeline on n8n

From bug whack-a-mole to self-healing machine: building an autonomous bug pipeline on n8n

From bug whack-a-mole to self-healing machine: building an autonomous bug pipeline on n8n

A year ago my morning routine was predictable and miserable. Open laptop. Open GitHub. Scroll through a dozen issues auto-filed overnight by PostHog. TypeError: Cannot read properties of undefined. Error: Cannot set headers after they are sent. Same errors, different days, different repos. Two hours of triage across four repositories, every morning, and things still went unfixed.

Today I wake up to one Telegram message telling me what happened overnight, what was fixed, what's pending, and which branches are waiting for review. I haven't touched a GitHub issue manually in months.

This is the architecture that emerged, and — more usefully — the four problems that nearly killed it. If you're building something similar, the failure modes are the valuable part. The happy path is easy.

The starting point: a fire hose of errors

Fismbot runs a hybrid architecture: an Express.js backend serving an Angular frontend, across multiple repositories. We use PostHog for error tracking, configured to auto-file a GitHub issue whenever a new error fingerprint appears.

Great for visibility. Terrible for sanity — you don't miss anything.

The issues fall into three predictable buckets:

Bucket Share Examples
Noise ~60% Browser extension conflicts, PostHog's own SDK swallowing itself, Script error. cross-origin ghosts with zero actionable data
Real but repetitive ~30% Null-check bugs — Cannot read properties of undefined (reading 'length')
Actually interesting ~10% New regressions, deployment failures, race conditions

The bottleneck was never the coding. It was context-switching. Every issue required loading a different mental model of a different part of the codebase.

The first intuition: triage follows rules

TypeError: Cannot read properties of undefined is either a missing null guard in our code or PostHog SDK noise. EPERM: operation not permitted, rename is always Windows filesystem noise. Script error. is always cross-origin and never actionable.

These aren't judgment calls. They're pattern matches. If a human can triage by pattern, a machine can too — it just needs access to GitHub, PostHog, and the codebase.

The architecture: separate the worries

A single monolithic agent doing everything would be fragile and insecure. So I split the problem into layers with hard boundaries.

Layer Role Can push code?
1. Orchestration (n8n on a Raspberry Pi) State machine, cron scheduling, queueing ✅ It is the only thing that can
2. Intelligence (Hermes Agent) Analysis, diagnosis, fix proposals ❌ SSHs into the Pi when it needs to act
3. The human (me) Reviews branches, merges, deploys Decides, doesn't grind

The Raspberry Pi's double life

A £35 Raspberry Pi on my desk does two jobs at once, and that duality is what makes the security model work.

As orchestration host, it runs n8n. Four workflows fire on cron:

Together they form a state machine tracking every issue through its lifecycle:

unseen → queued → dispatched → annotated → approved → implementing → completed

The state lives in an n8n data table — a lightweight alternative to a full database, keeping everything self-contained on the Pi.

As execution worker, the Pi has a dedicated fismbot Unix user with SSH keys for GitHub, cloned copies of every repo, and a Git config with the org PAT embedded in the remote URL. When a fix needs applying, the Pi doesn't delegate. It does the work itself.

This is the security model: the Pi is the only machine that can push code. Not Hermes. Not the webhook sessions. Not any cloud service.

If an AI agent is compromised, an attacker can't push code. If the Pi is compromised, an attacker can't reach the AI provider's API keys. Defence in depth, implemented as architectural boundaries rather than config flags — with a £35 computer as the perimeter.

Problem 1: the webhook wall

This was the most frustrating failure, and the one nobody warns you about.

n8n dispatches annotation tasks to Hermes via a webhook POST. The webhook lands in a Hermes session — but that session has a different toolset than a normal CLI session. No terminal. No filesystem. No GitHub integration.

So Hermes would receive: "Clone the repo, analyze this error, post a comment on issue #34."

And respond: "I can't. I don't have bash. I don't have git. I can't authenticate to GitHub."

For weeks, 107 tasks sat in annotation_requested status — dispatched, never completed. Executions showed "success." No comments ever appeared.

The fix

Accept the limitation. The webhook context will never have full tool access — that's a security feature, not a bug. So:

  1. Triage happens in the webhook session, using what's already in the payload: the error message, candidate related issues, and public documentation. Less thorough than code-level analysis, but it catches obvious patterns.
  2. Writing shifts to the CLI session. The Pi has the GitHub PAT and can authenticate to private repos.

The pattern that emerged: webhook intelligence, CLI execution. If you're building this, design for it from the start rather than discovering it after a hundred stuck tasks.

Problem 2: the draft/active version trap

If you take one thing from this article, take this. It's the bug that cost me the most time, and it's poorly documented.

About two months in, everything broke: "Could not find the data table." I'd accidentally deleted the n8n data table tracking issue state. 216 rows, gone.

Rebuilding taught me the real lesson. n8n workflows have a draft version and an active version, and they are not the same thing.

When you PATCH a workflow's nodes and connections, you update the draft. The execution engine runs the active version — a separate snapshot. If you don't explicitly activate the new versionId, your changes never run.

I patched workflows successfully, saw the changes in the editor, and watched the pipeline keep failing because the active version was pinned to an old snapshot. It took four attempts to make one notification stick.

The fix pattern

1. PATCH  /workflows/{id}              → update the draft
2. GET    /workflows/{id}              → retrieve the new versionId
3. POST   /workflows/{id}/activate     → pass { "versionId": "..." }
4. VERIFY  activeVersionId == versionId

Every time. Without exception.

A note on the workaround you'll find elsewhere: the n8n community's answer is often a direct database edit:

UPDATE workflow_entity SET "activeVersionId" = "versionId" WHERE id = 'YOUR_WORKFLOW_ID';

That works, but it bypasses the API's validation and leaves no audit trail. Use the API pattern instead — it's idempotent and safe to re-run.

Second gotcha: when rebuilding connections, rebuild all of them from scratch, not just the new ones. An edit in the UI, a version bump, a save — any of these can revert the active version to a snapshot predating your new nodes. Nodes that appear connected in the editor had been floating disconnected in production for a day.

Problem 3: the silence problem

Once the pipeline worked, a new problem appeared. When all repositories were clean, the pipeline went silent. No errors, no notifications, no activity.

Silence in an automated system is indistinguishable from failure.

Was the pipeline broken? Was the tunnel down? Was the Pi crashed? Or were there genuinely no issues? Without a heartbeat I had to manually check every time I got suspicious — which defeated the point.

The fix: explicit "nothing to do" notifications. When Intake finds no open issues in a repo, it sends a repo_blocked event. When the Resolver finds no approved work, it sends resolver_queue_empty.

Getting repo_blocked: fism-bot/FISM has no open issues is now as valuable as annotation posted to #34. Positive confirmation of idleness beats silence. If you're building any long-running automation, add this on day one, not month six.

Problem 4: speed vs. safety

Early on, I had the agent committing directly to master. Fast: issue filed, fix written, deployed, closed, one automated cycle.

Also reckless.

The problem isn't that the AI writes bad code. It usually writes good code. The problem is no human verified the fix before it hit production. A misdiagnosis — blaming the wrong code path, fixing a symptom instead of the cause, introducing a subtle regression — goes straight to users.

The fix is standard software engineering: create a branch. Every fix goes to fix/issue-N-short-description. The branch is pushed and the issue annotated with the diagnosis, but nothing merges until a human reviews it.

You keep the speed gain in analysis and fix-writing. You keep the safety gain in the merge decision.

How it works end to end

PostHog error
    ↓
Auto-filed GitHub issue
    ↓
[Intake] dedupe → label → close pure noise
    ↓
[Resolver] dispatch to Hermes via webhook
    ↓
Hermes triage → diagnosis (webhook session, read-only)
    ↓
Pi SSH session → post annotation, create branch
    ↓
Telegram → "3 fixed, 2 pending review"
    ↓
Human reviews → merges → deploys

What it handles automatically:

What it deliberately doesn't do: merge to master, deploy, or close issues needing judgment.

Roughly 85–90% of the bug lifecycle runs autonomously. The machine grinds; I decide.

The part that actually mattered

The most satisfying metric isn't issue count or response time. It's that I no longer dread opening my laptop.

That change in posture — from firefighter to editor — is worth every hour of debugging n8n connections. And if you're building this yourself, the four problems above are the ones that will cost you the most time. Everything else is just wiring.

Sources and further reading

Last updated: September 2026.