<?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: Abhishek.M. Shivanagoudar</title>
    <description>The latest articles on DEV Community by Abhishek.M. Shivanagoudar (@abhishiv17).</description>
    <link>https://dev.to/abhishiv17</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%2F4154361%2F263c8675-2b9a-4a2e-a63f-8192020537b3.jpg</url>
      <title>DEV Community: Abhishek.M. Shivanagoudar</title>
      <link>https://dev.to/abhishiv17</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhishiv17"/>
    <language>en</language>
    <item>
      <title>"KindRed: when a kidney swap breaks, the graph finds another way (built on FalkorDB)"</title>
      <dc:creator>Abhishek.M. Shivanagoudar</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:20:33 +0000</pubDate>
      <link>https://dev.to/abhishiv17/kindred-when-a-kidney-swap-breaks-the-graph-finds-another-way-built-on-falkordb-27di</link>
      <guid>https://dev.to/abhishiv17/kindred-when-a-kidney-swap-breaks-the-graph-finds-another-way-built-on-falkordb-27di</guid>
      <description>&lt;p&gt;Sunita is 48 and lives in Hyderabad. Her kidneys are failing, and her husband wants to give her one of his. He can't: she carries antibodies against his tissue type, so her body would reject the kidney. Worse, she's highly sensitised. Her antibodies react to most donors, so only a handful of people in any pool could ever give to her.&lt;/p&gt;

&lt;p&gt;Somewhere in another city there's a family with the mirror-image problem. If Sunita's husband gives to their patient and their donor gives to Sunita, two people get a transplant. That's a paired exchange, a swap. With three families it becomes a ring: A's donor gives to B, B's to C, C's back to A.&lt;/p&gt;

&lt;p&gt;We built &lt;strong&gt;KindRed&lt;/strong&gt;, a paired exchange for kidney and liver across hospitals in India, on FalkorDB. This post walks through one swap breaking and the graph repairing it, the agent that runs a patient's case week by week, and the things FalkorDB taught us along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://3o3ntcusu2.execute-api.ap-south-1.amazonaws.com" rel="noopener noreferrer"&gt;https://3o3ntcusu2.execute-api.ap-south-1.amazonaws.com&lt;/a&gt; (no sign-up)&lt;br&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/abhishiv17/Graph-hacks" rel="noopener noreferrer"&gt;https://github.com/abhishiv17/Graph-hacks&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Video:&lt;/strong&gt; see below&lt;/p&gt;

&lt;p&gt;Built for Graph Hacks 2026 (WeMakeDevs × FalkorDB), Tracks 1 and 2.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Everything in KindRed is synthetic.&lt;/strong&gt; Sunita and Rajesh are demo personas with generated lab histories. No real patient or hospital record has been near this system. It's a software prototype, not a clinical tool, and nothing here is medical advice.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h2&gt;
  
  
  Why India needs this
&lt;/h2&gt;

&lt;p&gt;India meets about 7% of its kidney transplant need and about 1.5% of its liver need (&lt;em&gt;Lancet Regional Health - Southeast Asia&lt;/em&gt;, 2026). In 24 years the country has done 1,839 kidney exchange transplants across 65 centres, and 259 liver exchanges across 7 (Kute et al., 2025). Most of those swaps happened inside one hospital's small pool.&lt;/p&gt;

&lt;p&gt;A bigger, shared pool finds more swaps. But finding the swap is only half of it. Each planned donation still has to pass a real lab test, a crossmatch, where the patient's blood is mixed with the donor's cells. If one test in a 3-way ring comes back positive, the whole ring fails and somebody starts again by phone.&lt;/p&gt;

&lt;p&gt;KindRed is built around that moment: one failed match, and what happens next.&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%2Fyzogtawmw2fcnyo3lssy.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%2Fyzogtawmw2fcnyo3lssy.png" alt=" " width="799" height="365"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;What our year-long simulation says a shared, failure-aware pool could change. Simulation numbers, not clinical ones.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Swaps are cycles
&lt;/h2&gt;

&lt;p&gt;"Which donors are blood group O?" is a table question. "Who can swap with whom?" is a path question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 2-way swap is a cycle of length 2: A can give to B, and B can give back to A.&lt;/li&gt;
&lt;li&gt;A 3-way swap is a cycle of length 3: A → B → C → A.&lt;/li&gt;
&lt;li&gt;"Why can't this donor give to this patient?" is a 3-hop path: donor → tissue type ← antibody ← patient.&lt;/li&gt;
&lt;li&gt;"Who could replace the family that just dropped out?" is a search over a neighbourhood whose size you don't know ahead of time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In SQL each of those is another self-join. In a graph they're patterns you write down directly. So the model came out small:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thing&lt;/th&gt;
&lt;th&gt;In the graph&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A family (patient + willing donor)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Pair&lt;/code&gt; node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"This donor could give to that patient"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CAN_GIVE_TO&lt;/code&gt; edge, with its reasons stored on it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A swap&lt;/td&gt;
&lt;td&gt;A cycle of &lt;code&gt;CAN_GIVE_TO&lt;/code&gt; edges, plus a &lt;code&gt;Loop&lt;/code&gt; node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A failed lab test&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;FAILED&lt;/code&gt; edge, kept forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A repair&lt;/td&gt;
&lt;td&gt;A local search a couple of hops around the break&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2F5y8a2bnocizttm2keofz.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%2F5y8a2bnocizttm2keofz.png" alt=" " width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  One FalkorDB server, many graphs
&lt;/h2&gt;

&lt;p&gt;FalkorDB is the only database in KindRed. One server holds many graphs, and we use them as tenants:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Graph&lt;/th&gt;
&lt;th&gt;What's in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exchange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The shared pool: families, blood groups, tissue types, antibodies, &lt;code&gt;CAN_GIVE_TO&lt;/code&gt; links, swaps, offers, crossmatches, failures, decisions, tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hosp_&amp;lt;city&amp;gt;&lt;/code&gt; (11 of them)&lt;/td&gt;
&lt;td&gt;Each hospital's private data: names, documents, full tissue typing, lab results. Only de-identified facts reach &lt;code&gt;exchange&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mem_sunita&lt;/code&gt;, &lt;code&gt;mem_rajesh&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Each demo patient's memory: findings, beliefs, playbooks, sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chat_&amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Each family's conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;copy&amp;gt;__exchange&lt;/code&gt;, …&lt;/td&gt;
&lt;td&gt;A visitor's private copy of all of the above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fx7nttpaqnb5ozca2pw2e.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%2Fx7nttpaqnb5ozca2pw2e.png" alt=" " width="799" height="364"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The shared exchange graph as the How it works page draws it. The matching rules live in the graph too: antibodies &lt;code&gt;REACTS_WITH&lt;/code&gt; the tissue types they attack, and a failed test is a &lt;code&gt;FAILED&lt;/code&gt; edge that is never proposed again.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When a family joins, a named query (Q02 for kidney, Q21 for liver) builds its &lt;code&gt;CAN_GIVE_TO&lt;/code&gt; edges and writes the reasons onto each one. Here's a real edge from our run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P-0211 → P-0388   p_success 0.56   status: failed
  "B → B · same blood group"
  "virtual test clear: no antibody at or above 2000 MFI against this donor"
  "donor DQ not typed: higher risk of a positive lab test"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the third reason. Bengaluru's donor was never typed for DQ, so the virtual check couldn't see a DQ clash, and the success estimate dropped to 56% before any lab test was run. This is the edge that fails later in the story.&lt;/p&gt;

&lt;p&gt;All the Cypher lives in 59 numbered files in &lt;code&gt;backend/queries/&lt;/code&gt; (Q00 to Q59). Any query without a write clause runs through &lt;code&gt;GRAPH.RO_QUERY&lt;/code&gt;, so FalkorDB itself refuses a write from it, whoever calls it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-hop queries
&lt;/h2&gt;

