<?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: SAUD SATOPAY</title>
    <description>The latest articles on DEV Community by SAUD SATOPAY (@saudsatopay).</description>
    <link>https://dev.to/saudsatopay</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%2F4192229%2F75587e4b-723a-44c7-9137-3fa59492b225.jpg</url>
      <title>DEV Community: SAUD SATOPAY</title>
      <link>https://dev.to/saudsatopay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saudsatopay"/>
    <language>en</language>
    <item>
      <title>I shut down the L train in a FalkorDB graph: 23,445 riders, one min cut, and a fix tested on a fork</title>
      <dc:creator>SAUD SATOPAY</dc:creator>
      <pubDate>Sun, 18 Oct 2026 14:23:47 +0000</pubDate>
      <link>https://dev.to/saudsatopay/i-shut-down-the-l-train-in-a-graph-max-flow-min-cut-and-ai-agents-on-the-new-york-subway-40g7</link>
      <guid>https://dev.to/saudsatopay/i-shut-down-the-l-train-in-a-graph-max-flow-min-cut-and-ai-agents-on-the-new-york-subway-40g7</guid>
      <description>&lt;p&gt;Picture 8 a.m. on a Wednesday in October. A signal fire in the Canarsie Tube stops the L between Bedford Av and 8 Av, in both directions, until further notice.&lt;/p&gt;

&lt;p&gt;That one sentence breaks the trips of &lt;strong&gt;23,445 riders an hour&lt;/strong&gt;. A transit control room now has minutes to answer three questions: who is stuck, where will they go, and can the rest of the network carry them?&lt;/p&gt;

