<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akshay Shah</title>
    <description>The latest articles on DEV Community by Akshay Shah (@akshay_shah_d03f6cb77c182).</description>
    <link>https://dev.to/akshay_shah_d03f6cb77c182</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4192049%2F5e804257-627c-43a0-812a-bba2e4c3dd0b.png</url>
      <title>DEV Community: Akshay Shah</title>
      <link>https://dev.to/akshay_shah_d03f6cb77c182</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akshay_shah_d03f6cb77c182"/>
    <language>en</language>
    <item>
      <title>Seven Alerts at 2 AM: Building an AI On-Call Agent with a Graph Database</title>
      <dc:creator>Akshay Shah</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:26:36 +0000</pubDate>
      <link>https://dev.to/akshay_shah_d03f6cb77c182/seven-alerts-at-2-am-building-an-ai-on-call-agent-with-a-graph-database-519h</link>
      <guid>https://dev.to/akshay_shah_d03f6cb77c182/seven-alerts-at-2-am-building-an-ai-on-call-agent-with-a-graph-database-519h</guid>
      <description>&lt;p&gt;It's 2 AM. Your phone buzzes, then buzzes again. Seven alerts in four minutes.&lt;/p&gt;

&lt;p&gt;Frontend says &lt;em&gt;Orders failing&lt;/em&gt;. The API gateway is throwing 5xx errors. Checkout is erroring, payment is slow, cart is slow. Somewhere near the bottom there's a quiet &lt;em&gt;warning&lt;/em&gt; on the cache, and a &lt;em&gt;High CPU&lt;/em&gt; alert on the recommendation service.&lt;/p&gt;

&lt;p&gt;Which one do you look at first?&lt;/p&gt;

&lt;p&gt;Most people start with the loudest one. That's usually wrong. The loud alerts are symptoms. The real cause is often a small change on a shared service that everything else depends on. Finding it means connecting a lot of dots: which service calls which, who deployed what and when, who owns it, and what fixed this last time.&lt;/p&gt;

&lt;p&gt;That's what I built for &lt;strong&gt;Graph Hacks&lt;/strong&gt; (WeMakeDevs × FalkorDB): &lt;strong&gt;RootCause Agent&lt;/strong&gt;, an AI on-call agent that finds the real cause of an outage by walking a graph.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://rootcauseagent.streamlit.app" rel="noopener noreferrer"&gt;https://rootcauseagent.streamlit.app&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/beyondions/rootcause-agent" rel="noopener noreferrer"&gt;https://github.com/beyondions/rootcause-agent&lt;/a&gt;&lt;br&gt;
🎥 &lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=YDL6ZGoFmOY" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=YDL6ZGoFmOY&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Where I started
&lt;/h2&gt;

&lt;p&gt;Four days ago I had never written a line of Cypher. I didn't know how an AI agent calls tools. I'd heard "graph database" but couldn't have told you why you'd pick one over Postgres.&lt;/p&gt;

&lt;p&gt;I chose Track 1, &lt;em&gt;Agents That Act on Connected Data&lt;/em&gt;, because on-call incidents felt like the clearest case where the answer lives in the connections, not in any single document.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this is a graph problem
&lt;/h2&gt;

&lt;p&gt;Think about what an on-call engineer actually needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is this alert the cause or a symptom?&lt;/strong&gt; That depends on what the service depends on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What else will break?&lt;/strong&gt; That's everything that depends on the failing service, several hops deep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed?&lt;/strong&gt; A deploy, connected to a service, connected to the engineer who made it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who do I wake up, and how was it fixed last time?&lt;/strong&gt; Owners, past incidents, the people who resolved them, and the runbook they used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a path through connected data. So I modelled it as a graph in FalkorDB: 25 services of an online shop, plus teams, engineers, deploys, alerts, past incidents and runbooks, joined by relationships like &lt;code&gt;DEPENDS_ON&lt;/code&gt;, &lt;code&gt;OWNS&lt;/code&gt;, &lt;code&gt;AUTHORED&lt;/code&gt;, &lt;code&gt;DEPLOYED_TO&lt;/code&gt;, &lt;code&gt;FIRED_ON&lt;/code&gt; and &lt;code&gt;RESOLVED&lt;/code&gt;. All the data is synthetic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqck6yzexaqat46dghcm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqck6yzexaqat46dghcm.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How the agent finds the root cause
&lt;/h2&gt;