&lt;p&gt;The virtual crossmatch is three hops: does the patient have an antibody, at or above 2,000 MFI, against any of the donor's tissue types?&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;// Q03 Virtual crossmatch: donor antigen &amp;lt;- antibody &amp;lt;- patient, at or above the MFI threshold.&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="nc"&gt;:Pair&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;pair_id:&lt;/span&gt; &lt;span class="n"&gt;$donor_pair&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;:HAS_DONOR&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;d:&lt;/span&gt;&lt;span class="n"&gt;Donor&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Pair&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;pair_id:&lt;/span&gt; &lt;span class="n"&gt;$patient_pair&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;:HAS_PATIENT&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;r:&lt;/span&gt;&lt;span class="n"&gt;Patient&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&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="n"&gt;d&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;:HAS_ANTIGEN&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;h:&lt;/span&gt;&lt;span class="n"&gt;HLAAntigen&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:REACTS_WITH&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;:Antibody&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;k:&lt;/span&gt;&lt;span class="n"&gt;HAS_ANTIBODY&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="n"&gt;r&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;k.mfi&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;$mfi&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;({&lt;/span&gt;&lt;span class="py"&gt;antigen:&lt;/span&gt; &lt;span class="n"&gt;h.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;mfi:&lt;/span&gt; &lt;span class="n"&gt;k.mfi&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;clashes&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d.untyped_loci&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;untyped_loci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The family explorer draws exactly that path. On Kolkata's P-0388, the red links show why the family's own donor can't give: the patient has antibodies against DQ8 (MFI 8,800), A1 (9,800) and B8 (11,200), all carried by that donor.&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%2Ftb5959h8icp32z5oqb70.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%2Ftb5959h8icp32z5oqb70.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Hovering one red link: "Anti-DQ8 → DQ8. This antibody attacks that tissue marker."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A 3-way swap is a 3-hop cycle over live edges, all in one program:&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;// Q05 All 3-way loops a -&amp;gt; b -&amp;gt; c -&amp;gt; a, each listed once (a has the smallest id).&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;Pair&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;e1:&lt;/span&gt;&lt;span class="n"&gt;CAN_GIVE_TO&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;b:&lt;/span&gt;&lt;span class="n"&gt;Pair&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;e2:&lt;/span&gt;&lt;span class="n"&gt;CAN_GIVE_TO&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;c:&lt;/span&gt;&lt;span class="n"&gt;Pair&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;e3:&lt;/span&gt;&lt;span class="n"&gt;CAN_GIVE_TO&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;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a.organ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b.organ&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b.organ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c.organ&lt;/span&gt; &lt;span class="n"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$organ&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;a.organ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;$organ&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'waiting'&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'waiting'&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;c.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'waiting'&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;e1.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'live'&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;e2.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'live'&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;e3.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'live'&lt;/span&gt;
  &lt;span class="n"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$ids&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;a.pair_id&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$ids&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;b.pair_id&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$ids&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;c.pair_id&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$ids&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a.pair_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.pair_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c.pair_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;pairs&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e1.p_success&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e2.p_success&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e3.p_success&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;probs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The organ check on every node is why a kidney family and a liver family can never land in the same ring. Each edge's success estimate comes back with the loop, and the optimiser multiplies them into the chance the whole swap holds.&lt;/p&gt;

&lt;p&gt;Why bother with 3-way swaps at all? Ask KindRed answers "how far can this family's donor reach?" at any depth up to 3. For Kolkata's P-1015, the donor reached 3 waiting families in one donation and 82 within three (3 at one hop, 69 at two, 10 at three). For a hard-to-match patient, that difference is the whole case for 3-way swaps.&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%2Fu36l87gj6rerw8yz7xlt.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%2Fu36l87gj6rerw8yz7xlt.png" alt=" " width="799" height="364"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Answered live by &lt;code&gt;gpt-oss-120b&lt;/code&gt; in 2.1 s, with the graph paths behind the answer and every id checked.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FalkorDB's built-in algorithms are tools the agents call: &lt;code&gt;algo.WCC&lt;/code&gt; splits the pool into parts that can be matched independently (Q07), &lt;code&gt;algo.betweenness&lt;/code&gt; finds bridge families whose withdrawal would break the most swaps (Q19), and &lt;code&gt;algo.pageRank&lt;/code&gt; ranks how easy each family is to place, so the matching agent can put the hardest first (Q59).&lt;/p&gt;
&lt;h2&gt;
  
  
  One failed match, end to end
&lt;/h2&gt;

&lt;p&gt;We ran this live in a private copy. Every number below came back from the API in that run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plan.&lt;/strong&gt; The pool holds 198 synthetic families (154 kidney, 44 liver) across 11 hospitals. The matching agent finds candidate loops in the graph, and a CP-SAT optimiser (OR-Tools) picks the set with the most expected transplants. This week's plan has 32 swaps, 85 donations and 64.1 expected transplants. One of them is &lt;strong&gt;S-0076: Hyderabad → Bengaluru → Kolkata&lt;/strong&gt;, with a 46% chance every link holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A test fails.&lt;/strong&gt; A new family registers in Chennai, and the graph builds its compatibility links. Then the crossmatch coordinator runs S-0076's lab tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hyderabad → Bengaluru   negative
Bengaluru → Kolkata     positive   (DQ7 antibody, MFI 9000)
Kolkata → Hyderabad     cancelled  (the loop broke first)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the risky edge from earlier. The failure is written to the graph as a &lt;code&gt;Failure&lt;/code&gt; node that &lt;code&gt;BROKE&lt;/code&gt; the loop and a &lt;code&gt;FAILED&lt;/code&gt; edge between the two families, so that donation is never proposed again. No rule engine needed: every future search simply doesn't walk that edge.&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%2Fpqrrjns0suuq9eocjzij.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%2Fpqrrjns0suuq9eocjzij.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The &lt;code&gt;FAILED&lt;/code&gt; edge between Bengaluru's and Kolkata's families, both &lt;code&gt;IN_LOOP&lt;/code&gt; of S-0076, and the &lt;code&gt;Failure&lt;/code&gt; node that &lt;code&gt;BROKE&lt;/code&gt; it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repair locally.&lt;/strong&gt; The matching agent doesn't re-plan the whole country. It reads the failure memory first (Q54), then searches only 2 hops around the three released families (Q11), keeps the swap's stored 2-way fallback in play (Q12), and ranks options with the same optimiser. In our run it searched 33 candidate loops in 55 ms and left the other 31 swaps alone.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Expected transplants&lt;/th&gt;
&lt;th&gt;Every link holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hyderabad → Bengaluru → Chennai (recommended)&lt;/td&gt;
&lt;td&gt;2.29 of 3&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored 2-way fallback (S-0076-FB)&lt;/td&gt;
&lt;td&gt;1.46 of 2&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hyderabad ⇄ Bengaluru&lt;/td&gt;
&lt;td&gt;1.46 of 2&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bengaluru → Chennai → Ahmedabad&lt;/td&gt;
&lt;td&gt;1.85 of 3&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The new Chennai family closes the ring.&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%2F85j3ll4atvzfso375wit.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%2F85j3ll4atvzfso375wit.png" alt=" " width="800" height="357"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;"Nothing changes for any family until you decide." Each link's reasons are read from the graph.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A person approves.&lt;/strong&gt; Nothing changes for any family until the exchange coordinator, a human, says yes. The repaired swap becomes S-0078, and the Hyderabad → Bengaluru test that already passed carries over instead of being run again. The decision is stored as &lt;code&gt;Decision -[:APPROVED_BY]-&amp;gt; Agent {name: 'Exchange coordinator (person)'}&lt;/code&gt;. Approving the same task twice returns a 409, and so does asking for more tests on the broken loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liver fails differently.&lt;/strong&gt; A liver donor gives part of their liver, and the graft has to be at least 0.8% of the patient's body weight. Until a CT scan measures the lobe, the exchange only has an estimate. In L-0034 (Delhi → Chennai → Kochi), the Chennai → Kochi link looked fine on paper at 0.84%. The CT measured the right lobe at 780 ml, 0.74% of the patient's 105 kg. That link failed, and the repair (28 ms) proposed the stored 2-way fallback, Delhi ⇄ Chennai. The graph had flagged it in advance: "Liver size is only estimated and close to the limit; the CT scan will decide."&lt;/p&gt;
&lt;h2&gt;
  
  
  An agent that runs the case, over MCP
&lt;/h2&gt;

&lt;p&gt;Two demo patients are followed week by week. Sunita's eGFR (kidney function) is falling; Rajesh's MELD score (how ill a liver patient is) is rising. Press &lt;strong&gt;Next week&lt;/strong&gt; and a new lab result arrives, the health watch assesses it, and the &lt;strong&gt;coordinator agent&lt;/strong&gt; decides what to do about the patient's swap.&lt;/p&gt;

&lt;p&gt;The coordinator is the one place where a language model drives an action. Each week it reads the patient's state and memory from the graph and picks one step: search for swaps (2-way first, then 3-way), form a swap, run tests, request papers, book surgery, or wait. It's a real MCP client. KindRed runs its own MCP server (37 tools) as a child process over stdio, and the coordinator lists and calls its case tools through the protocol. The model is Groq's &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;, falling back to &lt;code&gt;gpt-oss-20b&lt;/code&gt; when the first one is rate limited.&lt;/p&gt;