&lt;p&gt;A trip planner finds one rider a route; this is a flow problem. So for the WeMakeDevs × FalkorDB Graph Hacks hackathon I built &lt;strong&gt;Detour&lt;/strong&gt;: agents on a FalkorDB graph of the real subway that measure what the network can still carry (&lt;strong&gt;17,167 of 18,432&lt;/strong&gt; riders an hour toward Manhattan, with the bottleneck at &lt;strong&gt;1 Av&lt;/strong&gt;) and test a shuttle on a forked copy of the graph before anyone is told. The graph computes every number; an independent NetworkX program re-checks them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One rule shaped everything: the graph computes, and the model only chooses tools and explains.&lt;/strong&gt; No number in Detour comes from a language model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The real subway in FalkorDB, built from MTA open data: 445 station complexes, 1,459 scheduled segments with capacity, and 30,205 origin-destination flows.&lt;/li&gt;
&lt;li&gt;When a line breaks, agents call typed graph tools. A &lt;code&gt;MATCH&lt;/code&gt; finds who is hit, &lt;code&gt;algo.SPpaths&lt;/code&gt; shows where they go, &lt;code&gt;algo.maxFlow&lt;/code&gt; measures what the network can still carry and its min cut names the bottleneck, and &lt;code&gt;GRAPH.COPY&lt;/code&gt; forks test shuttle plans.&lt;/li&gt;
&lt;li&gt;Memory, conversations and the agency's own records (people, tickets, chats, decisions) live in the same graph, with dates, and they change the next answer.&lt;/li&gt;
&lt;li&gt;An independent NetworkX program recomputes every number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://detour-orpin.vercel.app" rel="noopener noreferrer"&gt;https://detour-orpin.vercel.app&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/SaudSatopay/detour" rel="noopener noreferrer"&gt;https://github.com/SaudSatopay/detour&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/11xHEZ07rlw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtostr7ij8uh72l32m7t.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtostr7ij8uh72l32m7t.gif" alt="A run of the L closure in Detour's control room: the controller's tool calls stream into the trace, the night map shows each step's evidence, and the G overload and max flow appear with their numbers." width="720" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The L closure in six questions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;How the graph answers&lt;/th&gt;
&lt;th&gt;For the L&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whose trips cross the closure?&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;MATCH&lt;/code&gt; on demand edges&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;23,445&lt;/strong&gt; riders an hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where do they go on their own?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;algo.SPpaths&lt;/code&gt;, every pair in one query&lt;/td&gt;
&lt;td&gt;The G at &lt;strong&gt;244%&lt;/strong&gt;; &lt;strong&gt;4,689&lt;/strong&gt; riders with no rail path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What can the network still carry?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;algo.maxFlow&lt;/code&gt; on a fork&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;17,167 of 18,432&lt;/strong&gt; toward Manhattan (93%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where is the bottleneck?&lt;/td&gt;
&lt;td&gt;The min cut&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 Av&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does a shuttle help?&lt;/td&gt;
&lt;td&gt;The same tools on a &lt;code&gt;GRAPH.COPY&lt;/code&gt; fork&lt;/td&gt;
&lt;td&gt;Riders with no path: &lt;strong&gt;4,689 → 623&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did memory change the plan?&lt;/td&gt;
&lt;td&gt;Past incidents stored in the graph&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;27%&lt;/strong&gt; more buses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Putting the subway in a graph
&lt;/h2&gt;

&lt;p&gt;Everything comes from MTA open data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GTFS static.&lt;/strong&gt; Weekday trains departing between 8:00 and 8:59 give trains per hour on every directed segment. Multiplied by planning loads per car, that is capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stations and complexes.&lt;/strong&gt; 445 complexes, with accessibility flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 2025 origin-destination ridership estimate.&lt;/strong&gt; A Wednesday in October at 8 a.m.: 446,230 riders an hour. I keep the 30,205 station pairs with at least two riders, which carry 90.5% of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A build script assigns every pair to its fastest route (in-vehicle time, half the headway as the wait, a transfer penalty), which gives every segment a baseline load. Only one segment comes out over capacity at baseline: the 5 from Grand Central to Union Square, at 101%. That was my sign the model was sane.&lt;/p&gt;

&lt;p&gt;The model is small on purpose:&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="c1"&gt;// scheduled segments&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Station&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;:RUNS&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="k"&gt;load&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&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="nc"&gt;:Station&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// complexes under 400 m apart&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Station&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;:WALK&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;meters&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capacity&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="nc"&gt;:Station&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// 8 a.m. flows; via = the segment ids of the path&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Station&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;:DEMAND&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;riders&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;via&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="nc"&gt;:Station&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// a platform layer, so waits and transfers cost minutes&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Station&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;:BOARD&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="nc"&gt;:Platform&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;:RIDE&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="nc"&gt;:Platform&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Platform&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;:ALIGHT&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="nc"&gt;:Station&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And each job gets its own graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detour:template       the network and seeded memory, never changed
detour:sbx:&amp;lt;visitor&amp;gt;  your own copy, made with GRAPH.COPY
detour:fork:&amp;lt;...&amp;gt;     a throwaway copy for each what-if
detour:demand         the 30,205 flows, shared and read-only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Who is hit? One MATCH
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;DEMAND&lt;/code&gt; edge keeps the segment ids of its 8 a.m. path in a &lt;code&gt;via&lt;/code&gt; property. That one property turns the first question into a single query:&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="py"&gt;o:&lt;/span&gt;&lt;span class="n"&gt;Station&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="py"&gt;d:&lt;/span&gt;&lt;span class="n"&gt;DEMAND&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="py"&gt;x:&lt;/span&gt;&lt;span class="n"&gt;Station&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;d.via&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$closed&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;o.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d.riders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;riders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the L between Bedford Av and 8 Av, that is 23,445 riders an hour across 2,021 station pairs. Only 7,823 of them board at the closed stations; counting outward, 14,713 board within four stops of the closure. An alert sent only to the closed stations would miss two thirds of the people affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Where do they go? Shortest paths that price a transfer
&lt;/h2&gt;

&lt;p&gt;Riders don't spread out politely. They take their fastest open path. To make waits and transfers cost something, the graph has a platform layer: a &lt;code&gt;Platform&lt;/code&gt; node per station and route, &lt;code&gt;BOARD&lt;/code&gt; edges that cost a three-minute transfer penalty plus half the headway, &lt;code&gt;RIDE&lt;/code&gt; edges along the scheduled segments, and free &lt;code&gt;ALIGHT&lt;/code&gt; edges. Then every displaced pair is rerouted in one query:&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;UNWIND&lt;/span&gt; &lt;span class="n"&gt;$pairs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&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="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Station&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;p.o&lt;/span&gt;&lt;span class="ss"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Station&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;p.d&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;algo.SPpaths&lt;/span&gt;&lt;span class="ss"&gt;({&lt;/span&gt;&lt;span class="py"&gt;sourceNode:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;targetNode:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="py"&gt;relTypes:&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'BOARD'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'RIDE'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ALIGHT'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'WALK'&lt;/span&gt;&lt;span class="ss"&gt;],&lt;/span&gt; &lt;span class="py"&gt;weightProp:&lt;/span&gt; &lt;span class="s1"&gt;'minutes'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;YIELD&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathWeight&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p.o&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p.d&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathWeight&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 2,021 pairs reroute in under a second. Left to themselves, L riders pile onto the G: four G segments run at 190 to 244% of capacity, and 4,689 riders an hour have no open rail path at all.&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%2F2jysz85946xfmafvcd2v.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%2F2jysz85946xfmafvcd2v.png" alt="Detour's home page: the G runs at 244% of capacity, and the night map shows displaced riders' fastest paths overloading the G in red." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: What can the network still carry? Max flow and the min cut
&lt;/h2&gt;