&lt;p&gt;The rule turned out to be simple once I saw it as a graph:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An alerting service is a &lt;strong&gt;symptom&lt;/strong&gt; if something it depends on is also alerting. What's left are the &lt;strong&gt;candidates&lt;/strong&gt;, ranked by how many alerting services depend on them, and then by whether they had a recent deploy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my scenario, frontend, API gateway, checkout, payment and cart are all symptoms, because each of them sits upstream of another alerting service. Two candidates are left:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cache&lt;/strong&gt; explains 5 alerts and had a deploy 11 minutes before the outage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recommendation&lt;/strong&gt; explains only 2, and had no recent deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the answer is &lt;strong&gt;cache&lt;/strong&gt;, and the deploy behind it is &lt;strong&gt;D-1: "Config change: maxmemory lowered"&lt;/strong&gt;. The recommendation CPU alert is a red herring.&lt;/p&gt;

&lt;p&gt;The core of it is one line of Cypher, a variable-length traversal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Service&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$service&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:DEPENDS_ON&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="n"&gt;.&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;dep.name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;*1..&lt;/code&gt; means "follow DEPENDS_ON one or more hops". In SQL that would be recursive joins. In Cypher it's two characters.&lt;/p&gt;

&lt;p&gt;From there, the agent also finds the &lt;strong&gt;evidence path&lt;/strong&gt; (frontend → API gateway → cart → cache), the &lt;strong&gt;blast radius&lt;/strong&gt; (5 critical services), &lt;strong&gt;who to wake up&lt;/strong&gt; (Priya, who fixed the same problem before, and Arjun, who made the change), the &lt;strong&gt;fix&lt;/strong&gt; from the past incident's runbook, and then it &lt;strong&gt;acts&lt;/strong&gt;: it writes a new incident back into the graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  My design rule: facts from the graph, the LLM only explains
&lt;/h2&gt;

&lt;p&gt;The first versions of the agent let the LLM write the whole answer. It was fluent and confident, and sometimes wrong.&lt;/p&gt;

&lt;p&gt;The moment that changed my approach: the agent suggested a fix called &lt;em&gt;"Restore Cache Memory Settings"&lt;/em&gt;. That runbook doesn't exist. The real one is &lt;em&gt;"Roll back cache config"&lt;/em&gt;. My tests didn't catch it, because they only checked the root cause.&lt;/p&gt;

&lt;p&gt;At 2 AM, a made-up runbook name is worse than no answer. So I set a rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Facts come from the graph. Structure is enforced in code. The LLM only explains.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The LLM still decides which graph tools to call and writes the explanation. But the lines that matter (the first line, the suggested fix, yes/no answers like "is recommendation the cause?") are rebuilt in code from the tool results before anyone sees them. [Adjust if needed: the contact line got the same treatment after the model once invented a team name.]&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving the graph actually does the work
&lt;/h2&gt;

&lt;p&gt;It's easy to &lt;em&gt;say&lt;/em&gt; a graph helps. I wanted to measure it.&lt;/p&gt;

&lt;p&gt;So I built a vector-RAG baseline over the &lt;strong&gt;same data&lt;/strong&gt;: every node and relationship turned into a sentence, TF-IDF retrieval with stopwords and stemming, the current alerts added to the query, top 8 documents, &lt;strong&gt;same LLM&lt;/strong&gt;. I tried hard to make it fair. My first version was accidentally unfair, and when I fixed it, RAG started finding cache too. That changed my pitch from "RAG gets it wrong" to "here's what RAG &lt;em&gt;can't&lt;/em&gt; do".&lt;/p&gt;