&lt;p&gt;We ran both patients from week 0 to transplant on the live deployment, in a fresh private copy, with Groq answering. Every tool call went over MCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sunita (kidney)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Health watch saw&lt;/th&gt;
&lt;th&gt;Coordinator did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;eGFR 19, stage → Prepare&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;find_swaps(2)&lt;/code&gt; → 4 found; &lt;code&gt;form_swap&lt;/code&gt; → S-0078, Chennai → Hyderabad → Mumbai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;eGFR 18&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;run_tests&lt;/code&gt; → every lab crossmatch passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;eGFR 17&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;request_papers&lt;/code&gt; → consent, ID proof and relationship proof from P-0377&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;eGFR 16, stage → Act now&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;book_surgery&lt;/code&gt; → 22 Nov&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Transplant done&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rajesh (liver)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Health watch saw&lt;/th&gt;
&lt;th&gt;Coordinator did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;MELD 20, stage → Prepare&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;find_swaps(2)&lt;/code&gt; → 0, &lt;code&gt;find_swaps(3)&lt;/code&gt; → 0, then the model's call failed and the rules ran the step: wait&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;MELD 22&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;find_swaps(2)&lt;/code&gt; → 0, &lt;code&gt;find_swaps(3)&lt;/code&gt; → 2; &lt;code&gt;form_swap&lt;/code&gt; → L-0037, Kochi → Bengaluru → Mumbai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;MELD 25&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;run_tests(L-0037)&lt;/code&gt; → donor work-ups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;MELD 26&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;request_papers&lt;/code&gt; → consent from P-5014, ID proof from P-0903&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;book_surgery&lt;/code&gt; → 29 Nov&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;MELD 27&lt;/td&gt;
&lt;td&gt;Transplant done, 29 Nov&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The presses of Next week we timed came back in about 2 seconds or less, model turns included.&lt;/p&gt;

&lt;p&gt;Rajesh's week 2 is the one we like most. The 2-way search found nothing. One family further, the 3-way search found two swaps, and the coordinator formed L-0037. A 2-way-only matcher would have left him waiting.&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%2F3bp1rh46lorjfclzzqnk.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%2F3bp1rh46lorjfclzzqnk.png" alt=" " width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The guardrails are code
&lt;/h3&gt;

&lt;p&gt;The model never writes Cypher and never sees the raw pool. What keeps it honest is plain code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One action per week, enforced by the MCP server.&lt;/strong&gt; Calls in the same week share a session, so the rule holds for any MCP client, not only ours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every tool checks the graph before it acts.&lt;/strong&gt; &lt;code&gt;find_swaps&lt;/code&gt; refuses if the patient isn't waiting; &lt;code&gt;run_tests&lt;/code&gt; refuses a loop that isn't this patient's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reason check.&lt;/strong&gt; The model writes a one-line reason for the care team. Every id in it must appear in that week's tool result, or the reason is replaced with the result itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rules as a safety net.&lt;/strong&gt; If no model is configured, or the model fails, the rules run the same tools and the trace says so.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;did&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;did&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pair_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;span class="n"&gt;stray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;‑&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s reason named &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, which this week&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s result doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t show, so it was replaced by the result.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We didn't stage any failures, and the run hit two anyway. In Rajesh's week 1, Groq returned an HTTP error after the two searches, and the rules ran the step instead (nothing was found, so: wait). In Sunita's week 1, the reason check fired. The search found four 2-way swaps, but &lt;code&gt;form_swap&lt;/code&gt; hands the final choice to the optimiser, which picked a 3-way swap with Chennai and Mumbai. The model's sentence named P-1046, which wasn't in that week's result, so the page shows the result instead of the model's words. We'll take a strict check that sometimes throws away a sentence over one that lets an invented id through.&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%2Fvclcaqd6ydr7jx4spd2k.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%2Fvclcaqd6ydr7jx4spd2k.png" alt=" " width="800" height="218"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;"The model's reason named P-1046, which this week's result doesn't show, so it was replaced by the result."&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Ask KindRed: tools, not generated Cypher
&lt;/h2&gt;

&lt;p&gt;Coordinators can ask questions in plain words: "What breaks if Bengaluru's P-0211 withdraws?" or "Which waiting families are hardest to match?". A model picks from 15 read-only graph tools, each a wrapper around named queries. Ids are resolved against the graph first, every answer shows its tool trace and the graph paths it rests on, and if the draft names an id no query returned, the raw results replace it.&lt;/p&gt;

&lt;p&gt;Asking about a family that doesn't exist gets the honest answer instead of "no impact":&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%2Fr7a6e1i1l9ci4ksgxk77.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%2Fr7a6e1i1l9ci4ksgxk77.png" alt=" " width="799" height="221"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The model picked &lt;code&gt;withdrawal_impact&lt;/code&gt;; the id lookup (Q36) found no such family, so nothing else ran.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Testing it live also turned up a bug. The model sometimes writes ids with a non-breaking hyphen, &lt;code&gt;P‑0433&lt;/code&gt; (U+2011) instead of &lt;code&gt;P-0433&lt;/code&gt;. Ask KindRed's id pattern only matched the ASCII hyphen, so for that answer nothing got checked. The coordinator already normalised that character; Ask KindRed didn't. Our checks had only been tested with friendly input.&lt;/p&gt;
&lt;h2&gt;
  
  
  Memory is more nodes
&lt;/h2&gt;

&lt;p&gt;Agent memory in KindRed isn't a separate store. It's more nodes and edges in the same FalkorDB server, one graph per patient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What happened:&lt;/strong&gt; each week the health watch writes a &lt;code&gt;Finding&lt;/code&gt; linked &lt;code&gt;BASED_ON&lt;/code&gt; the readings it used. In the exchange, failed tests are &lt;code&gt;Failure&lt;/code&gt; nodes and &lt;code&gt;FAILED&lt;/code&gt; edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the agents believe:&lt;/strong&gt; stage, trend and swap are &lt;code&gt;Fact&lt;/code&gt; nodes. A new belief doesn't overwrite the old one. It &lt;code&gt;SUPERSEDES&lt;/code&gt; it, and the old one stays with its date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to act:&lt;/strong&gt; each stage has a &lt;code&gt;Playbook&lt;/code&gt; of ordered &lt;code&gt;Step&lt;/code&gt; nodes, which the health watch reads from the graph before handing work out.
&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="c1"&gt;// Q46 Replace one belief. The old fact stays, marked not current, behind a SUPERSEDES edge.&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;me:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;persona_id:&lt;/span&gt; &lt;span class="n"&gt;$persona_id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&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="n"&gt;me&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;:KNOWS&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;old:&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;$key&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;current:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;old.value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;$value&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;me&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;:KNOWS&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;Fact&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;fact_id:&lt;/span&gt; &lt;span class="n"&gt;$fact_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;$key&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;value:&lt;/span&gt; &lt;span class="n"&gt;$value&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;text:&lt;/span&gt; &lt;span class="n"&gt;$text&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;at:&lt;/span&gt; &lt;span class="n"&gt;$at&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;current:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;FOREACH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="ss"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;x.current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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="n"&gt;x&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.fact_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fact_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fqd1gla91533dc8zwvnq0.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%2Fqd1gla91533dc8zwvnq0.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sunita's beliefs in &lt;code&gt;mem_sunita&lt;/code&gt;: each newer &lt;code&gt;Fact&lt;/code&gt; is linked to the one it replaced by a &lt;code&gt;SUPERSEDES&lt;/code&gt; edge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Agents never call each other. When the health watch sees a more urgent stage, it writes one &lt;code&gt;Task&lt;/code&gt; per playbook step, &lt;code&gt;ASSIGNED_TO&lt;/code&gt; the scheduler, the committee clerk or the patient liaison, and they pick the tasks up on their next turn. Every agent step also stores what it read first and who wrote it. Before searching, the matching agent read one failed-link memory ("P-0211 → P-0388 (positive_xm)") and skipped that link. The Agents page draws all of this live from FalkorDB, and no line on it joins two agents directly.&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%2F3p0zmhfmm9ri8m9rit6y.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%2F3p0zmhfmm9ri8m9rit6y.png" alt=" " width="800" height="340"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Each box is an agent and each cylinder is a FalkorDB graph. Lines are writes, reads and &lt;code&gt;Task&lt;/code&gt; hand-offs.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Many judges, one demo
&lt;/h2&gt;