&lt;p&gt;Fastest paths show what riders &lt;em&gt;will&lt;/em&gt; do. Max flow shows what the network &lt;em&gt;could&lt;/em&gt; do if they spread out perfectly.&lt;/p&gt;

&lt;p&gt;Each call forks the visitor's graph with &lt;code&gt;GRAPH.COPY&lt;/code&gt;, deletes the closed segments, frees the capacity the displaced riders were using, and wires a super source and a super sink to every station by its balance of displaced riders, once per direction of travel. Then:&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="py"&gt;src:&lt;/span&gt;&lt;span class="n"&gt;Hub&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$src&lt;/span&gt;&lt;span class="ss"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;snk:&lt;/span&gt;&lt;span class="n"&gt;Hub&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$snk&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;algo.maxFlow&lt;/span&gt;&lt;span class="ss"&gt;({&lt;/span&gt;&lt;span class="py"&gt;sourceNodes:&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="ss"&gt;],&lt;/span&gt; &lt;span class="py"&gt;targetNodes:&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;snk&lt;/span&gt;&lt;span class="ss"&gt;],&lt;/span&gt;
  &lt;span class="py"&gt;relationshipTypes:&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'FLOW'&lt;/span&gt;&lt;span class="ss"&gt;],&lt;/span&gt; &lt;span class="py"&gt;capacityProperty:&lt;/span&gt; &lt;span class="s1"&gt;'spare'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;YIELD&lt;/span&gt; &lt;span class="n"&gt;maxFlow&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edgeFlows&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;maxFlow&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edgeFlows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;algo.maxFlow&lt;/code&gt; takes a single relationship type and no parallel edges, and a subway corridor is full of parallel edges: the 4, 5 and 6 run between the same stations, plus walks. So the fork sums the spare capacity per station pair into one &lt;code&gt;FLOW&lt;/code&gt; edge and keeps the ids of the segments behind it. That maps the flow and the cut back to real lines.&lt;/p&gt;

&lt;p&gt;The cut comes from the residual graph: starting at the super source, the tool follows every edge that still has spare capacity, or flow it could push back, and the cut is every edge from a station it reached to one it didn't.&lt;/p&gt;

&lt;p&gt;The answer for the L: &lt;strong&gt;17,167 of the 18,432 riders an hour headed into Manhattan can still move. That's 93%.&lt;/strong&gt; The min cut sits at &lt;strong&gt;1 Av&lt;/strong&gt;. Its only rail links are the closed L, so no spare capacity anywhere else can reach its riders. That is exactly where a shuttle bus should start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test the fix on a fork before anyone is told
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GRAPH.COPY&lt;/code&gt; makes a what-if cheap. The controller looks for shuttle bridges within 2 km of the stations the network can't clear. Then it copies the closure fork once per plan, adds the bus legs and runs the same tools again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1 Av ↔ Astor Pl at 25 buses an hour:&lt;/strong&gt; riders with no path drop from 4,689 to &lt;strong&gt;623&lt;/strong&gt;, and the max-flow shortfall from 1,266 to &lt;strong&gt;315&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A two-leg plan does no better, with 18 more buses.&lt;/strong&gt; One leg wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Riders an hour are rounded to whole riders, which is why 18,432 minus 17,167 shows up as a 1,266 shortfall.)&lt;/p&gt;

&lt;p&gt;One result surprised me. With the shuttle, fastest-path overflow &lt;em&gt;rises&lt;/em&gt;, from 24,078 to 28,762, because 4,066 riders who had no way through can now travel. So the plan also needs the rider desk to steer people onto lines with room. And for the other two closures in the demo, the Lexington express and the Cranberry Street Tunnel, the open network absorbs everyone, so the controller proposes steering riders and no buses at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents that never write Cypher
&lt;/h2&gt;