&lt;p&gt;Then I scored both against an answer key &lt;strong&gt;read from the graph&lt;/strong&gt;, not typed in by hand:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Graph agent&lt;/th&gt;
&lt;th&gt;Vector RAG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Score (11 checks)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11/11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1/11&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RAG could guess that the cache was involved. But it blamed the API gateway as the primary driver, it never found deploy D-1 or who made it, it couldn't list the full blast radius, it couldn't say who to wake up and why, and it can't take action. Asked "what changed?", it answered &lt;em&gt;an increase in cache latency&lt;/em&gt;, which is an alert, not a change.&lt;/p&gt;

&lt;p&gt;Same data, same model. The only difference is the graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvj10syblcmgiemrgzq8w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvj10syblcmgiemrgzq8w.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes I made along the way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My first root-cause query returned all zeros.&lt;/strong&gt; I tried to do everything in one big Cypher query. I split it into simple traversals and combined the results in Python, and it worked straight away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;shortestPath&lt;/code&gt; inside &lt;code&gt;MATCH&lt;/code&gt; threw an error.&lt;/strong&gt; In FalkorDB it has to go in &lt;code&gt;RETURN&lt;/code&gt; or &lt;code&gt;WITH&lt;/code&gt;. That took me a while to figure out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My blast radius was hard-coded in the tests.&lt;/strong&gt; I noticed when I asked myself why it never changed. Now every expected value comes from the graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent treated every question as the current outage.&lt;/strong&gt; I typed &lt;em&gt;"auth is not working, who is responsible?"&lt;/em&gt; and it blamed the cache, woke up the wrong people, and opened an incident. Auth wasn't even alerting. That led to &lt;strong&gt;service-check mode&lt;/strong&gt;: if you ask about a service with no alerts, the agent checks its dependencies in the graph, flags a possible monitoring gap, names the owners, and opens nothing. It uses zero LLM tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I ran out of tokens.&lt;/strong&gt; Groq's free tier gives about 200k tokens per model per day, and a full investigation costs about 11k. Two test runs used up a whole day. I added a model fallback chain, an answer cache, and token logging, so the demo keeps working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment had its own surprises.&lt;/strong&gt; My &lt;code&gt;.env&lt;/code&gt; still pointed at localhost while I was trying to reach FalkorDB Cloud, and I once pasted the port into the host field, so it tried to connect to &lt;code&gt;...cloud:59676:59676&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live app:&lt;/strong&gt; (&lt;a href="https://rootcauseagent.streamlit.app" rel="noopener noreferrer"&gt;https://rootcauseagent.streamlit.app&lt;/a&gt;)&lt;br&gt;
💻 &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/beyondions/rootcause-agent" rel="noopener noreferrer"&gt;https://github.com/beyondions/rootcause-agent&lt;/a&gt;&lt;br&gt;
🎥 &lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=YDL6ZGoFmOY" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=YDL6ZGoFmOY&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try &lt;em&gt;"Orders are failing. What's the root cause and who do I wake up?"&lt;/em&gt;, then &lt;em&gt;"Is postgres healthy?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Right now it's one scripted outage on synthetic data with a fixed clock. The real next step is feeding it a real service map (for example from OpenTelemetry) and real deploy history, then seeing whether the same rule holds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Four days ago I didn't know Cypher. Now I understand why some questions are graph questions, and why an agent that answers at 2 AM should take its facts from data, not from the model's imagination.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;strong&gt;WeMakeDevs&lt;/strong&gt; and &lt;strong&gt;FalkorDB&lt;/strong&gt; for the push.&lt;/p&gt;

&lt;h1&gt;
  
  
  FalkorDB #GraphHacks #WeMakeDevs #AIAgents #GraphDatabase #SRE
&lt;/h1&gt;

</description>
      <category>agents</category>
      <category>database</category>
      <category>wemakedev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
