Blog / Technical Staff

We Gave Software an Immune System

Light runs around 100 customer-facing apps on top of our public API. Say a field gets renamed in the API. An app still asks for it by the old name. The request goes through. The page loads fine. The app just gets nothing back, because nothing told it the name had changed. And now there is a blank number sitting on the customer's screen.

That becomes a silent error. So the customer does the only thing left to do. They report it.

That kicks off a process that can run for hours, sometimes days. Someone has to work out what happened, reproduce it, find the right engineer, figure out the fix, test it, ship it, and finally tell the customer it is fixed.

Traditional monitoring is built on one assumption: that broken software is loud. Silent errors break that assumption.

2 kinds of error

Loud errors are the obvious failures: a page that will not load because a connection dropped, a database that refuses to answer, an automated test that fails. Those are easy, and we usually catch them before anything ships, the moment the code change is first proposed.

The dangerous kind is silent: the app asks for data in a way that no longer matches the API, or a pricing table quietly goes out of date. Nobody notices. It can corrupt data for days while people keep using it, trusting it.

Two kinds of error. A loud error is caught the moment the change is proposed: the check runs, the change is blocked, and nothing reaches a customer. A silent error passes every check: the field is renamed, the request still succeeds, and a blank number sits on the customer's screen for days while nothing goes red.
The loud one is a bug. The silent one is a bug nobody is looking for.

When software breaks, it should notice, work out what went wrong, and start fixing itself. That cycle of noticing and improving, the way a living thing does, is what we call Organic Software.

So we stopped waiting for the crash

The pipeline we built has 4 jobs.

The 4 stages a problem passes through: Detect, where scheduled checks find a silent failure; Shape, where the detector turns it into a structured Finding carrying its evidence and the limits of the fix; File, a single API call to the agent; and Fix, where the agent writes the change and a person approves it.
Detect, shape, file, fix. Every stage is allowed to stop.

Detection is the part that watches for the things ordinary monitoring misses, which means the silent half. So we run scheduled checks: for places where the live API and its published description have drifted apart, for pricing tables that have gone stale, for apps whose descriptions of themselves no longer match. We check our own test data. We watch for errors as they happen. And we check whether the scheduled checks are actually running.

That last one sounds pointless until the day you need it. On August 26, every scheduled check failed to run, because GitHub Actions, the service that runs our checks, was having an incident. Nothing went red. Nobody was told. From the inside, a check that never ran looked exactly like a check that ran and passed. Nothing errored, so nothing looked wrong.

So we added a check for the checks. It is a small example of a bigger principle: a system that heals itself has to be able to see its own blind spots.

Finding a problem is not the same as knowing the fix

The next thing we learned is that detection is the easy part. The hard part is turning what you found into something another system can safely act on.

So every detector produces the same kind of structured report, which we call a Finding. It says what is wrong, what evidence backs that up, which files are actually affected, what the replacement should be, how confident we are, and how far the eventual fix is allowed to go. Filing is the simple step. Once a detector has shaped a Finding, filing it is just an API call to Sirius, our coding agent, which picks it up and does the actual fixing.

The part that matters most is that the Finding also sets the limits of what the fix is allowed to do. If the real fix falls outside those limits, the agent has to recognise that and stop, rather than force something through.

A Finding, the structured report a detector produces. It records what is wrong, the evidence, the replacement field name, which files are affected and how confident the detector is. Below a divider it sets the limits of the fix: what the fix may do, and what it may not.
A real one. The last row is the important one: it is what the agent is not allowed to do.

A wrong answer is worse than no answer. If a system whose whole job is to make failures quietly disappear writes the wrong fix, we assume the failure is handled and stop thinking about it. Meanwhile the real bug is still there in the code, and now nobody is looking for it.

Then we taught it how to be wrong

To stop the agent from shipping more silent errors, which would defeat the whole point, we had to teach it how to say no. Sometimes that means recognising there is nothing to do. Sometimes it means realising it cannot do the job safely and stopping right there. Both are answers the agent has to be allowed to give. 2 cases taught us why.

Case 1

Nothing to change

The agent was told to rename a field from required to isRequired. Nothing in the code used that field, so there was nothing to rename. Its instructions gave it no way to say so, and it hand-edited our own copy of the API specification instead. The result looked right. The reasoning was broken.

It had no way to report an empty task

Case 2

Already fixed

An app was supposed to change primary to isPrimary, but that app had already been fixed 2 days earlier. Again the task had no exit for "nothing to do", so the agent went looking for something to change and edited the stand-in test data instead, breaking a check that protects us.

It went looking for work that was done

Those were bugs in our instructions, not the agent's fault. So we changed how we write them. A brief can now say, in plain terms: nothing uses this field, no change is needed. And the agent can now stop and report back when a fix is beyond what it should safely attempt.

That turned out to be a real design improvement. A system that cannot say no, whether that is "nothing to do here" or "I cannot do this safely", is not a self-healing system. It is an autonomous system trying to look busy.

The system has to be quieter than the bugs

There is another lesson that only shows up once the system runs every single day. Automation that creates more noise than it removes is not really automation.

There is a cap on how many tasks a single run can produce. If a broken check suddenly reports 200 failures, the right response is not 200 pull requests. It is to tell a human that the check itself needs attention. Findings also do not pile up. The same problem showing up 3 nights in a row should stay 1 problem, not turn into 3 separate pieces of work.

We learned that one the hard way too. A CI check had that de-duplication switched off, and a single problem spun up 4 near-identical pull requests over a weekend.

A single problem spun up 4 near-identical pull requests over a weekend. The code was doing exactly what we had told it to. We just had not told it what good behaviour looked like.

Humans are still in the loop

When people talk about self-healing software, there is a pull toward jumping straight to the fully autonomous version. We have not, and that is not the right place to start. Before the system can put up a fix, it needs explicit sign-off. Silence does not count as a yes. Every fix still passes through a person. What has changed is what that person does: read the agent's explanation, approve the change, or send it back with notes.

What software should feel like

This started as a very practical problem in our own codebase. But the thing we actually care about is bigger.

Imagine software where your relationship with failure is different. Something breaks. The software notices before you have even reported it. It tells you what went wrong, and that it is already on it. 30 minutes later, the bug is gone.

The same accounts receivable summary with Outstanding showing 56,850 euros again. A note underneath reads: Outstanding is back. Broke at 09:12, noticed at 09:14, fixed at 09:41. No ticket was raised, because nobody had to raise one.
Broke at 09:12, noticed at 09:14, back at 09:41. No ticket, because nobody had to raise one.

Today, we mostly accept that software is something we put up with and maintain. We file tickets. We wait for engineers. We read release notes. We learn workarounds for bugs that have been around so long they start to feel like features.

Self-healing is the second step we are taking toward Organic Software. The first was adaptability, covered by my colleague in The First Step to Organic Software.

The pipeline is still early. It has caught real silent failures and produced real fixes, and it has also shown us where the hard problems are: ambiguity, noisy detection, out-of-date assumptions, unsafe fixes, and knowing when to stop. It still has to learn to cover new code as it ships, and to look after a codebase by living inside it. None of that is a reason to walk away from the idea. It is the engineering work that makes it trustworthy.

Because the future we want is not software that never breaks. It is software that heals, breathes, and lives.

The apps this pipeline looks after run inside Light, the agentic accounting platform. See what Light already connects to in the App Store, or book a demo.

Similar articles

Ready to build what's next?

Bring your ideas to life with Light. Explore what we can create together.

Book a demo