<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Arihant Jain]]></title><description><![CDATA[Arihant Jain]]></description><link>https://arihantt.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 23:15:14 GMT</lastBuildDate><atom:link href="https://arihantt.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[My ERP Said Everything Was Fine. A Flood Three Tiers Away Said Otherwise.]]></title><description><![CDATA[How I built DOMINO, a FalkorDB agent that traces a disaster through every tier of a supply chain, tests the fix on copies of the world, and waits for a…]]></description><link>https://arihantt.hashnode.dev/my-erp-said-everything-was-fine-a-flood-three-tiers-away-said-otherwise</link><guid isPermaLink="true">https://arihantt.hashnode.dev/my-erp-said-everything-was-fine-a-flood-three-tiers-away-said-otherwise</guid><category><![CDATA[falkordb]]></category><category><![CDATA[AI]]></category><category><![CDATA[graph database]]></category><category><![CDATA[supply chain]]></category><category><![CDATA[hackathon]]></category><dc:creator><![CDATA[Arihant Jain]]></dc:creator><pubDate>Sun, 18 Oct 2026 06:50:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad46663d92c91860372828c/85683685-fdd7-4021-94f7-5d1361c565fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2>
<ul>
<li><p>Most supply-chain systems only see the suppliers you pay directly. Disasters rarely respect that boundary.</p>
</li>
<li><p>DOMINO models the whole chain as a graph in FalkorDB and follows a disruption hop by hop, down to the customer orders that will actually ship late.</p>
</li>
<li><p>It measures real capacity with max-flow, rejects "backup" suppliers that share the same failure, and tests five recovery plans inside <code>GRAPH.COPY</code> sandboxes.</p>
</li>
<li><p>An AI agent (Claude, over MCP) picks the tools. It never writes a query and never places an order on its own. A human approves; the database enforces the rest.</p>
</li>
<li><p>Across 40 simulated disruptions, the graph-aware plans left <strong>71 orders late versus 130</strong> for the same planner limited to a tier-1 view.</p>
</li>
</ul>
<p><strong>Links:</strong> <a href="https://domino-jiie.onrender.com">Live demo</a> · <a href="https://github.com/arihantjaino7/domino-agent">Code</a> · <a href="https://youtu.be/H7s2jJbKR4Y">Video</a></p>
<p><strong>Contents</strong></p>
<ol>
<li><p>The incident nobody's dashboard caught</p>
</li>
<li><p>Why this is a graph problem</p>
</li>
<li><p>Exposed is not late</p>
</li>
<li><p>How much can we really get? Ask max-flow</p>
</li>
<li><p>The backup that wasn't</p>
</li>
<li><p>Rehearsing the fix on a copy of the world</p>
</li>
<li><p>The agent proposes, a human decides</p>
</li>
<li><p>Memory that outlives a reset</p>
</li>
<li><p>Does the graph actually help? The numbers</p>
</li>
<li><p>Five things FalkorDB taught me</p>
</li>
<li><p>Running it</p>
</li>
<li><p>What's still missing</p>
</li>
</ol>
<hr />
<h2>1. The incident nobody's dashboard caught</h2>
<p>Here is the scenario DOMINO is built around. The companies are fictional; the geography and the failure mode are very real.</p>
<p><strong>Day 0.</strong> Monsoon flooding hits Ayutthaya, Thailand. Industrial estates close. Recovery is expected in three to five weeks.</p>
<p>Kestrel Devices, a laptop maker, checks its supplier list. Nobody it buys from is in Thailand. The tier-1 dashboard shows <strong>zero affected suppliers</strong>.</p>
<p><strong>Day 0, a few seconds later.</strong> DOMINO declares an incident on VoltCell Energy, a battery-cell factory in Ayutthaya, and follows the graph upstream to downstream. VoltCell sells cells to two of Kestrel's battery-pack suppliers, BatteryMax in Penang and PowerCell Packs in Hai Phong. <strong>38 customer orders</strong> depend on that chain.</p>
<p><strong>Day 11.</strong> The Penang plant runs out of 6-cell battery packs.</p>
<p><strong>Day 26.</strong> The Bac Ninh plant runs out of 4-cell packs.</p>
<p><strong>Day 28.</strong> VoltCell is back. Too late for <strong>13 orders</strong>, carrying $536,370 in late-delivery penalties.</p>
<p>What this tells us: the risk was never in the supplier list. It was two hops away, in a factory the company had no contract with. Any tool that stops at tier 1 is structurally blind to it.</p>
<h2>2. Why this is a graph problem</h2>
<p>A supply chain is a network, but most systems store it as rows of "who do I buy from". That's one hop. The flood was further out.</p>
<p>In DOMINO the whole chain is a single FalkorDB graph:</p>
<ul>
<li><p><strong>Sites</strong> that make parts, and which site <strong>FEEDS</strong> which.</p>
</li>
<li><p>Which sites <strong>SUPPLY</strong> which plants, with weekly capacity on the edge.</p>
</li>
<li><p>What each product <strong>REQUIRES</strong>, what each plant <strong>STOCKS</strong> and <strong>BUILDS</strong>.</p>
</li>
<li><p>Customers, the orders they <strong>PLACED</strong>, and where those orders are <strong>FULFILLED_AT</strong>.</p>
</li>
</ul>
<p>Once it's a graph, "who does this flood hurt?" is a path query:</p>
<pre><code class="language-cypher">MATCH path = (hit:Site)-[:FEEDS*0..4]-&gt;(t1:Site)-[:SUPPLIES]-&gt;(pl:Plant)
</code></pre>
<p>and from the plant on to products and orders. The path is not just the answer; it's the evidence. When DOMINO says an order is at risk, it can show the exact chain from the flooded factory to that order. In the war room UI those paths are labelled E1, E2, E3 and the agent has to cite them.</p>
<h2>3. Exposed is not late</h2>
<p>This distinction is where a lot of risk tooling loses people's trust.</p>
<p>38 orders are <strong>exposed</strong>: they depend on VoltCell somewhere upstream. But exposure isn't damage. A plant with enough stock rides out a 28-day outage. An order due after the factory reopens is fine.</p>
<p>So DOMINO projects the shortage day by day, per plant and per part: stock on hand, what is still arriving, when each order needs its parts. Only the orders that actually run dry count as <strong>projected late</strong>. That's 13, not 38.</p>
<p>If an agent cried wolf about all 38, planners would learn to ignore it within a week.</p>
<h2>4. How much can we really get? Ask max-flow</h2>
<p>The obvious next question: if we shift volume to the suppliers that still work, how much can we get?</p>
<p>The naive answer adds up tier-1 capacity. For 6-cell packs at Penang that's <strong>980 a week</strong>.</p>
<p>That number is wrong, because tier-1 suppliers need inputs, and inputs come from upstream sites with their own limits, some of them under water. DOMINO builds a small flow network from the graph, splits each site into an in-node and an out-node so site capacity counts too, and runs FalkorDB's <code>algo.maxFlow</code> on it.</p>
<p>The real figure is <strong>650 a week</strong>.</p>
<p>The min cut of the same network tells you <em>where</em> the bottleneck is. That's far more useful than a list of edges that happen to be saturated, which is what you get if you eyeball utilisation.</p>
<h2>5. The backup that wasn't</h2>
<p>When DOMINO looks for alternatives, it checks each candidate's upstream paths too.</p>
<p>AmpereOne in Johor Bahru looks like a great backup for 4-cell packs: approved, with spare capacity on paper. But it buys its cells from the same flooded VoltCell factory. Its <em>clean</em> capacity is zero, and DOMINO rejects it, with the evidence path attached.</p>
<p>The rule matters in both directions. Exposure alone is not a reason to reject: PowerCell Packs also touches VoltCell, but it still has healthy capacity, so it stays on the table with a warning. A source is rejected only when nothing clean is left.</p>
<p>The naive tier-1 plan in the demo does pick AmpereOne. It looks cheaper on paper, and it fails two of DOMINO's post-execution checks.</p>
<h2>6. Rehearsing the fix on a copy of the world</h2>
<p>Before anyone approves anything, DOMINO tests five strategies:</p>
<table>
<thead>
<tr>
<th>Plan</th>
<th>Late orders</th>
<th>Spend</th>
</tr>
</thead>
<tbody><tr>
<td>S0: do nothing</td>
<td>13</td>
<td>$0</td>
</tr>
<tr>
<td>S1: naive tier-1</td>
<td>7</td>
<td>$108,169</td>
</tr>
<tr>
<td>S2: lowest cost</td>
<td>3</td>
<td>$114,982</td>
</tr>
<tr>
<td>S3: max service</td>
<td>1</td>
<td>$162,569</td>
</tr>
<tr>
<td>S4: DOMINO balanced</td>
<td>1</td>
<td>$142,475</td>
</tr>
</tbody></table>
<p>Each one runs inside a copy of the live graph made with <code>GRAPH.COPY</code>. The plan executes for real in the copy, the same projection engine scores it, and the copy is dropped.</p>
<p>This is my favourite part of the design. There is no separate simulator that can drift out of sync with production logic. "What happens if we buy from these three suppliers?" becomes "do it in a sandbox and look", using the exact code that will later run against the real world.</p>
<p>The same trick powers the demo's <strong>Reset the world</strong> button: a pristine graph is copied over the live one.</p>
<h2>7. The agent proposes, a human decides</h2>
<p>The AI is deliberately on a short leash.</p>
<ul>
<li><p><strong>It chooses, it doesn't compute.</strong> The model (Claude, over MCP) picks from 16 fixed, parameterised tools: resolve the places in an alert, trace exposure, project shortages, compute capacity, find alternatives, simulate, submit a plan. Every number comes from a deterministic engine.</p>
</li>
<li><p><strong>It never writes Cypher.</strong> Every query is a checked-in file. Every ID the model passes is resolved against the graph first; anything unknown comes back as <code>unknown_entity</code>.</p>
</li>
<li><p><strong>It can only propose.</strong> <code>submit_plan_for_approval</code> pauses the run. A person approves or rejects in the war room UI. If they reject with feedback ("keep it under $120K"), the agent has to re-plan; resubmitting the same plan is refused.</p>
</li>
<li><p><strong>The database enforces the approval.</strong> It is bound to the plan's hash and to a world version stored in the graph. If anything changed since the simulation, the approval fails and the agent must simulate again.</p>
</li>
<li><p><strong>One atomic write.</strong> On approval, a single query marks the decision executed and creates the purchase orders, the allocations and an audit record. A unique constraint on <code>PurchaseOrder.id</code> makes duplicate orders impossible. Then nine verification queries check budget, capacity, material balance and receipt counts.</p>
</li>
<li><p><strong>The report gets audited too.</strong> If the agent cites an ID, an evidence path or a number that no tool returned during that run, the report is rejected and has to be fixed.</p>
</li>
</ul>
<p>In the Monsoon case the approved plan buys battery packs from three other suppliers for $142,475 and protects 12 of the 13 late orders.</p>
<p>Note the wording: <strong>protected (projected)</strong>. The purchase orders exist; the parts haven't arrived. I made the UI say exactly that, because "fulfilled" would be a lie.</p>
<h2>8. Memory that outlives a reset</h2>
<p>Every completed run writes lessons into a separate memory graph, linked to the sites and parts they're about. Resetting the world doesn't touch it.</p>
<p>Next time something hits Ayutthaya, the agent's <code>recall_history</code> call returns the earlier lesson with the date it was learned, for example that VoltCell is a hidden shared upstream of three tier-1 suppliers, and the agent has to check it against the current run.</p>
<p>It's honest to say what memory does <em>not</em> do yet: it informs the agent and the report, but it doesn't change the allocator's maths.</p>
<h2>9. Does the graph actually help? The numbers</h2>
<p>It's easy to say "graphs are better". I wanted a number.</p>
<p>The setup: 20 generated worlds, each hit by two disruptions, so <strong>40 cases and 4,766 orders</strong>. Every method runs on its own copy of the graph and is scored by the same projection.</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Late orders</th>
</tr>
</thead>
<tbody><tr>
<td>B0: do nothing</td>
<td>383</td>
</tr>
<tr>
<td>B1: tier-1 view</td>
<td>130</td>
</tr>
<tr>
<td>B2: graph-aware</td>
<td>71</td>
</tr>
<tr>
<td>S4: DOMINO balanced</td>
<td>57</td>
</tr>
</tbody></table>
<p>The interesting comparison is B1 against B2. They use <strong>the same allocator and the same budget</strong>. The only difference is that B2 can see upstream.</p>
<ul>
<li><p><strong>B1's plans break.</strong> It picked a source with zero clean capacity in 20 of the 40 cases, failed 40 verifier checks, and 88 of the orders it marked "protected" were still late.</p>
</li>
<li><p><strong>B2 never loses.</strong> Fewer late orders in 19 cases, a tie in 21, worse in 0.</p>
</li>
<li><p><strong>The gain is specific.</strong> In the factory-fire scenario every method ties at zero late orders, because there the backup supplier is genuinely independent. The graph pays off exactly when a backup shares an upstream site, which is the failure mode it was built for.</p>
</li>
</ul>
<p>To avoid grading my own homework, a separate checker recomputes everything from a raw export of the graph with NetworkX. It imports no DOMINO code. It agrees on every exposed order (998/998), every late order (386/386) and every capacity (85/85).</p>
<h2>10. Five things FalkorDB taught me</h2>
<p>A few lessons I'd pass on to anyone building on FalkorDB:</p>
<ol>
<li><p><strong>Alias every relationship you count or sum.</strong> With two parallel edges between the same nodes, a pattern that doesn't name the relationship returned one row in my tests; naming it returned two. Unnamed edges in an aggregate give you wrong totals without any error.</p>
</li>
<li><p><strong>A MATCH that misses is silent.</strong> In a batched <code>UNWIND</code> loader, a row whose endpoint doesn't exist is simply dropped. Return a count and compare it with the batch size.</p>
</li>
<li><p><code>algo.maxFlow</code> <strong>only returns edges that carry flow.</strong> If the max flow is zero, you get no rows at all. Treat "empty" as zero, not as a failure.</p>
</li>
<li><p><code>GRAPH.COPY</code> <strong>carries indexes and constraints across</strong>, so a sandbox is ready to query the moment it exists.</p>
</li>
<li><p><strong>Use</strong> <code>GRAPH.RO_QUERY</code> <strong>for every read.</strong> "This tool can't change anything" becomes a database guarantee instead of a code-review promise.</p>
</li>
</ol>
<h2>11. Running it</h2>
<p>Everything runs in one container: FalkorDB listening only inside it, the FastAPI backend, the MCP server and the React war room. A full agent run peaks at about 160 MB of memory.</p>
<p>The war room has four modes:</p>
<ul>
<li><p><strong>Replay</strong> plays back a recorded Claude Code run, step by step, without touching the graph.</p>
</li>
<li><p><strong>Demo</strong> runs a scripted agent through the real tools, the approval pause and the verification.</p>
</li>
<li><p><strong>Claude Code</strong> lets you drive DOMINO from your own terminal over MCP. It's one command:</p>
</li>
</ul>
<pre><code class="language-bash">claude mcp add --transport http --scope user domino https://domino-jiie.onrender.com/mcp/ --header "Authorization: Bearer YOUR_KEY"
</code></pre>
<p>Then <code>/mcp__domino__monsoon</code> inside Claude Code hands it the alert together with the agent's playbook, which the server itself provides. The war room shows each tool call as it happens, including the Cypher behind it.</p>
<ul>
<li><strong>Claude API</strong> runs the agent inside the backend when an Anthropic key is configured.</li>
</ul>
<p>One deployment war story: my uptime monitor reported the site down for an hour while it was serving traffic perfectly. The monitor checked with <code>HEAD</code> requests, and the health endpoint only answered <code>GET</code>, so every check returned 404. One line fixed it. It's a good reminder that "down" is whatever your probe says it is.</p>
<h2>12. What's still missing</h2>
<p>DOMINO is a hackathon build, so here are the limits, stated plainly:</p>
<ul>
<li><p><strong>The data is synthetic.</strong> Real use means loading a bill of materials, approved-vendor lists, sub-tier supplier maps and open orders into the same schema.</p>
</li>
<li><p><strong>The allocator is greedy</strong>, not optimal.</p>
</li>
<li><p><strong>Everyone shares one live world.</strong> Per-user worlds are a <code>GRAPH.COPY</code> away but not built yet.</p>
</li>
<li><p><strong>"Not in the graph" never means "safe".</strong> When an alert names a place DOMINO doesn't model, like a port strike in Antwerp, it says exactly that instead of reporting no impact.</p>
</li>
</ul>
<p>Next on the list: single-point-of-failure scans (knock out each site in turn and re-run max-flow), route search over the logistics network with FalkorDB's path algorithms, and an import path for real data.</p>
<p>If you work in supply chain and the Ayutthaya scenario feels familiar, I'd like to hear how you get tier-2 visibility today. My guess is: spreadsheets, and a lot of phone calls.</p>
]]></content:encoded></item></channel></rss>