&lt;p&gt;The model's job is to choose tools and explain the results, not to invent queries or numbers. The agents pick from a fixed catalogue of 13 typed tools, backed by 83 queries. Each tool returns one of four answer shapes (answered, from memory, not in the graph, unknown name), a one-line summary, the data, and the exact Cypher it ran with its timings. The control room shows all of it, step by step.&lt;/p&gt;

&lt;p&gt;Here is the recorded L run, with the time each tool spent in FalkorDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool                queries     time  result
resolve_places            2    10 ms  Bedford Av, 14 St/8 Av, the L
recall                    3    10 ms  elevator outage; June shuttle at 79%
close_line                6     9 ms  10 L segments closed
affected_riders          10   152 ms  23,445 riders an hour, 2,021 pairs
measure_capacity         19   811 ms  4 segments overflow, G at 244%
shuttle_candidates       16   220 ms  1 Av -&amp;gt; Astor Pl, 0.8 km
simulate_plan            24  1551 ms  no-path riders 4,689 -&amp;gt; 623
simulate_plan            24  1404 ms  two legs, 26 buses: no better
remember                  4     3 ms  "the G overloads first"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine tool calls, 108 queries, 4.2 seconds inside FalkorDB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dispatcher
    |
    v
controller agent --- 13 typed tools, 83 queries ---&amp;gt; FalkorDB
    |
    v
a person approves
    |  HANDED_OFF
    v
rider desk: one agent per remembered rider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing reaches riders until a person approves a plan. Then the controller hands off to the rider desk, a &lt;code&gt;HANDED_OFF&lt;/code&gt; edge records it, and one rider agent per remembered rider writes a new route, step-free where that rider needs it.&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%2Fpv05i28tjmf34b8xrk99.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%2Fpv05i28tjmf34b8xrk99.png" alt="After approval: two shuttle plans tested on forked graphs, the recommended one published to riders, and a rider agent routing Tomás around the closure." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Detour runs on free model tiers, and Gemini's free tier allows only a few full runs a day. So I also wrote the controller's procedure as code. When the quota runs out, the same tools run in the same order on the visitor's graph, and the run is labeled &lt;em&gt;scripted&lt;/em&gt;. When the database is unreachable, a run recorded on FalkorDB plays back, labeled as a recording. The same tools are also served over MCP, so other agents can use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory that changes the answer
&lt;/h2&gt;

&lt;p&gt;Memory is not a log beside the graph. It hangs off the same nodes the agents already query:&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="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Agent&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;:WROTE&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="nc"&gt;:Finding&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validFromMs&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validToMs&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Finding&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;:ABOUT&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="nc"&gt;:Station&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;:Route&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Finding&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;:SUPERSEDES&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="nc"&gt;:Finding&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are two moments where it changes what the agents do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A plan sized by experience.&lt;/strong&gt; A past incident remembers that the June bus bridge carried 79% of its planned riders, so the controller sizes the new shuttle 27% larger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A route that follows the facts.&lt;/strong&gt; Aisha uses a wheelchair. Memory says the elevator to the L platform at Myrtle-Wyckoff Avs is out, so her step-free route takes the M: 33 minutes. Report it fixed and the old fact gets an end date (it is not deleted), and a new fact supersedes it. Her next route takes the L: 24 minutes.&lt;/li&gt;
&lt;/ul&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%2F0m6hxkil58dfobqc8747.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%2F0m6hxkil58dfobqc8747.png" alt="Aisha's step-free route: the L in 24 minutes now, and the M in 33 minutes before the memory changed." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because closed facts stay in the graph, the memory page can draw every fact over its validity window, next to incidents and agent steps.&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%2Fkbj5i72senw68bn88jp7.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%2Fkbj5i72senw68bn88jp7.png" alt="Memory over time: each fact as a bar over its validity window, grouped by line, with past incidents and a closed elevator fact." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conversations are memory too. Every question to the rider desk or the brain becomes a &lt;code&gt;Turn&lt;/code&gt; node, chained by &lt;code&gt;NEXT&lt;/code&gt;, linked to the stations it mentions and to the nodes its answer cited. The agents read their history back from the graph rather than from the browser, so a visitor who comes back the next day picks up the same conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A company brain for the agency
&lt;/h2&gt;

