{"slug": "i-gave-my-agent-a-graph-and-it-stopped-guessing-who-owned-what", "title": "I gave my agent a graph and it stopped guessing who owned what", "summary": "A developer replaced vector-chunk retrieval in an on-call assistant with a knowledge graph stored in FalkorDB Cloud, running Cypher queries over roughly 4,000 nodes in a graph called company_brain. The schema links people, projects, systems, tickets, documents and decisions through typed relationships, so the agent can trace service ownership, dependency impact up to three hops, and the reasoning behind past decisions with provenance. The developer reports that answers now cite the actual document and author rather than an opaque chunk ID, and that agent memory lives in the same graph instead of a second database.", "body_md": "How moving company knowledge from vector chunks into FalkorDB changed the way my agent answers the three questions on-call engineers actually ask.\n\nThe 2am page\n\nA payments retry storm hits. The pager goes off, and the first thing someone asks the assistant is: who owns payments-api?\n\nIt gives a name. The problem is that the person left the team four months ago.\n\nNobody updated the wiki. The org chart is current, the Slack thread is current, and the ticket history is current, but none of that is connected to the paragraph the model retrieved. The right answer exists. Finding it is the problem.\n\nThat was when I stopped treating company knowledge as text to search and started treating it as a graph to traverse.\n\nWhy RAG wasn't enough\n\nVector search answers a simple question: which text looks most like what I just asked?\n\nBut on-call questions are about relationships.\n\nA chunk might contain the sentence \"Aarav owns payments-api.\" But that doesn't tell you that Aarav is connected to the service through the Checkout project, that checkout-web depends on it, or who actually wrote the document explaining the Postgres decision.\n\nThose relationships are what a graph is built to represent.\n\nSo I changed the retrieval path. Instead of relying on embedding search to find relevant paragraphs, the agent runs Cypher queries against FalkorDB and gets back structured results with provenance.\n\nThe model in five nodes\n\nThe core of the graph is pretty small:\n\n``` php\n(Person)-[:WORKS_ON]->(Project)-[:USES]->(System)\n(System)-[:DEPENDS_ON]->(System)\n(Ticket)-[:REPORTED_IN]->(System)\n(Doc)-[:DECIDED]->(Decision)\n(Person)-[:WROTE]->(Doc)\n```\n\nFrom there, chats can mention systems, users can ask questions and store facts, agents can hand work to other agents, and tickets can be assigned to people. Playbooks can also be updated as the agent works through incidents.\n\nEverything lives in one FalkorDB Cloud instance, in a graph called company_brain. The demo has around 4,000 nodes.\n\nOne design decision I made early was not to create a generic node called \"knowledge.\" Docs, tickets, and chats remain their own types, linked to the systems they relate to. That means an answer can point to an actual document and its author instead of some opaque chunk ID.\n\nThree queries that made the difference\n\n``` php\nMATCH (p:Person)-[:WORKS_ON]->(:Project)-[:USES]->(s:System)\nWHERE (s.name CONTAINS $q OR $q CONTAINS s.name)\n  AND p.synthetic IS NULL\nRETURN p.name AS person, count(*) AS projects\nORDER BY projects DESC LIMIT 5\n```\n\nThe point isn't just to return a name. The graph gives us the relationships used to find the person, making it possible to inspect why that person was returned.\n\nIf the answer is wrong, we can investigate the underlying relationships instead of trying to figure out why a similarity search retrieved the wrong chunk.\n\n```\nMATCH (s:System {name:$system})<-[:DEPENDS_ON*1..3]-(x)\nRETURN DISTINCT x.name AS impacted LIMIT 20\n```\n\nThis follows dependency relationships up to three hops and returns the systems that could be affected.\n\nFor payments-api, the demo returns checkout-web and webhook-worker. We're not relying on two pieces of text sounding related. We're following the actual relationships stored in the graph.\n\n``` php\nMATCH (d:Doc)-[:DECIDED]->(dec:Decision)\nMATCH (p:Person)-[:WROTE]->(d)\nWHERE dec.topic CONTAINS 'Postgres'\nRETURN dec.topic, dec.reason, d.title, p.name LIMIT 1\n```\n\nAsk why we chose Postgres for the ledger, and the query can return the decision topic, the reason behind it, the document, and the author.\n\nThat makes the answer easier to verify. Instead of getting a plausible paragraph about why Postgres might be a good choice, you can trace the answer to the information stored in the graph.\n\nMemory without a second database\n\nI didn't want to introduce another database just to handle memory. Once memory lives somewhere else, the agent has to pull information from two different systems and reconcile it.\n\nSo I put memory in the same graph.\n\n``` php\nMERGE (u:User {id:$uid})\nCREATE (u)-[:ASKED {at:$at}]->(:Question {text:$q})\n```\n\nEpisodic memory records what a user asked and when. Semantic memory stores facts through KNOWS_FACT relationships to Fact nodes, such as team = Checkout. Procedural memory uses Playbook nodes that the agent can update while triaging tickets, allowing later sessions to reuse those steps instead of starting from scratch.\n\nFor the demo, I isolated tenants by prefixing user IDs, such as acme:u9 and stark:u9, within the same graph.\n\nThat's what I built, not a separate graph for every tenant. At real multi-tenant scale, I'd move to one graph per tenant because the isolation boundary is cleaner and reduces the risk of a bad query crossing tenant boundaries. I'd rather be clear about that limitation than claim to have implemented something I haven't.\n\nThe part that surprised me: actually taking action\n\nOnce ownership and impact analysis are graph queries, taking action becomes a much smaller step.\n\nThe agent ranks potential owners by proximity, then runs a query to assign the ticket and record what happened.\n\n```\nMATCH (t:Ticket {id:$ticket}), (p:Person {name:$who})\nMERGE (t)-[r:ASSIGNED_TO]->(p) SET r.at=$at, r.by='ops-agent'\nMERGE (a:Action {ticket:$ticket, kind:'auto-assign'}) SET a.to=$who, a.system=$system\nSET t.status='triaged'\n```\n\nAfter that query runs, the graph has changed. The ticket has an assignee, the action is recorded, and the status is updated.\n\nThat's the difference between an assistant that tells you what to do and an agent that can actually do it.\n\nThe agent also has access to FalkorDB algorithms as tools. algo.bfs gives it a frontier of nodes to investigate from an alert, while algo.pageRank('System','DEPENDS_ON') ranks systems by importance within the graph.\n\nBoth return in roughly 300 ms on the demo graph. All seven tools are exposed over MCP, so any MCP-compatible client can discover and call them.\n\nThree things that broke along the way\n\nshortestPath wasn't usable. FalkorDB rejects undirected shortestPath and only allows it inside WITH or RETURN. I switched to algo.bfs, which turned out to be a better fit for the agent anyway. It returns a frontier to investigate rather than just one path.\n\nSynthetic data started polluting real answers. After bulk-loading 2,000 synthetic users, the owner ranking started returning fake users ahead of the real ones. I added a synthetic=true tag to generated nodes and made every ranking query filter them out with p.synthetic IS NULL. I caught this with a local test harness before recording the demo.\n\nfalkordb-ts returned objects instead of arrays. I expected something like [\"Aarav\"], but got objects like {\"p.name\":\"Aarav\"}. That assumption silently produced undefined in the UI until I updated every handler to normalise both return shapes.\n\nThere was another lesson from ingestion. My first bulk-load query used CREATE for shared nodes and ended up creating 201 duplicate Project nodes. I switched to MERGE for shared nodes, making ingestion idempotent. Running it twice no longer creates the same projects all over again.\n\nWhat I'd do next\n\nThere are a few things I'd improve before treating this as a production system.\n\nLinks\n\nRepo: [https://github.com/SznImperial/ops-brain](https://github.com/SznImperial/ops-brain)\n\nLive demo: [https://ops-brain.netlify.app](https://ops-brain.netlify.app)\n\nDemo video: [https://www.loom.com/share/29ddc85394764764b898bf3bfe2a1440](https://www.loom.com/share/29ddc85394764764b898bf3bfe2a1440)\n\nQueries: app/queries.cypher — the queries used in the post, validated against the live graph.\n\nBuilt solo for Graph Hacks (FalkorDB × WeMakeDevs), entering T1 Agents That Act, T2 Memory & Coordination, and T3 Company Brain.", "url": "https://wpnews.pro/news/i-gave-my-agent-a-graph-and-it-stopped-guessing-who-owned-what", "canonical_source": "https://dev.to/sznimperial/i-gave-my-agent-a-graph-and-it-stopped-guessing-who-owned-what-4gbi", "published_at": "2026-10-18 16:37:53+00:00", "updated_at": "2026-10-18 16:47:34.525032+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "artificial-intelligence", "mlops"], "entities": ["FalkorDB", "FalkorDB Cloud", "company_brain", "Cypher", "payments-api", "checkout-web", "Postgres"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-gave-my-agent-a-graph-and-it-stopped-guessing-who-owned-what", "markdown": "https://wpnews.pro/news/i-gave-my-agent-a-graph-and-it-stopped-guessing-who-owned-what.md", "text": "https://wpnews.pro/news/i-gave-my-agent-a-graph-and-it-stopped-guessing-who-owned-what.txt", "jsonld": "https://wpnews.pro/news/i-gave-my-agent-a-graph-and-it-stopped-guessing-who-owned-what.jsonld"}}