&lt;p&gt;During judging, two people might press Next week on the same patient. So "Make my own copy" rebuilds the whole demo in the visitor's own graphs (&lt;code&gt;&amp;lt;copy&amp;gt;__exchange&lt;/code&gt;, &lt;code&gt;&amp;lt;copy&amp;gt;__mem_sunita&lt;/code&gt;, …). The copy id travels in a header, and a context variable routes every query to that copy's graphs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_world&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;world&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;real&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The real FalkorDB graph name for a logical one (&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hosp_hyd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mem_sunita&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) in the current world.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isolation doesn't depend on remembering a filter in every query. The query just runs against a different graph.&lt;/p&gt;

&lt;p&gt;We also load tested it. Every family write is version-checked (Q01). With 16 agents updating one family at the same moment (160 updates), the check refused 1,498 stale writes, which were retried, and lost 0 updates. The same run without the check lost 138.&lt;/p&gt;

&lt;p&gt;Through the whole API, using Artillery on one laptop, our first run managed 28 requests a second with a p95 of 25.6 s, and 575 of 1,590 visitors timed out. Profiling showed most of the time going to parsing FalkorDB replies in pure Python, and every visitor re-running the same whole-pool traversals. Adding &lt;code&gt;hiredis&lt;/code&gt; and a small read cache that any write to the exchange graph clears fixed both:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Requests/s&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;Visitors timed out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;25.6 s&lt;/td&gt;
&lt;td&gt;575 of 1,590&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With hiredis + cache&lt;/td&gt;
&lt;td&gt;51.6&lt;/td&gt;
&lt;td&gt;3.7 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Afterwards, all 1,046 chat messages were in the graph exactly once. When 20 people pressed Next week on one patient in the same second, 1 moved the journey and 19 were refused.&lt;/p&gt;

&lt;h2&gt;
  
  
  We stopped FalkorDB to check it's doing the work
&lt;/h2&gt;

&lt;p&gt;"The graph does the work" is a claim, so we stopped only the FalkorDB container and called the API. Health, pool, agents, patients, the family explorer and Ask KindRed all returned 500. No stale answers, and no model answering without the graph. The web app falls back to a clearly labelled recording of a real run, and its buttons say nothing can be rebuilt.&lt;/p&gt;

&lt;p&gt;When we started FalkorDB again, the API recovered by itself in about 16 seconds. All 57 graphs were still there, including the private copy and its two &lt;code&gt;FAILED&lt;/code&gt; edges, because FalkorDB wrote its snapshot on shutdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things FalkorDB taught us the hard way
&lt;/h2&gt;

&lt;p&gt;None of these threw an error, which is what made them expensive.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Results are capped at 10,000 rows by default.&lt;/strong&gt; A big pool has more 3-way loops than that, and the extras just weren't returned. One line in schema setup fixes it: &lt;code&gt;db().config_set("RESULTSET_SIZE", -1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A variable-length path can't take a parameter.&lt;/strong&gt; &lt;code&gt;[:CAN_GIVE_TO*1..$hops]&lt;/code&gt; isn't accepted, so Q33 uses &lt;code&gt;*1..3&lt;/code&gt; and filters with &lt;code&gt;WHERE length(path) &amp;lt;= $hops&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shortestPath&lt;/code&gt; only works in &lt;code&gt;RETURN&lt;/code&gt; or &lt;code&gt;WITH&lt;/code&gt;.&lt;/strong&gt; Q34 computes the route inside the return expression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An edge alias you never reference didn't get the constraints we expected.&lt;/strong&gt; In cycle patterns, Q57 now references every edge alias in &lt;code&gt;WHERE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;MATCH&lt;/code&gt; straight after a &lt;code&gt;WHERE&lt;/code&gt; could drop that filter.&lt;/strong&gt; The liver integrity checks put a &lt;code&gt;WITH&lt;/code&gt; after every filtered &lt;code&gt;MATCH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chained &lt;code&gt;OPTIONAL MATCH&lt;/code&gt; aggregations miscounted rows.&lt;/strong&gt; We split link counts into their own query and gather antibodies before the other optional matches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson: test graph queries against answers you already know, not just for "no exception".&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell another team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store the reason on the edge.&lt;/strong&gt; Putting "why this donation works" on &lt;code&gt;CAN_GIVE_TO&lt;/code&gt; gave us the hover explanations, the step-by-step walkthroughs, the decision trail and the risk flag that predicted the DQ7 failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember failures as structure.&lt;/strong&gt; A &lt;code&gt;FAILED&lt;/code&gt; edge is cheaper than any rule engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search locally.&lt;/strong&gt; A 2-hop repair took 55 ms and left 31 swaps alone. Re-planning the pool would have reshuffled families who were fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrain the action space, not just the prompt.&lt;/strong&gt; One action per week on the server, preconditions in every tool and an id check on the model's reason caught real failures. Prompt wording didn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Synthetic data only. The generator reproduces published match rates (Saidman et al., 2006), but this isn't a clinical tool.&lt;/li&gt;
&lt;li&gt;One backend process. The load test shows where that ends.&lt;/li&gt;
&lt;li&gt;Free Groq keys hit rate limits quickly. KindRed falls back to a second model, then to the rules.&lt;/li&gt;
&lt;li&gt;In our yearly simulation, failure-aware matching places fewer highly sensitised patients than a failure-blind matcher (39% vs 43%), because their links fail more often. Sunita is exactly that patient, and we haven't solved it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Open the &lt;a href="https://3o3ntcusu2.execute-api.ap-south-1.amazonaws.com" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; and press &lt;strong&gt;Make my own copy&lt;/strong&gt; in the top bar first, so your Next week presses don't move anyone else's patient. Then open &lt;strong&gt;Demo patients&lt;/strong&gt; and walk Sunita or Rajesh to transplant, or open the &lt;strong&gt;Live map&lt;/strong&gt; for the failed-swap story.&lt;/p&gt;

&lt;p&gt;To run it yourself you need Docker Desktop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/abhishiv17/Graph-hacks.git kindred
&lt;span class="nb"&gt;cd &lt;/span&gt;kindred
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app is on &lt;a href="http://localhost:3001" rel="noopener noreferrer"&gt;http://localhost:3001&lt;/a&gt;, FalkorDB Browser on &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; (try &lt;code&gt;MATCH (a:Pair)-[f:FAILED]-&amp;gt;(b:Pair) RETURN a, f, b&lt;/code&gt; on the &lt;code&gt;exchange&lt;/code&gt; graph), and the API docs on &lt;a href="http://localhost:8000/docs" rel="noopener noreferrer"&gt;http://localhost:8000/docs&lt;/a&gt;. The coordinator and Ask KindRed need a model: set &lt;code&gt;GROQ_API_KEY&lt;/code&gt; and &lt;code&gt;KINDRED_ASK_PROVIDER=groq&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, or point it at a local Ollama model. Everything else runs without a key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; FalkorDB · Python 3.11 + FastAPI · OR-Tools CP-SAT · MCP Python SDK · Groq (&lt;code&gt;gpt-oss-120b&lt;/code&gt;, &lt;code&gt;gpt-oss-20b&lt;/code&gt;) · Sarvam for voice · Next.js + Tailwind · Docker Compose · AWS (EC2, API Gateway, VPC link) · Artillery · 79 pytest tests&lt;/p&gt;

&lt;p&gt;Built by Team KindRed: Abhishek M Shivanagoudar, Afreen Hossain and Akshay R, for Graph Hacks 2026 by &lt;a href="https://www.wemakedevs.org/" rel="noopener noreferrer"&gt;WeMakeDevs&lt;/a&gt; and &lt;a href="https://www.falkordb.com/" rel="noopener noreferrer"&gt;FalkorDB&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>wemakedevs</category>
      <category>graphhacks</category>
      <category>falkordb</category>
    </item>
    <item>
      <title>Mergetober part 2: my plan was wrong twice, and a free trial caught it before the code did</title>
      <dc:creator>Abhishek.M. Shivanagoudar</dc:creator>
      <pubDate>Wed, 14 Oct 2026 23:23:14 +0000</pubDate>
      <link>https://dev.to/abhishiv17/mergetober-part-2-my-plan-was-wrong-twice-and-a-free-trial-caught-it-before-the-code-did-22g9</link>
      <guid>https://dev.to/abhishiv17/mergetober-part-2-my-plan-was-wrong-twice-and-a-free-trial-caught-it-before-the-code-did-22g9</guid>
      <description>&lt;p&gt;After my &lt;a href="https://dev.to/abhishekm_shivanagoudar/my-mergetober-story-building-a-bamboohr-connector-for-cognee-4d1n"&gt;BambooHR connector&lt;/a&gt;, I picked up a second Mergetober issue: a &lt;strong&gt;Customer.io connector for cognee&lt;/strong&gt;. Last time the lesson was "read the API before writing code". This time it was "&lt;strong&gt;test your plan before writing code&lt;/strong&gt;", because my plan was wrong in public, twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The issue
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;cognee&lt;/a&gt; turns your data into a knowledge graph that AI agents can search. Issue &lt;a href="https://github.com/topoteretes/cognee/issues/4786" rel="noopener noreferrer"&gt;#4786&lt;/a&gt; asked for a connector for &lt;strong&gt;Customer.io&lt;/strong&gt;, a marketing messaging platform. The idea: if your team's emails, push notifications and in-app messages live in Customer.io, your agent should be able to answer "what does our French welcome email say?"&lt;/p&gt;

