Crashlytics to Code Review: How Halodoc Fixes Android Crashes and ANRs Automatically
People open Halodoc at moments that matter: to talk to a doctor, to order medicine they need today, to book a lab test or to pay for care. An app crash in any of those flows is more than a bad session. It can cut a consultation short, interrupt a payment, or leave a patient unsure whether their order went through. For a healthcare app, reliability is part of the care we provide, so every new crash on Android deserves a fast, careful fix.
Fast and careful rarely go together when a person does every step by hand. So we built a pipeline that takes each new Android crash and ANR (an "Application Not Responding" freeze) from our crash reporting tool to a fix that is ready for review, with an AI coding agent doing the investigation in between. An engineer's part of a crash fix went from hours of investigation to under half an hour of review. This post walks through each stage of the pipeline, from crash alert to merged fix, and we have published the agent's method so other teams can use it too.
- Trigger: A new crash or ANR is reported in Firebase Crashlytics.
- Engine: A small n8n workflow for intake, an automated pipeline for each run, and Claude Code following a skill: a written, versioned procedure the agent follows step by step, like a runbook a senior engineer would hand a new teammate.
- Output: Merge requests (proposed code changes waiting for review) with the root cause, the fix and tests, in every library the fix touches.
- Decision: Always an engineer. The AI drafts; people approve.
What a crash used to cost us
Our Android apps are large multi-module projects. Each feature and each piece of shared code lives in its own library, published on its own and pulled into the apps through a shared build configuration that pins every library's version. That is good for teams working in parallel, but it turns a crash fix into a small project:
- Triage. Read the stack trace and work out which library the crash actually lives in.
- Reproduce and diagnose. Understand which input was in which unexpected state, and why only some users hit it.
- Fix and publish. Change the library, then wait for it to publish a new version.
- Update every consumer. Bump the version in each module that depends on the library, in the right order, before the fix can reach users. A bug in a widely shared library can touch many dependent modules.
ANRs are harder still, because nothing is thrown. The only clue is which main-thread frame was blocked, and on what: disk, a lock, a call to another process, or heavy computation. Depending on the crash, all of this took an engineer two to five hours. None of the steps is difficult on its own. Together they are slow, repetitive and easy to get subtly wrong.
The one rule we kept
The AI never merges its own code. It can investigate, write a fix, run tests and open merge requests, but whether a change ships is always an engineer's call. Everything else in the pipeline is designed to make that decision quick: the reviewer gets the evidence, the reasoning and the risk in one place, instead of starting from a raw stack trace.
This is built into the tooling, not left to the agent's good behaviour. Neither the agent's procedure nor the pipeline around it has a merge step: every change is pushed to its own branch and opened as a merge request. The main development branch is protected, so nothing reaches it except through a merge request, and every merge so far has been done by an engineer.
A few more rules keep an unattended agent predictable. Each run is handled by a single agent from start to finish, because splitting the work across parallel agents produced conflicting changes. The agent keeps no memory between runs and works only from the crash data and the code in front of it. And crash data is treated as evidence, never as instructions: a stack trace or an issue title is something to analyse, not something to obey.
The pipeline at a glance