&lt;p&gt;The agency that runs the response lives in the same graph. Its teams, people, documents (after-action reports, playbooks, procedures, notices), maintenance tickets, chat messages and decisions are nodes linked to the stations, routes, incidents and findings the agents already use. They are sample records; the subway is real. The &lt;a href="https://detour-orpin.vercel.app/brain" rel="noopener noreferrer"&gt;brain page&lt;/a&gt; answers the questions a new dispatcher would ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who owns the Myrtle-Wyckoff elevator outage?&lt;/strong&gt; &lt;code&gt;OWNS&lt;/code&gt; and &lt;code&gt;MAINTAINS&lt;/code&gt; edges give the teams and their people, the open ticket gives the assignee, and authorship ranks who knows it best.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why do L tube bus bridges get 27% more buses?&lt;/strong&gt; The decision is traced back two hops: to the earlier decision, chat message and finding it rests on, and from that finding to the after-action report and ticket it came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed this week?&lt;/strong&gt; Every dated change in the graph, from facts learned to tickets resolved and decisions lifted.
&lt;/li&gt;
&lt;/ul&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="py"&gt;d:&lt;/span&gt;&lt;span class="n"&gt;Decision&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$id&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;:BASED_ON&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="py"&gt;f:&lt;/span&gt;&lt;span class="n"&gt;Finding&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;:SOURCED_FROM&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;s&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;f.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;s.id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every answer cites the nodes it came from, and it stays current: report Aisha's elevator fixed, and in the same request the outage fact is closed, its ticket resolves, and the decision to route step-free riders around that platform is lifted.&lt;/p&gt;

&lt;p&gt;It takes knowledge in, too. Paste &lt;em&gt;Elevator to the A platform at Jay St-MetroTech is stuck between floors&lt;/em&gt; into &lt;strong&gt;Add to the brain&lt;/strong&gt;. The message is linked to the station and the A, ticket EE-1208 is filed with the elevator maintenance lead, and the outage goes into memory, so step-free routes avoid that platform at once. A second report joins the same ticket instead of opening another, and &lt;em&gt;back in service&lt;/em&gt; resolves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't trust the graph: check it
&lt;/h2&gt;

&lt;p&gt;I didn't want to trust a number just because a database produced it. A separate Python program built on NetworkX reads the same data files, follows the same written spec, and recomputes everything. FalkorDB and NetworkX agree on the max flow in all six direction groups of the three closures, and on the displaced-rider counts at every depth, and the checker confirms that each min cut's capacity matches its flow. The proof page shows them side by side.&lt;/p&gt;

&lt;p&gt;Every visitor also gets their own sandbox graph, so I load-tested the multi-tenancy. 48 reader agents and 6 writers across 6 tenant graphs sustained &lt;strong&gt;7,195 reads and 719 writes a second, with zero errors&lt;/strong&gt;, and a closure in one tenant never showed up in another.&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%2Fqb3d84nernb2plwu1tyh.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%2Fqb3d84nernb2plwu1tyh.png" alt="The proof page: FalkorDB and NetworkX side by side for every closure." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FalkorDB, and not just NetworkX?
&lt;/h2&gt;