&lt;p&gt;I posted a plan on the issue: merge + tombstones like BambooHR, with a doc id of "action id + language" so translations wouldn't overwrite each other.&lt;/p&gt;

&lt;p&gt;Honestly, I didn't expect to get it. I'd asked a maintainer on Discord whether we could comment on several issues at once, since a lot of people were doing it. The answer was yes, but issues go to the best proposal, and people who already have an issue assigned are lower priority so everyone gets a chance. I already had BambooHR. A few other people had posted plans on this issue too.&lt;/p&gt;

&lt;p&gt;So I didn't comment on every issue I could find. I only commented on two in total, and I took my time on both plans.&lt;/p&gt;

&lt;p&gt;Yesterday I checked my mail as soon as I woke up: &lt;strong&gt;"Issue assigned to &lt;a class="mentioned-user" href="https://dev.to/abhishiv17"&gt;@abhishiv17&lt;/a&gt;"&lt;/strong&gt;. That felt really good. 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  A trial workspace before any code
&lt;/h2&gt;

&lt;p&gt;I signed up for a free Customer.io trial and built a small test automation: one email in Default, English and French, plus a newsletter. Then I wrote a throwaway script that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;saved every API response as raw JSON (&lt;code&gt;snapshot NAME&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;printed what appeared, disappeared or changed between two snapshots (&lt;code&gt;diff A B&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping the raw responses turned out to matter. Fields I wasn't even looking at showed up in the diffs, and that's how most of the surprises below got caught.&lt;/p&gt;

&lt;p&gt;A small tip if you try this: my trial account was &lt;strong&gt;"on hold"&lt;/strong&gt; until verified. That only blocks &lt;em&gt;sending&lt;/em&gt;. The App API worked fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong #1: translations
&lt;/h2&gt;

&lt;p&gt;My plan assumed one message comes back once per language. In reality, &lt;strong&gt;each language is its own action with its own id&lt;/strong&gt;, linked by a &lt;code&gt;multi_language_branch_action_id&lt;/code&gt;. The default language is &lt;code&gt;""&lt;/code&gt;, not &lt;code&gt;en&lt;/code&gt;. Adding "English" created a brand new action.&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;br&gt;&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%2Fs6191tcl81jqmwtmbocq.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%2Fs6191tcl81jqmwtmbocq.png" alt=" " width="570" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the action id alone was already unique, and my "id + language" key wasn't needed. But then I found a collision I hadn't planned for: a newsletter's content and a campaign message were &lt;strong&gt;both id &lt;code&gt;5&lt;/code&gt;&lt;/strong&gt;. They use separate counters. Doc ids got a type prefix: &lt;code&gt;action:5&lt;/code&gt;, &lt;code&gt;newsletter_content:5&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong #2: &lt;code&gt;deduplicate_id&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;My plan said &lt;code&gt;deduplicate_id&lt;/code&gt; "looks like id + timestamp" and proposed using it to detect changes. Then action 5 came back as &lt;code&gt;4:1792011300&lt;/code&gt;. The prefix was the branch id, not its own id.&lt;/p&gt;

&lt;p&gt;It got worse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publishing an edit to &lt;strong&gt;one&lt;/strong&gt; language bumped &lt;code&gt;updated&lt;/code&gt; on &lt;strong&gt;every&lt;/strong&gt; language, even ones I didn't touch.&lt;/li&gt;
&lt;li&gt;Adding a push step to the campaign bumped &lt;code&gt;updated&lt;/code&gt; on all the existing emails, with nothing else changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither field could tell me "this content changed".&lt;/p&gt;

&lt;h2&gt;
  
  
  The open question, and changing one thing at a time
&lt;/h2&gt;

&lt;p&gt;The issue suggested using the campaign's &lt;code&gt;updated&lt;/code&gt; timestamp as the sync cursor. I wasn't sure editing a message actually moves it, so I'd said I'd check.&lt;/p&gt;

&lt;p&gt;My first test was useless: in the same publish I edited a body &lt;strong&gt;and&lt;/strong&gt; added a language. The campaign's &lt;code&gt;updated&lt;/code&gt; moved, but I couldn't tell which change did it. I reran with a body-only edit. Result: &lt;strong&gt;the campaign's &lt;code&gt;updated&lt;/code&gt; doesn't move when you edit a message.&lt;/strong&gt; It only tracks structure (steps added or removed).&lt;/p&gt;

&lt;p&gt;Lesson: change one thing per test, or the test can't answer anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing the design (again)
&lt;/h2&gt;

&lt;p&gt;I first posted a fix on the issue: hash the message fields and re-ingest only when the hash changes. Then, while coding, it clicked.&lt;/p&gt;

&lt;p&gt;I had copied my own BambooHR design (merge + tombstones), but the reason it fit BambooHR was its &lt;strong&gt;change feed&lt;/strong&gt;. Customer.io doesn't have one. And since editing a message doesn't move the campaign timestamp, I have to list every message on every run anyway.&lt;/p&gt;

&lt;p&gt;So the connector became a &lt;strong&gt;full snapshot&lt;/strong&gt;, like cognee's Notion connector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every run lists everything, with &lt;code&gt;replace&lt;/code&gt;. Anything that disappeared upstream is forgotten by cognee's cleanup.&lt;/li&gt;
&lt;li&gt;Rows hold &lt;strong&gt;only the message text&lt;/strong&gt;, no &lt;code&gt;updated&lt;/code&gt; and no &lt;code&gt;deduplicate_id&lt;/code&gt;. cognee already hashes each row, so an untouched message keeps the same hash and isn't processed again, even when Customer.io bumps its timestamp.&lt;/li&gt;
&lt;li&gt;It works with cognee's default settings. No &lt;code&gt;write_disposition="merge"&lt;/code&gt; for users to forget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one rule this brings: &lt;strong&gt;if a listing fails, the run must abort&lt;/strong&gt;. A partial snapshot would make cognee forget messages that still exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The half-built SMS that broke everything
&lt;/h2&gt;

&lt;p&gt;Then that rule bit me. I added a push step and an SMS step but left them unfinished, since both needed a provider set up.&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;br&gt;&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%2F4b4zigg3wp8xhp13orm7.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%2F4b4zigg3wp8xhp13orm7.png" alt=" " width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Suddenly the campaign's actions list returned &lt;strong&gt;500&lt;/strong&gt;, every time. But each message still loaded fine on its own by id, except the empty SMS. Deleting the SMS step made the list work again.&lt;/p&gt;

&lt;p&gt;With "abort on a failed listing", one half-finished draft would have &lt;strong&gt;blocked syncing the whole workspace&lt;/strong&gt;. So now, on a 5xx, the connector fetches that campaign's messages one by one, and skips only the one that keeps failing. If nothing at all loads, the run aborts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning up the text
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;body_plain&lt;/code&gt; comes back empty, so the connector converts the HTML body to text and strips the &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt; and &lt;code&gt;script&lt;/code&gt; tags, plus the &lt;strong&gt;unsubscribe footer&lt;/strong&gt; Customer.io adds to every email. Otherwise every document in the graph would end with "Unsubscribe" (or "Se désabonner").&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;br&gt;&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%2Fc6esgmfrwotezg2sw0tn.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%2Fc6esgmfrwotezg2sw0tn.png" alt=" " width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few more things the trial showed: unpublished edits never reach the API (good, only published content goes in), a deleted newsletter just disappears and returns 404, and webhooks are refused because their bodies can carry tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;36 unit tests&lt;/strong&gt; with a mocked API that copies the real response shapes, including a real load into SQLite.&lt;/li&gt;
&lt;li&gt;I &lt;strong&gt;broke the code on purpose&lt;/strong&gt;: the footer stripping, the 5xx fallback and the abort guard, one at a time. The tests caught each one. A test suite that passes on the first try proves nothing until you've seen it fail.&lt;/li&gt;
&lt;li&gt;I also ran my own code over my saved snapshots. That caught a &lt;strong&gt;wrong claim in my own notes&lt;/strong&gt; about which text had been published.&lt;/li&gt;
&lt;li&gt;A live run against the trial made 3 API calls and staged exactly what I'd typed in the UI. The empty push was skipped, and there was no "Unsubscribe" anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;channel&lt;/th&gt;
&lt;th&gt;language&lt;/th&gt;
&lt;th&gt;subject&lt;/th&gt;
&lt;th&gt;text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;action:2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;hello&lt;/td&gt;
&lt;td&gt;English version four&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;action:3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;fr&lt;/td&gt;
&lt;td&gt;bonjour&lt;/td&gt;
&lt;td&gt;Version française une&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;action:5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;en&lt;/td&gt;
&lt;td&gt;hello&lt;/td&gt;
&lt;td&gt;English version one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;action:9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;in-app&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;In-app body one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then end to end on local &lt;strong&gt;Ollama&lt;/strong&gt; (&lt;code&gt;llama3.1:8b&lt;/code&gt; + &lt;code&gt;nomic-embed-text&lt;/code&gt;, CPU only): the first sync took about &lt;strong&gt;141s&lt;/strong&gt;, and the second sync with nothing changed took &lt;strong&gt;7s&lt;/strong&gt;, because nothing was re-processed.&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows gotchas (again)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git Bash rewrote my API path.&lt;/strong&gt; Passing &lt;code&gt;/v1/...&lt;/code&gt; as an argument turned it into &lt;code&gt;C:/Program Files/Git/v1/...&lt;/code&gt;. PowerShell didn't do that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Version franÃ§aise"&lt;/strong&gt; in my output looked like an encoding bug in cognee. Printing the stored text with &lt;code&gt;ascii()&lt;/code&gt; showed &lt;code&gt;\xe7&lt;/code&gt;, a correct &lt;code&gt;ç&lt;/code&gt;. It was just PowerShell decoding the output with the wrong code page. Check the bytes at each step before blaming the code.&lt;/li&gt;
&lt;li&gt;And yes, I ran &lt;code&gt;git init&lt;/code&gt; in the wrong folder again. 😅&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cognee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;customerio_source&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customerio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need an &lt;strong&gt;App API key&lt;/strong&gt; (not the Track API keys). Setup steps are in the package README.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your plan is a guess until you test it.&lt;/strong&gt; Correcting it in public with evidence is better than quietly changing the design in the PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't copy a design without its reason.&lt;/strong&gt; Merge fit BambooHR because of the change feed. No feed here meant a snapshot was simpler.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change one thing per test.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One broken item shouldn't stop everything.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only claim what you've run.&lt;/strong&gt; I removed two claims from the README that I hadn't actually tested.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The PR is up and waiting for review: &lt;a href="https://github.com/topoteretes/cognee-community/pull/388" rel="noopener noreferrer"&gt;cognee-community#388&lt;/a&gt;. Hoping it gets merged! 🤞&lt;/p&gt;

&lt;p&gt;Thank you &lt;a href="https://github.com/dexters1" rel="noopener noreferrer"&gt;@dexters1&lt;/a&gt; for assigning me the issue, and to the WeMakeDevs community for Mergetober. On to the next one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR: &lt;a href="https://github.com/topoteretes/cognee-community/pull/388" rel="noopener noreferrer"&gt;https://github.com/topoteretes/cognee-community/pull/388&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/topoteretes/cognee/issues/4786" rel="noopener noreferrer"&gt;https://github.com/topoteretes/cognee/issues/4786&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 1 (BambooHR): &lt;a href="https://dev.to/abhishekm_shivanagoudar/my-mergetober-story-building-a-bamboohr-connector-for-cognee-4d1n"&gt;https://dev.to/abhishekm_shivanagoudar/my-mergetober-story-building-a-bamboohr-connector-for-cognee-4d1n&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;cognee: &lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;https://github.com/topoteretes/cognee&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mergetober</category>
      <category>cognee</category>
      <category>wemakedevs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>"My Mergetober story: building a BambooHR connector for cognee"</title>
      <dc:creator>Abhishek.M. Shivanagoudar</dc:creator>
      <pubDate>Wed, 07 Oct 2026 22:27:58 +0000</pubDate>
      <link>https://dev.to/abhishiv17/my-mergetober-story-building-a-bamboohr-connector-for-cognee-4d1n</link>
      <guid>https://dev.to/abhishiv17/my-mergetober-story-building-a-bamboohr-connector-for-cognee-4d1n</guid>
      <description>&lt;p&gt;This Mergetober I wanted to do more than fix a typo. I wanted to build something real, get it reviewed, and actually understand every line of it. I ended up building a &lt;strong&gt;BambooHR connector for cognee&lt;/strong&gt;, and this post is the story of how it went, mistakes included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the issue
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;cognee&lt;/a&gt; is an open source project that turns your data into a knowledge graph AI agents can search, basically long-term memory for agents. It uses "connectors" to pull data in from tools like Notion, Gmail and Google Drive.&lt;/p&gt;

&lt;p&gt;While browsing the issues I found &lt;a href="https://github.com/topoteretes/cognee/issues/4795" rel="noopener noreferrer"&gt;#4795&lt;/a&gt;: add a connector for &lt;strong&gt;BambooHR&lt;/strong&gt;, an HR platform. The idea was simple: if your company's employee directory and handbooks are in BambooHR, your AI agent should be able to answer "who works in Sales?" or "how many days of leave do we get?"&lt;/p&gt;

&lt;p&gt;The issue suggested copying the existing Notion connector. Before commenting, I read through BambooHR's API docs and noticed something important: BambooHR has a &lt;strong&gt;change feed&lt;/strong&gt;, an endpoint that tells you exactly which employees were added, updated or deleted since a given time.&lt;/p&gt;

&lt;p&gt;The Notion connector re-downloads everything every run and &lt;em&gt;replaces&lt;/em&gt; the old data, because Notion has no such feed. If I copied that approach but only fetched changes, every employee who didn't change would get wiped from memory. So in my comment I proposed using the change feed with a &lt;strong&gt;merge&lt;/strong&gt; strategy instead, like the Gmail and Google Drive connectors do. I also asked what should happen to employees who leave the company.&lt;/p&gt;

&lt;p&gt;That comment got me assigned. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up (and immediately messing up)
&lt;/h2&gt;

&lt;p&gt;My first hour was mostly Git mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I forked &lt;strong&gt;cognee&lt;/strong&gt; instead of &lt;strong&gt;cognee-community&lt;/strong&gt;. The issue lives in the main repo, but connectors actually go in the community repo.&lt;/li&gt;
&lt;li&gt;I ran &lt;code&gt;git init&lt;/code&gt; in the parent folder instead of inside the cloned repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not my proudest moment, but it's the kind of thing you only do once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a BambooHR trial account
&lt;/h2&gt;

&lt;p&gt;I didn't have access to a real company's BambooHR (and wouldn't want to test on one anyway), so I signed up for a &lt;strong&gt;free BambooHR trial&lt;/strong&gt;. It comes preloaded with around 118 fake employees and about 20 company files like handbooks and policies, which turned out to be perfect for testing.&lt;/p&gt;

&lt;p&gt;I spent a while just calling the API by hand to see what it actually returns. That's where I learned two things that shaped the whole design:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The first sync doesn't need a separate endpoint.&lt;/strong&gt; If you ask the change feed for everything since &lt;code&gt;1970-01-01&lt;/code&gt;, every employee comes back as "Inserted". One code path handles both the first sync and every sync after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BambooHR returns only the employee id unless you ask for specific fields.&lt;/strong&gt; That meant I could make the connector private by default: it only ever requests safe fields like name, job title and department. Salary, SSN, date of birth and home address are never even fetched.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Building it
&lt;/h2&gt;

&lt;p&gt;The connector syncs two things: the &lt;strong&gt;employee directory&lt;/strong&gt; and &lt;strong&gt;company files&lt;/strong&gt; (PDF, TXT, MD, CSV).&lt;/p&gt;

&lt;p&gt;For employees, it uses the change feed and stores BambooHR's own timestamp as a cursor for the next run. That cursor is only saved once the whole run succeeds, so a crash halfway doesn't skip anything. Deleted employees are removed, and terminated employees (which BambooHR marks as &lt;code&gt;Inactive&lt;/code&gt;) are forgotten by default too.&lt;/p&gt;

&lt;p&gt;Files were trickier because BambooHR has no change feed for them. So each run lists all files, compares them with the previous run to spot deletions, and lets cognee skip files whose content hasn't changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing with Ollama on Windows
&lt;/h2&gt;

&lt;p&gt;I didn't want to pay for API calls while experimenting, so I ran everything locally with &lt;strong&gt;Ollama&lt;/strong&gt;, using &lt;code&gt;llama3.1:8b&lt;/code&gt; as the LLM and &lt;code&gt;nomic-embed-text&lt;/code&gt; for embeddings. This is where most of my debugging time went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ollama models "downloaded" but didn't exist.&lt;/strong&gt; &lt;code&gt;ollama pull&lt;/code&gt; said success, yet &lt;code&gt;ollama list&lt;/code&gt; was empty. It turned out Ollama 0.40.0 stores model manifests as symlinks, and Windows 11 refused to follow them. Replacing the two symlinks with real copies of the files fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows' 260-character path limit.&lt;/strong&gt; cognee's vector store creates deeply nested folders, and my long project path pushed it over the limit. Moving the data to a short folder fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model returned the schema instead of the data.&lt;/strong&gt; cognee asks the LLM for JSON in a fixed shape, and the small 8B model sometimes replied with the &lt;em&gt;description&lt;/em&gt; of the shape instead. Adding this to &lt;code&gt;.env&lt;/code&gt; solved it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LLM_INSTRUCTOR_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"json_schema_mode"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it all worked, I loaded a few mock employees and a leave policy and asked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Who works in the Sales department?"&lt;/strong&gt; → it named the right person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How many days of paid leave do employees get?"&lt;/strong&gt; → &lt;em&gt;"every employee gets 24 days of paid leave per year."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I deleted that Sales employee upstream, synced again, and watched cognee's document count drop from 4 to 3. Seeing it forget someone was honestly the most satisfying moment of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing against the real trial
&lt;/h2&gt;

&lt;p&gt;Next I ran the connector against my BambooHR trial four times, making changes in between:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Employees fetched&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;Stored&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 (first sync)&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;Deleted and inactive sample employees skipped&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Picked up my edits&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;One deleted, one terminated&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Caught an employee I terminated &lt;em&gt;while run 3 was still running&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run 4 was a nice confirmation that saving the cursor only at the end really works: nothing slipped through.&lt;/p&gt;

&lt;p&gt;The trial also surprised me. One of the sample files was a &lt;strong&gt;Benefits CSV with per-employee data&lt;/strong&gt; in it. My field allowlist protects the employee directory, but it can't protect against a file like that. So I added a &lt;code&gt;file_categories&lt;/code&gt; option to let you choose exactly which file folders get synced. Another sample PDF turned out to be genuinely corrupt, so I made sure broken files are skipped with a warning instead of crashing the run.&lt;/p&gt;

&lt;p&gt;Alongside the live tests I wrote &lt;strong&gt;33 unit tests&lt;/strong&gt; with a mocked API, so reviewers can run them without a BambooHR account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening the PR
&lt;/h2&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/topoteretes/cognee-community/pull/282" rel="noopener noreferrer"&gt;the PR&lt;/a&gt;… and CI failed immediately. The repo requires a &lt;strong&gt;DCO sign-off&lt;/strong&gt; on every commit, which I'd never heard of. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;--signoff&lt;/span&gt; upstream/main
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;

&lt;p&gt;If your team uses BambooHR, here's all it takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages/connector/bamboohr &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv &lt;span class="nb"&gt;sync
export &lt;/span&gt;&lt;span class="nv"&gt;BAMBOOHR_COMPANY_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acme   &lt;span class="c"&gt;# from https://acme.bamboohr.com&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BAMBOOHR_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...           &lt;span class="c"&gt;# BambooHR → your name → API Keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cognee&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cognee_community_connector_bamboohr&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bamboohr_source&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cognee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;bamboohr_source&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bamboohr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;write_disposition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# important: cognee's default is replace
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cognee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Who works in the Sales department?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cognee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SearchType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GRAPH_COMPLETION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;datasets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bamboohr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;remember&lt;/code&gt; again anytime and only the changes are synced. You can also pass &lt;code&gt;include_inactive=True&lt;/code&gt; to keep former employees, or &lt;code&gt;file_categories=["Company Files"]&lt;/code&gt; to limit which files are synced.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the API before writing code.&lt;/strong&gt; One look at BambooHR's change feed changed the whole design, and it's what got me the issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A trial account beats mocks.&lt;/strong&gt; The broken PDF, the Benefits CSV and the inclusive timestamps only showed up with real-ish data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local LLMs are great for learning,&lt;/strong&gt; as long as you're ready to debug your OS along the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy is easier to design in than to bolt on.&lt;/strong&gt; Never requesting sensitive fields is safer than filtering them out later.&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%2F3fcjkfibv58n0t15cdy6.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%2F3fcjkfibv58n0t15cdy6.png" alt=" " width="799" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to the cognee maintainers for the guidance on the issue, and to the WeMakeDevs community for Mergetober pushing me to ship something real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR: &lt;a href="https://github.com/topoteretes/cognee-community/pull/282" rel="noopener noreferrer"&gt;https://github.com/topoteretes/cognee-community/pull/282&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/topoteretes/cognee/issues/4795" rel="noopener noreferrer"&gt;https://github.com/topoteretes/cognee/issues/4795&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;cognee: &lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;https://github.com/topoteretes/cognee&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Mergetober #WeMakeDevs #OpenSource
&lt;/h1&gt;

</description>
      <category>mergetober</category>
      <category>wemakedevs</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building CampusEvac | How We Won Best UI at First Commit</title>
      <dc:creator>Abhishek.M. Shivanagoudar</dc:creator>
      <pubDate>Thu, 01 Oct 2026 10:07:30 +0000</pubDate>
      <link>https://dev.to/abhishiv17/building-campusevac-how-we-won-best-ui-at-first-commit-5deg</link>
      <guid>https://dev.to/abhishiv17/building-campusevac-how-we-won-best-ui-at-first-commit-5deg</guid>
      <description>&lt;p&gt;&lt;em&gt;Two perspectives, one shared emergency, and the engineering behind a real-time evacuation simulation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A fire evacuation is not just a problem of finding the nearest exit. It is also a problem of understanding what is happening across a building, communicating information, and coordinating people who see the same emergency from different perspectives.&lt;/p&gt;

&lt;p&gt;We wanted to explore that problem through a browser-based simulation.&lt;/p&gt;

&lt;p&gt;CampusEvac was our attempt to turn an evacuation drill into an interactive, real-time experience. One participant navigates the building as an evacuee, while another observes the scenario as a warden. Both interact with the same simulation, but neither sees it in quite the same way.&lt;/p&gt;

&lt;p&gt;We built CampusEvac during First Commit, a WeMakeDevs hackathon, and our project was recognised in the &lt;strong&gt;Best UI track&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a look at the problem we chose, the architecture behind the application, the design decisions that shaped the experience, and what building a real-time product taught us.&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%2F0g45l6lu5ex1hi9d3kj5.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%2F0g45l6lu5ex1hi9d3kj5.png" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build an Evacuation Simulator?
&lt;/h2&gt;

&lt;p&gt;Emergency preparedness involves more than documenting procedures. People need opportunities to understand their environment, practice decisions, and become familiar with how an evacuation might unfold.&lt;/p&gt;

&lt;p&gt;Physical drills serve an important purpose, but organising them requires coordination and a suitable environment. We wanted to explore how a browser-based simulation could provide another way to practise and understand evacuation scenarios.&lt;/p&gt;

&lt;p&gt;That led us to a question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if two people could participate in the same evacuation drill from completely different roles, directly in their browsers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer became CampusEvac.&lt;/p&gt;

&lt;p&gt;Instead of creating a static building map or a conventional monitoring dashboard, we designed a two-player experience with a shared environment and role-specific interfaces.&lt;/p&gt;

&lt;p&gt;The project had three central requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A navigable building environment for the evacuee.&lt;/li&gt;
&lt;li&gt;An overhead monitoring interface for the warden.&lt;/li&gt;
&lt;li&gt;Real-time communication so that both participants could interact with the same evolving simulation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These requirements shaped both our interface design and our technical architecture.&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%2Fxvlwcgcsmgfcopil023r.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%2Fxvlwcgcsmgfcopil023r.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&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%2F6i65ge76ookzfpjo71ih.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%2F6i65ge76ookzfpjo71ih.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Roles, Two Different Interfaces
&lt;/h2&gt;

&lt;p&gt;One of the most important decisions was to avoid treating the evacuee and warden as two versions of the same user.&lt;/p&gt;

&lt;p&gt;They have different responsibilities, different information needs, and different ways of understanding the building.&lt;/p&gt;

&lt;h3&gt;
  
  
  The evacuee: navigating the situation
&lt;/h3&gt;

&lt;p&gt;The evacuee interacts with the environment from a first-person perspective.&lt;/p&gt;

&lt;p&gt;The interface needs to keep the building and navigation experience central, allowing the participant to understand their surroundings and make decisions without unnecessary interface clutter.&lt;/p&gt;

&lt;h3&gt;
  
  
  The warden: understanding the bigger picture
&lt;/h3&gt;

&lt;p&gt;The warden sees the building from an overhead perspective.&lt;/p&gt;

&lt;p&gt;Rather than navigating from ground level, the warden needs to understand the broader scenario and observe what is happening across the environment.&lt;/p&gt;

&lt;p&gt;That calls for a different information hierarchy, a different layout, and controls designed around monitoring rather than first-person movement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the distinction matters
&lt;/h3&gt;

&lt;p&gt;A role-based application should not simply change a heading or swap a few buttons when the user changes roles.&lt;/p&gt;

&lt;p&gt;The interface should reflect what that person needs to accomplish.&lt;/p&gt;

&lt;p&gt;For CampusEvac, we treated the two perspectives as complementary parts of the same experience. The evacuee focuses on navigating the environment; the warden focuses on observing the wider situation.&lt;/p&gt;

&lt;p&gt;This principle extends to many other applications, from collaborative tools to logistics dashboards and real-time monitoring systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real-Time Problem
&lt;/h2&gt;

&lt;p&gt;Two browser windows showing the same building do not automatically create a shared simulation.&lt;/p&gt;

&lt;p&gt;When participants interact independently, the application needs a way to communicate changes, manage connections, and keep the experience coherent.&lt;/p&gt;

&lt;p&gt;For CampusEvac, we used AWS AppSync Events for real-time communication and Amazon DynamoDB for application state.&lt;/p&gt;

&lt;p&gt;At a high level, the architecture consists of three main parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser application&lt;/td&gt;
&lt;td&gt;Provides the evacuee and warden interfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS AppSync Events&lt;/td&gt;
&lt;td&gt;Enables real-time communication between connected participants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon DynamoDB&lt;/td&gt;
&lt;td&gt;Stores application state persistently&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction between real-time communication and persistent state is important.&lt;/p&gt;

&lt;p&gt;A message can communicate that something has changed, but a message and a durable record of application state are not the same thing. A robust implementation needs to define which information is persisted, how updates are propagated, and what happens when a participant disconnects and returns.&lt;/p&gt;

&lt;p&gt;These questions become especially relevant in collaborative applications, where the experience depends on multiple clients interacting with shared state.&lt;/p&gt;

&lt;p&gt;Our architecture gave us a foundation for combining a browser-based simulation with AWS-managed real-time communication and persistence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why UI Was More Than Visual Polish
&lt;/h2&gt;

&lt;p&gt;The Best UI track gave us a reason to look closely at what makes an interface effective in the first place.&lt;/p&gt;

&lt;p&gt;It is tempting to equate good UI with gradients, animations, typography, and polished components. Those elements can contribute to the experience, but they do not replace clear interaction design.&lt;/p&gt;

&lt;p&gt;CampusEvac has an additional challenge: the interface needs to communicate a situation that changes over time.&lt;/p&gt;

&lt;p&gt;The evacuee needs an interface that supports navigation. The warden needs an interface that supports observation. Both need to understand that they are participating in the same simulation.&lt;/p&gt;

&lt;p&gt;That makes several design considerations particularly important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role clarity.&lt;/strong&gt; Participants should be able to understand their responsibilities from the interface they are using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Information hierarchy.&lt;/strong&gt; The most relevant information should be easy to distinguish from secondary controls and status indicators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contextual feedback.&lt;/strong&gt; Changes in a shared environment need to be communicated clearly enough that participants can understand what has happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional consistency.&lt;/strong&gt; The interface should remain understandable while the application is running, not merely in a static screenshot.&lt;/p&gt;

&lt;p&gt;The central idea is that UI quality depends on how effectively an interface supports its intended task.&lt;/p&gt;

&lt;p&gt;A visually polished interface is useful. A visually polished interface that also communicates the system's behaviour is more useful still.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Under Hackathon Constraints
&lt;/h2&gt;

&lt;p&gt;Hackathons require teams to make decisions quickly. There is limited time to explore a problem, choose a stack, implement the main experience, integrate services, test the application, and prepare a demonstration.&lt;/p&gt;

&lt;p&gt;For CampusEvac, we concentrated on the core interaction: two roles, two perspectives, and one shared simulation.&lt;/p&gt;

&lt;p&gt;We also incorporated AI coding agents into our development workflow.&lt;/p&gt;

&lt;p&gt;These tools helped accelerate implementation, but generating code was only one part of the process. The application still needed architectural decisions, integration work, and testing across its different components.&lt;/p&gt;

&lt;p&gt;A component can render correctly on its own while the complete application behaves incorrectly when two participants interact simultaneously.&lt;/p&gt;

&lt;p&gt;That distinction matters particularly in real-time systems. Testing an individual interface is not a substitute for testing the relationship between connected clients.&lt;/p&gt;

&lt;p&gt;Our experience reinforced a useful principle: AI-assisted development can reduce implementation effort, but it does not remove the need to understand the system being built.&lt;/p&gt;

&lt;p&gt;The quality of the result still depends on defining the problem clearly, making sound decisions, and verifying that the pieces work together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Would Improve Next
&lt;/h2&gt;

&lt;p&gt;CampusEvac gave us a working foundation, but a simulation of this kind has several areas worth exploring further.&lt;/p&gt;

&lt;p&gt;Some possible directions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Larger building scenarios:&lt;/strong&gt; More detailed environments that allow for varied evacuation routes and layouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clearer monitoring interfaces:&lt;/strong&gt; Better ways to communicate the overall state of a drill without overwhelming the warden with information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnection handling:&lt;/strong&gt; More robust behaviour when a participant leaves a session and later returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More extensive real-time testing:&lt;/strong&gt; Testing concurrent interactions, changing connection states, and the consistency of shared information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are potential improvements, not claims that every feature is already implemented.&lt;/p&gt;

&lt;p&gt;The broader challenge is to make the simulation useful beyond a successful demonstration: predictable behaviour, understandable interfaces, and a system that remains coherent when real users interact with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result: Best UI at First Commit
&lt;/h2&gt;

&lt;p&gt;CampusEvac was recognised in the Best UI track at WeMakeDevs' First Commit hackathon.&lt;/p&gt;

&lt;p&gt;For us, the project brought together several areas of engineering: interface design, browser-based interaction, real-time communication, persistent state, and AI-assisted development.&lt;/p&gt;

&lt;p&gt;The recognition was a milestone, but the process was equally valuable. It gave us an opportunity to think about how design decisions affect a product's functionality and how architectural choices influence the experience users ultimately see.&lt;/p&gt;

&lt;p&gt;I'm grateful to WeMakeDevs for the opportunity to participate, and to my teammates Afreen and Akshay for building CampusEvac with me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try CampusEvac
&lt;/h2&gt;

&lt;p&gt;The best way to understand the project is to explore the two perspectives and see how they fit together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://main.d3skpx6qxc85hh.amplifyapp.com/" rel="noopener noreferrer"&gt;Explore CampusEvac&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="//youtube.com/watch?v=FhgTfCz0ou8&amp;amp;feature=youtu.be"&gt;Watch the walkthrough&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;wemakedevs:&lt;/strong&gt; &lt;a href="https://www.wemakedevs.org/aws/first-commit/projects" rel="noopener noreferrer"&gt;showcase&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CampusEvac started with a simple idea: make an evacuation drill interactive and shared. Building it taught us how much work goes into connecting the interface, the underlying system, and the experience of the people using it.&lt;/p&gt;

&lt;p&gt;That is what made this project worth building.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Abhishek M. Shivanagoudar, Afreen, and Akshay for WeMakeDevs First Commit.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wemakedevs</category>
      <category>hackathon</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