Stage 1: Catching new crashes with n8n
n8n is an open-source workflow automation tool. You build a workflow by connecting small blocks, such as "receive a webhook", "look something up" and "call an API", and n8n runs it every time an event arrives. It suits this step because deciding whether to start a run is bookkeeping, not reasoning. It needs no AI, it should be cheap and predictable, and anyone on the team should be able to open it and see exactly what it does.
Our workflow is four blocks long. It receives the crash alert, checks a simple ledger of crashes already handled, stops if the crash is there, and otherwise starts a pipeline run and adds the crash to the ledger. One crash produces exactly one run, however many times it fires.
Stage 2: A clean, repeatable run
Each run happens in an automated pipeline on a dedicated build machine. Before the agent starts, the pipeline makes sure every run begins from the same place:
- Fresh code. Every repository is reset to the latest main development branch, so nothing left over from an earlier run can leak in.
- Narrow access. The agent gets read-only access to crash data, and nothing else, through MCP (Model Context Protocol), a standard way to give an AI agent access to a tool. What it needs to debug is technical: stack traces, app and OS versions, and how often a crash happens. Merge requests and team messages show only aggregate counts of events and affected users, never details about an individual user.
- No one at the keyboard. Claude Code runs headless with a turn limit, so a run always ends.
- A fixed exit. The agent logs every automatic decision as an
AUTOline, anything suspicious asWARN, and stops cleanly withFATALwhen it can't continue. It must finish by returning a small JSON result: the issues processed, the repositories changed, the merge request links and a one-line summary. The pipeline turns that into a message for the team.
Stage 3: The agent's method
The skill is what makes the output trustworthy, and it is the part other teams can reuse. It is versioned, and every merge request records the version that produced it. It works in three moves.
1. Evidence first. It pulls the crash summary, several sample events and how many users are affected, then picks the right frame to study: our own code closest to the crash or, for a freeze, the blocked main-thread frame and the thread holding any lock it waits on.
2. Read, hypothesise, check. It may not explain code it hasn't read. It opens the failing method and every caller first, then names the failing input in one sentence:
<Class>.<method> crashes because <input> is <unexpected state> when <condition>.It checks that sentence against questions including: does it explain why the crash happens at this frame and not one above it, why only some users hit it, whether the crash is brand new or back after a fix, and does it hold for every sample event? If not, the hypothesis is revised.
3. Fix, test, carry. It makes the smallest fix at the source of the problem and adds two unit tests: one for the failure and one that protects existing behaviour. For a crash, the first test reproduces the failing input and shows it is now handled. A freeze can't be reproduced in a unit test, so for an ANR the test checks that the slow work no longer runs on the main thread. The skill then compiles and tests the changed module. Finally it carries the fix through every module that depends on it, in order. If the bug is in library C, library B uses C, and the app uses B, the fix needs a new version of C, then of B, then an update to the app. A shared version tracker hands out version numbers so two runs never claim the same one.
Before choosing a fix, the skill also labels the crash: a value used before it was ready, a missing value, a screen used after it closed, two threads changing the same data, or, for freezes, what blocked the main thread (disk, a lock, another process, or heavy work). Each label points to a different kind of fix. It also asks whether the cause is in our own code at all. When the root is inside a third-party library or the operating system, it writes up the analysis and says so, instead of forcing a change into our code.
The full procedure is published here: Android Crash & ANR Analyser skill.
Stage 4: A review-ready merge request
Every fix arrives as a merge request that opens with a short summary: the crash and how many users it affects, its type, a regression risk (high, medium or low, with the reason), a release verdict, and whether the tests passed. Below that come the root cause in plain words, how to reproduce it, what changed, how to verify it, and the order in which to merge the related version changes. Each merge request is also picked up by our AI code reviewer, so its comments sit alongside the diff when an engineer opens it.
Stage 5: Closing the loop with the team
When a run ends, the pipeline posts to the team's code review channel. Every outcome gets a message: the merge requests to review, the reason no fix was applied (for example, a root cause outside our code), or why the run failed. A new crash never disappears silently, even when the agent couldn't fix it.
Results
| Before | Now | |
|---|---|---|
| Engineer time per crash with a fix | 2–5 hours | 20–30 minutes to review, test and merge |
| Engineer time saved | — | 75–90% on every crash where the agent produced a fix |
| Triage, diagnosis, fix and version updates | Done by an engineer | Done by the agent before anyone looks |
Engineer times are estimates from the engineers who did the work, and the 20 to 30 minutes applies to a fix in a single module. A fix in a shared library takes longer to review because each dependent module's update is checked too, but those updates now arrive already done, which is where the biggest savings are.
The benefits go beyond engineer hours:
- Consistency. Every crash gets the same careful investigation, whoever is on call and whatever time it arrives.
- Knowledge capture. Each merge request explains the root cause in plain words, so it doubles as documentation for the next engineer who touches that code.
- Nothing slips through. Every crash ends in a fix, a reason for no fix, or a reported failure.
- Focus. Engineers spend their time judging fixes, not searching for causes.
For our users, the result is simple: a fix is ready when an engineer first looks at a crash, so it can reach patients in the next release instead of waiting in a backlog.
What the agent gets wrong
The agent is not always right, which is exactly why an engineer reviews every change. The most common gap is a fix that addresses the right cause but misses a detail: a file left open when an error occurs, the same problem in sibling methods next to the one that crashed, or a guard on the crashing call where the better fix belongs in how the object is initialised. Its tests sometimes need adjusting to fit the project's test setup, and version updates can fall behind newer changes on the main branch and need refreshing before merge. It only compiles the module it changed, so the full app build happens at review. When the cause is in a third-party library or the operating system, it reports that rather than fixing it.
How to start
If you want to build something similar, we would grow it in three steps:
- Start with analysis only. The agent writes the root cause; engineers write the fix.
- Add the fix and tests once the analysis is consistently right.
- Add the multi-module version updates last. They are the biggest time saver and the riskiest step.
Challenges we faced
The first runs in May 2026 worked end to end, but they also showed us problems that only appear when a coding agent runs unattended on every new crash.
- Running out of usage quota. The first runs drew on one person’s usage limit, which other automations shared, and it ran out within days. Runs hung or stopped early, and because the limit was shared, one exhausted quota could stop every automation at once. We moved the pipeline to a dedicated automation identity with its own, larger quota. Lesson: give automation its own quota, sized for unattended runs.
- Runs that ended without a word. When a run failed, nothing reached the team, and because the crash was already in the ledger, it was never picked up again. Now every run ends with a message: the fix, the reason there is no fix, or the cause of the failure. Lesson: an unattended job must report its failures, not just its successes.
- Sign-ins that expired. The agent was signed in through an interactive login that expired every so often, so a job that worked one day failed the next until someone signed in again. Switching the automation to non-expiring service credentials removed the failure. Lesson: automation should never depend on a person signing in.
Principles behind the pipeline
- Keep bookkeeping out of the model. Duplicate checks, scheduling and notifications run in plain workflows and scripts, where they are cheap and predictable. The model only does the work that needs judgement.
- Start from a clean tree every time. Each analysis sees exactly what is on the main development branch.
- Give the agent an exit contract. A required JSON result turns a long, free-form session into something a script can count and report.
- Grant only the access the job needs. Read crash data, propose changes, never merge.
- Write the procedure down. A versioned skill means every run follows the same steps, and every improvement reaches every future run.
Conclusion
In a large multi-module Android codebase, a crash fix was never just a fix. It was triage, diagnosis, a library change and a chain of version updates across every module that depends on it, two to five hours of an engineer's day each time. By connecting Crashlytics, a small n8n workflow, an automated pipeline and Claude Code running a versioned skill, we turned that chain into a single run that ends with a tested fix waiting for review. We built it, it works, and we have published the method so other teams can use it.
If you're thinking of building something similar, these are the lessons we'd share:
- Let the agent finish the whole job. On Android, the fix itself is often the small part. The time goes into carrying it through every dependent module, and that is exactly the work an agent can do reliably once the rules are written down.
- Make it prove its reasoning before it writes code. Requiring the agent to read the code, name the failing input and check its hypothesis against every sample is what separates root-cause fixes from patches that only move the crash.
- Make every run end visibly. A fix, a reason for no fix, or a clear failure. Silent runs are how crashes slip through the cracks.
- Keep the human decision, and make it cheap. The agent never merges. Everything it produces is shaped so an engineer can make that call quickly instead of starting from a raw stack trace.
- The pattern is not Android-specific. It works with any crash or error tracker: plain automation for intake, an agent for judgement, people for the decision.
The outcome is more than faster fixes. New crashes now get the same careful investigation every time, engineers spend their time judging fixes instead of hunting for them, and the people who rely on Halodoc get a steadier app sooner.
References
Tools we used
- Firebase Crashlytics: firebase.google.com/docs/crashlytics
- Firebase MCP server: firebase.google.com/docs/ai-assistance/mcp-server
- Running Claude Code headless: code.claude.com/docs/en/headless
Our published skill
- Android Crash & ANR Analyser: github.com/halodoc-tech/halodoc-ai
Join Us
Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels, and if building reliable, AI-assisted engineering workflows like this one excites you, please reach out to us with your resumé at careers.india@halodoc.com.
About Halodoc
Halodoc is the number one all-around healthcare application in Indonesia. Our mission is to simplify and deliver quality healthcare across Indonesia, from Sabang to Merauke.
Since 2016, Halodoc has been improving health literacy in Indonesia by providing user-friendly healthcare communication, education, and information (KIE). In parallel, our ecosystem has expanded to offer a range of services that facilitate convenient access to healthcare, starting with Homecare by Halodoc as a preventive care feature that allows users to conduct health tests privately and securely from the comfort of their homes; My Insurance, which allows users to access the benefits of cashless outpatient services in a more seamless way; Chat with Doctor, which allows users to consult with over 20,000 licensed physicians via chat, video or voice call; and Health Store features that allow users to purchase medicines, supplements and various health products from our network of over 4,900 trusted partner pharmacies. To deliver holistic health solutions in a fully digital way, Halodoc offers Digital Clinic services including Haloskin, a trusted dermatology care platform guided by experienced dermatologists.
We are proud to be trusted by global and regional investors, including the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek, and many more. With over USD 100 million raised to date, including our recent Series D, our team is committed to building the best personalized healthcare solutions — and we remain steadfast in our journey to simplify healthcare for all Indonesians.