&lt;p&gt;NetworkX is the auditor: it recomputes everything in a batch job, after the fact. FalkorDB is where the agents work, live and per visitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every visitor gets a graph.&lt;/strong&gt; A sandbox is a &lt;code&gt;GRAPH.COPY&lt;/code&gt; of the template, and a what-if is a copy of that, so plans are tested side by side with no locking and no visitor ever sees another's closure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The algorithms run where the data lives.&lt;/strong&gt; 2,021 displaced pairs reroute in one &lt;code&gt;UNWIND&lt;/code&gt; query, max flow runs inside the request, and the whole L run takes 4.2 seconds of database time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One graph holds everything.&lt;/strong&gt; Stations, demand, memory, conversations and the agency's records are linked, so one query can go from a ticket to a station to a rider's route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every answer can be audited.&lt;/strong&gt; Each tool returns the Cypher it ran with its timing, and the control room shows it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demand is the MTA's estimate for an October Wednesday at 8 a.m., not live counts, and capacity uses planning loads per car.&lt;/li&gt;
&lt;li&gt;Max flow is the best case, with riders spread perfectly; fastest paths are the worst case. Reality is somewhere between.&lt;/li&gt;
&lt;li&gt;NetworkX checks the implementation against the same written spec, not the model against reality.&lt;/li&gt;
&lt;li&gt;The agency's people, tickets, chats and decisions are sample records.&lt;/li&gt;
&lt;li&gt;Free model tiers run out. Then the standard procedure answers, and with the database down a recorded run plays; both are labeled.&lt;/li&gt;
&lt;li&gt;The intake reads text with rules (station names, lines, outage words), not a language model, so a station written in an unusual way can be missed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What broke, and what I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose a problem where the algorithm is the product.&lt;/strong&gt; "Can the network absorb the L's riders?" is literally a max flow, and "why not?" is a min cut. A language model can't fake either one, and that kept the agents honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the expensive question in a property.&lt;/strong&gt; Storing each flow's path on its &lt;code&gt;DEMAND&lt;/code&gt; edge turned "who is affected?" into one &lt;code&gt;MATCH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variable-length paths timed out.&lt;/strong&gt; &lt;code&gt;-[:RUNS*0..8]-&amp;gt;&lt;/code&gt; exploded on corridors with many parallel routes. Walking a frontier one hop per query takes milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;algo.SSpaths&lt;/code&gt; was not what I thought.&lt;/strong&gt; It returns k paths in total, not one per target; one &lt;code&gt;SPpaths&lt;/code&gt; call per pair under &lt;code&gt;UNWIND&lt;/code&gt; was what I needed. And &lt;code&gt;algo.maxFlow&lt;/code&gt; wants one relationship type and no parallel edges, so I build &lt;code&gt;FLOW&lt;/code&gt; edges and keep their provenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forks make what-ifs and tenancy simple.&lt;/strong&gt; &lt;code&gt;GRAPH.COPY&lt;/code&gt; gave me a sandbox per visitor and a throwaway graph per plan, with no locking logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close facts; don't delete them.&lt;/strong&gt; Validity windows plus &lt;code&gt;SUPERSEDES&lt;/code&gt; let the next answer read only what is true now, while keeping the history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SET&lt;/code&gt; came back &lt;code&gt;NOPERM&lt;/code&gt;.&lt;/strong&gt; On FalkorDB Cloud's free tier my database user could run graph commands but not plain Redis ones, so rate limits, the seed lock and the sandbox registry moved into a small &lt;code&gt;detour:meta&lt;/code&gt; graph, with &lt;code&gt;MERGE&lt;/code&gt; doing the locking. Free model quotas run out too, so the controller's procedure also exists as code, and every run says which mode it used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An approval landed in the wrong sandbox.&lt;/strong&gt; An end-to-end test on a phone viewport caught a race: a new visitor's first page load could mint two visitor ids. A Next.js proxy now sets the id before any route runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd love to see in FalkorDB
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;algo.maxFlow&lt;/code&gt; with several relationship types, parallel edges and the min-cut edges in its output.&lt;/li&gt;
&lt;li&gt;A bounded breadth-first procedure that returns hop distances.&lt;/li&gt;
&lt;li&gt;Copy-on-write forks, so a fork per what-if would be nearly free.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;Break the L in the &lt;a href="https://detour-orpin.vercel.app/control?scenario=l-canarsie&amp;amp;run=1" rel="noopener noreferrer"&gt;control room&lt;/a&gt;, open any step to see its evidence and Cypher, and approve the plan.&lt;/li&gt;
&lt;li&gt;On the &lt;a href="https://detour-orpin.vercel.app/brain" rel="noopener noreferrer"&gt;brain page&lt;/a&gt;, ask &lt;em&gt;Why do L tube bus bridges get 27% more buses?&lt;/em&gt;, then paste an elevator outage into &lt;strong&gt;Add to the brain&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Check every number in this post on the &lt;a href="https://detour-orpin.vercel.app/proof" rel="noopener noreferrer"&gt;proof page&lt;/a&gt;, next to NetworkX's.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code, the graph model and every query are at &lt;a href="https://github.com/SaudSatopay/detour" rel="noopener noreferrer"&gt;https://github.com/SaudSatopay/detour&lt;/a&gt;. For Aisha's route change, report her elevator fixed on the riders page in a fresh sandbox (&lt;strong&gt;Reset my sandbox&lt;/strong&gt; in the control room): with the L closed, her route stays on the M whatever memory says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack and AI disclosure
&lt;/h2&gt;

&lt;p&gt;Next.js 16, FalkorDB 4.22 on FalkorDB Cloud, the Vercel AI SDK with free Gemini and Groq models, and MTA open data. I built Detour with Claude Code as a pair programmer, and it helped me draft this post. The graph model, the queries and every decision are documented in the repo.&lt;/p&gt;

&lt;p&gt;If you have ever had to reroute a city, or broken a network on purpose: what would you fork and test first? And if Detour was useful to you, a star on GitHub helps.&lt;/p&gt;

</description>
      <category>falkordb</category>
      <category>ai</category>
      <category>algorithms</category>
      <category>graphhacks</category>
    </item>
  </channel>
</rss>
