﻿<?xml version="1.0" encoding="utf-8"?><rss version="2.0"><channel><title>Ayende @ Rahien</title><link>http://ayende.com</link><description>Ayende @ Rahien</description><copyright>Copyright (C) Ayende Rahien  2004 - 2021 (c) 2026</copyright><ttl>60</ttl><item><title>Learning to code, 1990s vs 2026</title><description>&lt;p&gt;I still remember the bookstore. I was holding a 600-page brick of a book on how to build Windows applications, trying to convince my mother that I &lt;em&gt;really&lt;/em&gt;&amp;nbsp;needed it. This was 1994 or 1995. A book was how you learned to program at that time. You took it home, you read it cover to cover, you typed the examples by hand, and somewhere along the way, the ideas sank in.&lt;/p&gt;&lt;p&gt;From there, the tools for learning kept evolving. Printed books gave way to CD-ROMs and then to online documentation. Then came the explosion of blogs and RSS feeds. I started this blog at that time, and I still consider that era to be one of the best ones in terms of having amazing access to &lt;em&gt;smart&lt;/em&gt;&amp;nbsp;and knowledgeable people, freely sharing their insights and experiences.&lt;/p&gt;&lt;p&gt;Google killed Google Reader (yes, I am still angry about that)&amp;nbsp;and a lot of the&amp;nbsp;new people learned via Stack Overflow. The world entered a strange equilibrium that lasted, honestly, more than a decade. If you learned to code any time between roughly 2010 and 2022, you probably learned through some combination of Google, Stack Overflow, and maybe YouTube.&lt;/p&gt;&lt;p&gt;Then the floor moved again. First it was ChatGPT, where you copy-pasted code back and forth. Then the models were integrated into the IDE. Now, with Claude Code and Codex, it is something else entirely: an agent that just runs, makes decisions, and does the thing.&lt;/p&gt;&lt;p&gt;The arc is striking when you lay it out. You used to have to go to a physical library, pick up a physical book, read it, digest it, and think about it. Today, the prevailing message to a new developer is essentially: you do not need to know any of that. Just describe what you want, and it happens.&lt;/p&gt;&lt;h3&gt;Hidden costs for reduced conceptual depth&lt;/h3&gt;&lt;p&gt;This shift is not just about convenience. It changes the &lt;em&gt;depth&lt;/em&gt;&amp;nbsp;of knowledge a developer carries, and that has consequences. Here is the example I keep coming back to. Imagine you ask a developer to show you a website that they built.&lt;/p&gt;&lt;p&gt;If you asked that in the late nineties, it meant &lt;em&gt;something&lt;/em&gt;. To do that, you had to purchase a domain. Understand DNS well enough to wire it up correctly. Set up a web server, which meant getting Apache to actually run. Successfully configure PHP and deploy scripts to production. &lt;/p&gt;&lt;p&gt;By the time you could point to a working URL, you &lt;em&gt;had&lt;/em&gt;&amp;nbsp;to touch every layer of the stack. There was no other choice. Therefore, you were at least passingly familiar with a lot more than you would be today.&lt;/p&gt;&lt;p&gt;Ask that same question of many developers today, and the answer is a Vercel subdomain. That is not a dig at Vercel, mind you - it is a great product, and abstraction is the whole point. But some of these developers genuinely do not &lt;em&gt;know&lt;/em&gt;&amp;nbsp;what DNS is. They do not know what is running on the server versus the client. They do not know that there is even a meaningful distinction. And we have seen real security incidents come out of exactly that gap &amp;mdash; secrets leaking into client bundles, auth logic running where it should not, and CORS misconfigurations that nobody understood well enough to notice.&lt;/p&gt;&lt;p&gt;Now extend that same dynamic one more step. Take the cohort of developers who will learn to program primarily through this new generation of agentic tools. The abstraction is no longer just over DNS or deployment. It is over the act of writing the code itself.&lt;/p&gt;&lt;h3&gt;What is the role of a junior developer now?&lt;/h3&gt;&lt;p&gt;I think we are going to end up with a genuinely different type of engineer and, as a result, a genuinely different type of system.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&amp;ldquo;If men learn this, it will implant forgetfulness in their souls; they will cease to exercise memory because they rely on that which is written, calling things to remembrance no longer from within themselves, but by means of external marks&amp;rdquo;. &lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;&lt;li&gt;Plato, Phaedrus (c. 429-347 BCE) &amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Every generation has been accused of being softer than the previous generation, as the quote above can testify. In this case, Plato is decrying &lt;em&gt;writing&lt;/em&gt;&amp;nbsp;as a corrupting influence on youth who no longer bother to just &lt;em&gt;remember&lt;/em&gt;&amp;nbsp;things.&lt;/p&gt;&lt;p&gt;Without the attribution, I don&amp;rsquo;t think you would have realized that this isn&amp;rsquo;t me talking about developers utilizing coding agents instead of learning on their own. &lt;/p&gt;&lt;p&gt;In software, we see much the same pattern. The person who wrote assembly looked down on the C programmer. The C programmer looked down on the Java programmer. The Java programmer looked down on the person gluing libraries together in Python. Each step up the abstraction ladder lets people build bigger, more ambitious things with less effort. That is mostly good.&lt;/p&gt;&lt;p&gt;But there is a real asymmetry this time. The earlier steps abstracted away mechanical work &amp;mdash; memory management, boilerplate, deployment plumbing. This step abstracts away the reasoning itself. And reasoning is what you need when the abstraction leaks, which it always eventually does.&lt;/p&gt;&lt;p&gt;The question I am actually struggling with, day to day, is much more practical: how do I evaluate a junior developer in this sort of world?&lt;/p&gt;&lt;p&gt;The classic move was a take-home task. Build a small feature. Show me your thinking. The problem is that a capable model will produce a perfectly clean solution to any reasonable take-home in a few minutes. What you see in the submission tells you almost nothing about what the candidate actually understands. It tells you they can prompt well, which is a real skill, but it is not the skill I am trying to measure.&lt;/p&gt;&lt;p&gt;I can also ask them to solve a task while they are in our offices, so I can verify no AI use. But that is also stupid; I &lt;em&gt;want &lt;/em&gt;them to use AI. After all, that is a great productivity enhancer. So I need a way to test understanding, not just the output. &lt;/p&gt;&lt;p&gt;The signals I care about are the ones that are hardest to fake in an agent-assisted world. Can you debug something when the model is wrong? Can you explain why a piece of generated code is subtly unsafe, or slow, or wrong in a way that only matters at the hundredth user? Can you make a reasoned call about which abstraction to reach for and which one to reject? When the system behaves unexpectedly, do you know where to look?&lt;/p&gt;&lt;p&gt;At the same time, those aren&amp;rsquo;t usually qualities that you &lt;em&gt;can&lt;/em&gt;&amp;nbsp;look for in a junior developer. Having those qualities usually means that they aren&amp;rsquo;t junior anymore. &lt;/p&gt;&lt;p&gt;People used to train on LeetCode tests as a way to show how good they were in interviews. That was a good stand-in to see what they knew and understood. What is the next stage here?&lt;/p&gt;&lt;p&gt;What does a junior do to exercise their skills and show that they can bring value to the team? I don&amp;rsquo;t know if I have good answers to those questions. But that is something we, as an industry, need to consider carefully. &lt;/p&gt;&lt;p&gt;I do not want to be the old man yelling at the cloud. The tools are genuinely great, and refusing to use them is its own kind of malpractice. AI coding agents can make you &lt;em&gt;meaningfully &lt;/em&gt;more productive.&lt;/p&gt;&lt;p&gt;But when I talk to developers just starting out, the thing I keep pushing is this: use the tools, and also, on a regular basis, go down a layer. Set up a server yourself. Deploy something without a platform holding your hand. Read the DNS records. Look at what your framework is actually generating. Write something in a language without a package manager that hides the sharp edges.&lt;/p&gt;&lt;p&gt;Not because you will do it that way at work. But because the next time something breaks in a way the agent cannot fix&amp;nbsp;you will have a mental model to fall back on. You will know where the seams are. You will know what to look at.&lt;/p&gt;&lt;p&gt;That mental model is, I suspect, going to be the thing that separates the engineers who compound over a career from the ones who get stuck the first time the abstraction leaks. &lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203975-a/learning-to-code-1990s-vs-2026?Key=4bf78bb8-056a-412e-adcc-758ff6f269f1</link><guid>http://ayende.com/203975-a/learning-to-code-1990s-vs-2026?Key=4bf78bb8-056a-412e-adcc-758ff6f269f1</guid><pubDate>Tue, 05 May 2026 12:00:00 GMT</pubDate></item><item><title>The GPU Is the New Bangalore </title><description>&lt;p&gt;In the 2000s, the hottest move in software was offshoring. You&amp;#39;d ship your requirements to a development shop in India, Vietnam, or Bangladesh, pay a fraction of Western developer rates, and wait. The cost savings were real, every spreadsheet said so. The failure modes were also real, every CTO said so.&lt;/p&gt;&lt;p&gt;Even assuming that the teams working on your code were smart, motivated, and hardworking, the distance, communication overhead, the time zone mismatch, and misaligned incentives created a brutal set of constraints. If you wanted to get good results from offshoring, &amp;nbsp;you needed to be able to clearly specify what you wanted and be &lt;em&gt;good&lt;/em&gt;&amp;nbsp;at validating that you got what you expected.&lt;/p&gt;&lt;p&gt;You couldn&amp;#39;t just say &amp;quot;I need a login system.&amp;quot; You had to write detailed specs, break work into reviewable chunks, define acceptance criteria, and actually &lt;em&gt;read&lt;/em&gt;&amp;nbsp;the code that came back. Not rubber-stamp it. Read it, make sure that it passed muster and could be accepted internally, because the delta between &amp;quot;looks right&amp;quot; and &amp;quot;is right&amp;quot; could cost you six months of production incidents.&lt;/p&gt;&lt;p&gt;Sound familiar? Today, instead of shipping my requirements to a dev shop overseas, I&amp;#39;m shipping them to a GPU somewhere. I get something back. It looks like code. It might &lt;em&gt;be&lt;/em&gt;&amp;nbsp;code. It might be a very convincing facsimile of code that will quietly fail in production under load. I genuinely don&amp;#39;t know until I sit down and read it carefully.&lt;/p&gt;&lt;p&gt;The same discipline that separated successful offshore engagements from expensive disasters applies here as well:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Specification quality determines output quality.&lt;/strong&gt;&amp;nbsp;Vague prompts return vague code. The ability to articulate exactly what you want &amp;mdash; at the right level of abstraction &amp;mdash; is now a core engineering skill.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Validation is non-negotiable.&lt;/strong&gt;&amp;nbsp;&amp;quot;It passed the vibe check&amp;quot; is not a code review. The reviewer needs to understand what the code is doing and why, not just that it compiles and the tests are green.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Iterative delivery beats big-bang delivery.&lt;/strong&gt;&amp;nbsp;Nobody who survived offshoring tried to outsource an entire product in one shot. You stage it. You review at each stage. You course-correct before mistakes compound.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;The Bottleneck Has Moved&lt;/h3&gt;&lt;p&gt;Here&amp;#39;s what I think is the deeper shift: for most of software history, the bottleneck was &lt;em&gt;writing the code&lt;/em&gt;. That took time and required expensive humans. So the industry optimized heavily around it, better editors, better frameworks, and better abstractions. All in service of making the act of writing code faster and less error-prone.&lt;/p&gt;&lt;p&gt;That bottleneck is collapsing. What once took six months might take six hours. When the cost of implementation approaches zero, the bottleneck moves upstream: to design, specification, and verification. The expensive parts are now:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Understanding the problem clearly enough to describe it precisely.&lt;/li&gt;&lt;li&gt;Decomposing it into well-scoped, independently verifiable pieces.&lt;/li&gt;&lt;li&gt;Reviewing what comes back and actually understanding it.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;These are skills we largely deprioritized during the era when coding itself was the hard part. They&amp;#39;re about to become the most valuable things a technical person can do.&lt;/p&gt;&lt;p&gt;A lot of that used to be done &amp;ldquo;along the way&amp;rdquo; when you wrote the code. You would explore the problem and gain depth of understanding as you wrote the code. Now that just doesn&amp;rsquo;t happen, but you still need to do that work explicitly.&lt;/p&gt;&lt;h3&gt;A note about the importance of proper architecture&lt;/h3&gt;&lt;p&gt;There is this idea that the path to building big systems with AI is to spin up a swarm of specialized agents (a frontend agent, a backend agent, a database administrator agent, etc.) and somehow orchestrate them into a coherent product.&lt;/p&gt;&lt;p&gt;I find this baffling, because we already have a well-established protocol for coordinating the work of specialized, partially independent contributors on a complex system. It&amp;#39;s called software design.&lt;/p&gt;&lt;p&gt;Module boundaries. Interface contracts. Separation of concerns. Dependency management. SOLID principles and more. These patterns exist precisely because complex systems built by multiple contributors without clear interfaces turn into unmaintainable messes. This is true whether those contributors are humans, offshore teams, or language models.&lt;/p&gt;&lt;p&gt;The instinct to throw orchestration complexity at a coordination problem is exactly backwards. The answer isn&amp;#39;t a smarter message bus between your agents. The answer is better system design that minimizes how much the pieces need to talk to each other in the first place.&lt;/p&gt;&lt;p&gt;We have literally decades of experience in how to build large software systems (and thousands of years of experience in how to handle large projects in general). There isn&amp;rsquo;t anything inherently new here to deal with.&lt;/p&gt;&lt;p&gt;The developers who will thrive in this environment aren&amp;#39;t necessarily the ones who write the most elegant code. They&amp;#39;re the ones who can hold a complex system design in their head and communicate it clearly, break the work into well-specified, verifiable increments, and actually read the code that comes back and hold it to a real standard of quality.&lt;/p&gt;&lt;p&gt;These are, in large part, the same skills that made the best engineering leads effective during the offshoring era. The context has changed completely. The discipline hasn&amp;#39;t.&lt;/p&gt;&lt;p&gt;The GPU is the new Bangalore. Time to dust off the playbook.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203974-a/the-gpu-is-the-new-bangalore?Key=ffd714bb-2bf5-4256-8143-753e268a108d</link><guid>http://ayende.com/203974-a/the-gpu-is-the-new-bangalore?Key=ffd714bb-2bf5-4256-8143-753e268a108d</guid><pubDate>Fri, 01 May 2026 12:00:00 GMT</pubDate></item><item><title>Putting Claude up against our test suite</title><description>&lt;p&gt;I&amp;rsquo;m convinced that in hell, there is a special place dedicated to making engineers fix flaky tests. &lt;/p&gt;&lt;p&gt;Not broken tests. Not tests covering a real bug. Flaky tests. Tests that pass 999 times out of 1000 and fail on the 1,000th run for no reason you can explain with a clean conscience.&lt;/p&gt;&lt;p&gt;If you&amp;#39;ve ever shipped a reasonably complex distributed system, you know exactly what I&amp;#39;m talking about. RavenDB has, at last count, over 32,000 tests that are run continuously on our CI infrastructure. I just checked, and in the past month, we&amp;rsquo;ve had hundreds of full test runs.&lt;/p&gt;&lt;p&gt;That is actually a problem for our scenario, because with that many tests and that many runs, the law of large numbers starts to apply. Assuming we have tests that have 99.999%&amp;nbsp;reliability, that means that 1 out of every 100,000 test runs may fail. We run tens of millions of those tests in a month.&lt;/p&gt;&lt;p&gt;In a given week, something between ten and twenty of those tests will fail. Given the number of test runs, that is a good number in percentage terms. But each such failure means that we have to investigate it. &lt;/p&gt;&lt;p&gt;Those test failures are &lt;em&gt;expensive. &lt;/em&gt;Every ticket is a developer staring at logs, trying to figure out whether this is a genuine bug in the product, a bug in the test itself, or something broken in the environment. In almost all cases, the problem is with the test itself, but we have to investigate.&lt;/p&gt;&lt;p&gt;A test that consistently fails is easy to fix. A test that occasionally fails is the worst. &lt;/p&gt;&lt;p&gt;With a flaky test, you don&amp;#39;t just fix something and move on. You spend two days isolating it. Reproducing it. Building a mental model of a race condition that only manifests under specific timing, load, and cosmic alignment.&lt;/p&gt;&lt;p&gt;The tests that do this are almost always the integration tests. The ones that test complex distributed behavior across many parts of the system simultaneously. By definition, they are also the hardest to reason about.&lt;/p&gt;&lt;p&gt;The fact that, in most cases, those test failures add nothing to the &lt;em&gt;product&lt;/em&gt;&amp;nbsp;(i.e., they didn&amp;rsquo;t actually discover a real bug) is just crushed glass on top of the sewer smoothie. You spend a lot of time trying to find and fix the issue, and there is no real value except that the test now consistently passes. &lt;/p&gt;&lt;p&gt;We have a script that runs weekly, collects all test failures, and dumps them into our issue tracker. This is routine maintenance hygiene, to make sure we stay in good shape. &lt;/p&gt;&lt;p&gt;I was looking at the issue tracker when the script ran, and the entire screen lit up with new issues. &lt;/p&gt;&lt;p&gt;Just looking at that list of new annoyances was enough to ruin my mood. &lt;/p&gt;&lt;p&gt;And then, without much deliberate planning, I did something dumb and impulsive: I copy-pasted all of those fresh issues into Claude and told it to fix them. Then I went and did other things. I had very low expectations about this, but there was not much to lose.&lt;/p&gt;&lt;p&gt;A few hours later, I got a notification about a pull request. To be honest, I expected Claude to mark the flaky tests as skipped, or remove the assertions to make them pass.&lt;/p&gt;&lt;p&gt;I got an &lt;em&gt;actual&lt;/em&gt;&amp;nbsp;pull request, with real fixes, to my shock. Some of them were fixes applied to test logic. Some were actually fixes in the underlying code. &lt;/p&gt;&lt;p&gt;And then there was this one that stopped me cold. Claude had identified that in one of our test cases, we were waiting on the wrong resource. Not wrong in an obvious way &amp;mdash; wrong in the kind of way that works perfectly 99.9998% of the time and silently fails 0.0002% of the time. &lt;/p&gt;&lt;p&gt;The (test) code &lt;em&gt;looked&lt;/em&gt;&amp;nbsp;right. We were waiting for something to happen; we just happened to wait on the wrong thing, and usually the value we asserted on was already set by the time we were done waiting. &lt;/p&gt;&lt;p&gt;Claude found it. In one pass. For the price of a subscription I was already paying. For reference, that &lt;em&gt;single&lt;/em&gt;&amp;nbsp;&amp;ldquo;let me throw Claude at it&amp;rdquo; decision probably saved enough engineering time to cover the cost of Claude for the entire team for that month.&lt;/p&gt;&lt;p&gt;Let me be precise about what happened and what didn&amp;#39;t. Claude did not fix everything. Some of the &amp;quot;fixes&amp;quot; it produced were pretty bad, surface-level patches that didn&amp;#39;t address the real cause, or things that were legitimately out of scope. &lt;/p&gt;&lt;p&gt;You still need an engineer reviewing the output. And you still need judgment.&lt;/p&gt;&lt;p&gt;But it got things fixed, quickly, without needing two days to context-switch into the problem space. And the things it did fix well, it fixed really well.&lt;/p&gt;&lt;p&gt;The work it compressed would have realistically taken one developer a week or two to grind through &amp;mdash; and that&amp;#39;s assuming you could get a developer to focus on it for that long in the first place. Flaky test investigation is the kind of work that quietly kills team morale. &lt;/p&gt;&lt;p&gt;Engineers start dreading CI. They start treating red builds as background noise. That&amp;#39;s how quality degrades silently. Leaving aside new features or higher velocity, being able to offload the most annoying parts of the job to a machine to do is&amp;hellip; wow. &lt;/p&gt;&lt;p&gt;Based on this, we&amp;#39;re building this into our actual workflow as an integral part of how we handle test maintenance. Failures are collected, routed to Claude, and it takes a first pass at triage and repair. Then we create an issue in the bug tracker with either an &lt;em&gt;actual &lt;/em&gt;fix or a summary of Claude&amp;rsquo;s findings.&lt;/p&gt;&lt;p&gt;By the time a human reviews this, significant progress has already been made.&lt;/p&gt;&lt;p&gt;It doesn&amp;#39;t replace the engineer. But it means the engineer is doing the interesting part of the work: judgment, review, architectural reasoning. Skipping the part that requires staring at race condition logs until your vision blurs.&lt;/p&gt;&lt;p&gt;This isn&amp;rsquo;t the most exciting aspect of using a coding agent, I&amp;rsquo;m aware. But it may be one of the best aspects in terms of quality of life. &lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203973-a/putting-claude-up-against-our-test-suite?Key=51ff4d9c-0a4e-4dd4-bbb4-7b697713fc3c</link><guid>http://ayende.com/203973-a/putting-claude-up-against-our-test-suite?Key=51ff4d9c-0a4e-4dd4-bbb4-7b697713fc3c</guid><pubDate>Wed, 29 Apr 2026 12:00:00 GMT</pubDate></item><item><title>15+ years of working with coding agents</title><description>&lt;p&gt;No, the title is not a mistake, nor did I use my time travel pass to give you insights from the future. Bear with me for a moment while I explain my thinking.&lt;/p&gt;&lt;h3&gt;From individual contributor to oversight role&lt;/h3&gt;&lt;p&gt;I started writing RavenDB in a spare bedroom, which turned into an office. The project grew from a sparkle in my head that wouldn&amp;rsquo;t let me sleep into a major project in very short order. &lt;/p&gt;&lt;p&gt;Today, I want to talk about a pretty important stage that happened during that growth phase. Somewhere between having five and ten full-time developers working on RavenDB, I lost the ability to keep track of every single line of code that was going into the project.&lt;/p&gt;&lt;p&gt;I had been the primary developer for years at this point, I wrote the majority of the code, and I was the person making all the key decisions in the project. And then, gradually, I&amp;hellip; wasn&amp;#39;t that guy anymore. &lt;/p&gt;&lt;p&gt;There were too many moving parts, too many developers, too many decisions happening in parallel for me to have my hands on all of it. That was the whole &lt;em&gt;point&lt;/em&gt;&amp;nbsp;of growing the team, dividing the tasks among the team members, and getting good people to do things so I didn&amp;rsquo;t have to do it all myself.&lt;/p&gt;&lt;p&gt;What I didn&amp;#39;t expect was how much it would &lt;em&gt;bother &lt;/em&gt;me. Moving from being the primary developer to a supervisory role didn&amp;rsquo;t mean that I lost the ability to write code. In fact, in many cases, I could &amp;ldquo;see&amp;rdquo; what the solution for each issue should be.&lt;/p&gt;&lt;p&gt;I just didn&amp;rsquo;t have the &lt;em&gt;time&lt;/em&gt;&amp;nbsp;to do that, nor the capacity to sit with every single developer on every single issue and craft the right way to solve it. I&amp;#39;d hand a feature to a developer &lt;em&gt;know&lt;/em&gt;&lt;em&gt;ing&lt;/em&gt;&amp;nbsp;that the way they were going to handle it would not be&amp;nbsp;mine. &lt;/p&gt;&lt;p&gt;That doesn&amp;rsquo;t mean it would be wrong, but it wouldn&amp;rsquo;t be the same. It might need a review cycle or two to get to the right level for the product, or they wouldn&amp;rsquo;t consider how it fits into the grand scheme of things, etc. &lt;/p&gt;&lt;p&gt;And let&amp;rsquo;s not talk about the time estimates I got. I&amp;rsquo;m willing to assume that my personal timing estimates are highly subjective and influenced by my deep familiarity with the codebase. &lt;/p&gt;&lt;p&gt;But still. Multiple days for something that felt like it &lt;em&gt;should &lt;/em&gt;be a two-hour job was hard to sit with.&lt;/p&gt;&lt;p&gt;I carried around a background level of frustration for quite some time. It killed me that the pace of development wasn&amp;rsquo;t up to what I wanted it to be. &amp;ldquo;If I could just have the time to sit and write this&amp;rdquo;, I kept thinking, &amp;ldquo;we would be done by the end of the week.&amp;rdquo; &lt;/p&gt;&lt;p&gt;There &lt;em&gt;was&lt;/em&gt;&amp;nbsp;progress, to be clear, but nothing was moving fast enough. Everywhere I looked, we had stalled. &lt;/p&gt;&lt;p&gt;And then something happened. It didn&amp;rsquo;t happen all at once, but in the space of a month or two, features started to land. Each team had been heads-down on something for quite a while, and by some coincidence of timing, they all finished around the same time. &lt;/p&gt;&lt;p&gt;Suddenly, we moved from &amp;ldquo;we have nothing to ship&amp;rdquo; to &amp;ldquo;we &lt;em&gt;can&amp;rsquo;t &lt;/em&gt;have so many new features all at once&amp;rdquo;. I realized that I &lt;em&gt;would&lt;/em&gt;&amp;nbsp;be able to ship things faster, for sure. I could do two new features, maybe even three, in that same time frame. That would require head-down coding for the entire duration, of course.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Reading that last paragraph again, I have to admit that I may be letting some hubris color my perception &amp;#129335;&amp;#128527;. &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I wouldn&amp;rsquo;t be able to deliver the sheer quantity of features that the team was able to deliver. &lt;/p&gt;&lt;p&gt;What had felt like months of stagnation turned out to be parallelism in action.&lt;/p&gt;&lt;p&gt;Yes, some of the code wasn&amp;#39;t the same code that I would write. And some of the architectural decisions weren&amp;#39;t the ones I&amp;#39;d have made. That didn&amp;rsquo;t make them &lt;em&gt;wrong&lt;/em&gt;, mind. And those developers were working on things &lt;em&gt;I &lt;/em&gt;was not working on. And the sum total of what got built was something I could never have done solo.&lt;/p&gt;&lt;h3&gt;Treating coding agents as junior developers? &lt;/h3&gt;&lt;p&gt;I think about that experience constantly now, because I&amp;#39;m living a version of it again, except the new team member is Claude. Working with AI coding agents today feels remarkably like working with a junior developer who is also a savant. &lt;/p&gt;&lt;p&gt;They&amp;#39;ve read everything. They know an enormous amount. They can produce working code quickly and confidently across a staggering range of domains. And yet they&amp;#39;re also genuinely ignorant in ways that will surprise you: missing context, misreading intent, optimizing for the wrong thing, occasionally producing something that is confidently and completely broken.&lt;/p&gt;&lt;p&gt;This is not a criticism. This is just what it&amp;#39;s like. And I&amp;#39;ve dealt with this before. There are clear parallels between mentoring junior engineers and looking at the output from an AI agent. &lt;/p&gt;&lt;p&gt;There is an assumption that you need to get perfect output from a coding agent. But you are not likely to get perfect output from a human developer. Even experienced developers benefit greatly from reviews, guidance, etc. Junior developers need &lt;em&gt;more &lt;/em&gt;of that, of course, but they can still bring value, even if their output goes through several iterations.&lt;/p&gt;&lt;p&gt;For coding agents to bring real value, you need to consider them in the same light.&lt;/p&gt;&lt;p&gt;The shift that happened with my developer team is the same shift that&amp;#39;s happening now with AI agents.&lt;/p&gt;&lt;p&gt;Instead of writing every line yourself, you start spending time on the bigger picture: here&amp;#39;s the overall direction, here&amp;#39;s the architectural constraint, here&amp;#39;s what done looks like. Then you review the outputs. &lt;/p&gt;&lt;p&gt;Talking to a coding agent is a little different from discussing a feature with a dev and reviewing their code days later, except that the agent delivers the output in the time it takes to get coffee.&lt;/p&gt;&lt;p&gt;The fact that this cycle is done in a short amount of time means that you still have all the knowledge in your head. You can catch drift before it becomes technical debt. &lt;/p&gt;&lt;p&gt;The &lt;em&gt;cost&lt;/em&gt;&amp;nbsp;of going in the wrong direction is greatly reduced, which means that you can be far more radical about how you approach these tasks.&lt;/p&gt;&lt;h3&gt;Unnatural impulses as a developer&lt;/h3&gt;&lt;p&gt;I wonder if a lot of developers are facing challenges in this area specifically because they don&amp;rsquo;t have the managerial experience needed for this new aspect of the work. &lt;/p&gt;&lt;p&gt;I have been writing code with Claude recently. And the short feedback cycle means that I&amp;rsquo;m loving it. I&amp;#39;m not abdicating the technical judgment, mind. I&amp;#39;m applying it differently. &lt;/p&gt;&lt;p&gt;I&amp;#39;m writing the high-level design, not the implementation. I&amp;#39;m doing the review, not the first draft. And I&amp;#39;m being honest with myself that the output, while it isn&amp;rsquo;t always what &lt;em&gt;I &lt;/em&gt;would write, is covering ground I simply would not have covered otherwise.&lt;/p&gt;&lt;p&gt;I have been doing this for a long time and it feels quite natural. I also remember that this was a &lt;em&gt;difficult&lt;/em&gt;&amp;nbsp;transition for me at the time. &lt;/p&gt;&lt;p&gt;For those who want to better understand how they can get the most value from coding agents, you are probably better off looking into project management theory rather than optimizing your &lt;code&gt;agents.md&lt;/code&gt;&amp;nbsp;file.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203972-a/15-years-of-working-with-coding-agents?Key=0c842282-4ab8-41dd-978d-f26f0e80df59</link><guid>http://ayende.com/203972-a/15-years-of-working-with-coding-agents?Key=0c842282-4ab8-41dd-978d-f26f0e80df59</guid><pubDate>Mon, 27 Apr 2026 12:00:00 GMT</pubDate></item><item><title>Expertise in the age of AI, or: Matt's Claude'll handle this</title><description>&lt;p&gt;One of our team leads has been working on a major feature using Claude Code. He&amp;#39;s been at it for a few days and is nearly done. To put that in context: this feature would normally represent about a month of a senior developer&amp;#39;s time.&lt;/p&gt;&lt;p&gt;He did the backend work himself &amp;mdash; working with Claude to build it out, applying his knowledge of how the system should behave, reviewing, adjusting, and iterating. He handled only the backend, and when I asked him about the frontend, he said: &lt;em&gt;&amp;quot;I&amp;#39;m going to let Matt&amp;rsquo;s Claude handle that.&amp;quot;&lt;/em&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Context: Matt is the frontend team lead. &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Note the interesting phrasing. He didn&amp;#39;t say &amp;quot;I&amp;#39;ll do the UI later&amp;quot; or &amp;quot;Claude&amp;rsquo;ll handle the UI.&amp;quot; He deferred to the frontend lead who has the domain expertise to drive that part.&lt;/p&gt;&lt;p&gt;That&amp;#39;s not a throwaway comment. That&amp;#39;s an &lt;em&gt;important&lt;/em&gt;&amp;nbsp;statement about how work should be divided in the age of AI agents.&lt;/p&gt;&lt;p&gt;Here&amp;#39;s the thing: I&amp;#39;ve told Claude to build a UI for a feature, pointed it at the codebase, and it figured out how the frontend is structured, what patterns we use, and generated something I could work with. It wasn&amp;rsquo;t a sketch or a wireframe diagram, it was actually &lt;em&gt;usable&lt;/em&gt;. &lt;/p&gt;&lt;p&gt;I got a functional UI from Claude in less time than it would take to write up the issue describing what I want.&lt;/p&gt;&lt;p&gt;That UI was enough for me to explore the feature, do a small demo, etc. I&amp;rsquo;m not a frontend guy, and I didn&amp;rsquo;t even look at the code, but I assume that the output probably matched the rest of our frontend code. &lt;/p&gt;&lt;p&gt;We won&amp;rsquo;t be using the UI Claude generated for me, though. The gap in polish between what I got and what a real frontend developer produces is enormous. I got something I could play with, but it was very evident that it wasn&amp;rsquo;t something that had received real attention.&lt;/p&gt;&lt;p&gt;For the time being, it was more than sufficient. The problem is that&amp;nbsp;even leaning heavily on AI, the investment of time for me to do it &lt;em&gt;right&lt;/em&gt;&amp;nbsp;would be significant. I&amp;#39;d need to understand our frontend architecture, our conventions, our component library, how state flows, and what our designers expect. All of that would take real time, even with an AI doing most of the code generation.&lt;/p&gt;&lt;p&gt;That is leaving aside the things that I &lt;em&gt;don&amp;rsquo;t&lt;/em&gt;&amp;nbsp;know about frontend that I wouldn&amp;rsquo;t even realize I need to handle. I wouldn&amp;rsquo;t even know what to ask the AI about, even if it could do the right thing if I sent it the right prompt.&lt;/p&gt;&lt;p&gt;Contrast that with the frontend team. They &lt;em&gt;know&lt;/em&gt;&amp;nbsp;the architecture of the frontend, of course, and they know how things should slot together and what concerns they should address. They know when Claude&amp;#39;s suggestion is on the right track and when it&amp;#39;s going to create a mess three layers down. Effectively, they know the magic incantation that the agent needs in order to do the right thing. &lt;/p&gt;&lt;p&gt;What does this &lt;em&gt;say&lt;/em&gt;&amp;nbsp;about AI usage in general? Given two people with the same access to a smart coding agent like Claude or Codex, both performing the same task, their domain knowledge will lead to &lt;em&gt;very&lt;/em&gt;&amp;nbsp;different results. In other words, it means that Claude and its equivalents are &lt;em&gt;tools&lt;/em&gt;. And the wielder of the tool has a huge impact on the end result.&lt;/p&gt;&lt;p&gt;The role of expertise hasn&amp;#39;t diminished. It&amp;#39;s shifted. The expert is no longer the person who can produce the artifact. They&amp;#39;re the person who can &lt;em&gt;direct the production&lt;/em&gt;&amp;nbsp;of the artifact correctly and efficiently. That&amp;#39;s a different skill profile, but it&amp;#39;s no less valuable and the leverage is higher.&lt;/p&gt;&lt;p&gt;We&amp;#39;re still figuring out what this means structurally. But the instinct to say &amp;quot;that&amp;#39;s not my domain, let the person who knows it handle the AI that does it&amp;quot; is correct. Domain knowledge determines the quality of the output, even when the AI is doing all the typing.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203971-a/expertise-in-the-age-of-ai-or-matts-claudell-handle-this?Key=5cdca447-6773-4a90-8564-4765289ec644</link><guid>http://ayende.com/203971-a/expertise-in-the-age-of-ai-or-matts-claudell-handle-this?Key=5cdca447-6773-4a90-8564-4765289ec644</guid><pubDate>Thu, 23 Apr 2026 12:00:00 GMT</pubDate></item><item><title>Using AI agents in long-lived software projects</title><description>&lt;p&gt;You read the story a hundred times: &amp;ldquo;I told Codex (or Claude, or Antigravity, etc.) to build me a full app to run my business, and 30 minutes later, it&amp;rsquo;s done&amp;rdquo;. These types of stories usually celebrate the new ecosystem and the ability to build complex systems without having to dive into the details.&lt;/p&gt;&lt;p&gt;The benchmarks celebrate &amp;quot;one-shotting&amp;quot; entire applications, as if that&amp;#39;s the relevant metric. I think this is the wrong framing entirely. Mostly because I care very little about disposable software, stuff that you stop using after a few days or a week. I work on projects whose lifetime is measured in decades. &lt;/p&gt;&lt;p&gt;AI agent-driven development isn&amp;#39;t about the ability to use a one-shot prompt to generate a full-blown app that matches &lt;em&gt;exactly&lt;/em&gt;&amp;nbsp;what the user wants. That is a nice trick, but nothing more, because after you generate the application, you need to maintain it, add features (and ensure stability over time), fix bugs, and adjust what you have.&lt;/p&gt;&lt;p&gt;The process of using AI agents to build long-lived applications is distinctly different from what I see people bandying about. I want to dedicate this post to discussing some aspects of using AI agents to accelerate development in long-lived software projects. &lt;/p&gt;&lt;h2&gt;Code quality only matters in the long run&lt;/h2&gt;&lt;p&gt;The key difference between one-off work and long-lived systems is that we don&amp;rsquo;t care about code quality &lt;em&gt;at all&lt;/em&gt;&amp;nbsp;for the one-off stuff. It&amp;#39;s a throwaway artifact. Run it, get your answer, move on. I am usually not even going to &lt;em&gt;look&lt;/em&gt;&amp;nbsp;at the code that was generated; I certainly don&amp;rsquo;t care how it is structured.&lt;/p&gt;&lt;p&gt;If I need to make any changes, or have to come back to it in six months, it is usually easier to just regenerate the whole thing from scratch rather than trying to maintain or evolve it.&lt;/p&gt;&lt;p&gt;When you&amp;#39;re talking about an application that will live for a decade or more - or worse, an &lt;em&gt;existing&lt;/em&gt;&amp;nbsp;application with decades of accumulated effort baked into it - what happens then? The calculus changes completely. How do you even begin to bring AI into that kind of system?&lt;/p&gt;&lt;p&gt;It turns out that proper software architecture becomes &lt;em&gt;more&lt;/em&gt;&amp;nbsp;relevant, not less.&lt;/p&gt;&lt;h2&gt;Software architecture as context management for AI&lt;/h2&gt;&lt;p&gt;Think about what good software architecture actually gives you: components, layers, clear boundaries, and well-defined responsibilities. The traditional justification is that this lets you make small, careful, targeted changes. You know where to go, and you can change one thing. You slowly evolve things over time. Your changes don&amp;#39;t break ten others because not everything is intermingled. &lt;/p&gt;&lt;p&gt;Now think about how an AI operates on a codebase. It works within a context window. That constraint isn&amp;#39;t unique to AI, people do that too. There is only so much you can keep in your head, and proper architecture means that you are separating concerns so you can work with just the relevant details in mind. &lt;/p&gt;&lt;p&gt;When your architecture is clean, the AI can focus on exactly the right piece of the system. When it isn&amp;#39;t, you&amp;#39;re either feeding the AI irrelevant noise or hiding the context it actually needs from it.&lt;/p&gt;&lt;p&gt;Good architecture, it turns out, is also a good AI interface. And the reason this works is the same as for people: it reduces the cognitive load you have to carry while understanding and modifying the system. For AI, we just call it the context window. For people, it is cognitive load. Same term, same concept. &lt;/p&gt;&lt;p&gt;Beyond the mechanical benefits, good architecture gives you two things that I think are underappreciated in this conversation.&lt;/p&gt;&lt;p&gt;The first is &lt;em&gt;structural comprehension&lt;/em&gt;. You don&amp;#39;t need to have every line of a large codebase in your head. But you do need a genuine mental model of how data flows, how components relate, and where things live. That&amp;#39;s only possible if the architecture actually reflects the system&amp;#39;s intent. &lt;/p&gt;&lt;p&gt;When using AI to generate code, you &lt;em&gt;need&lt;/em&gt;&amp;nbsp;to have a proper understanding of the flow of the system. That allows you to look at a pull request and understand the changes, their intent, and how they fit into the greater whole. Without that, you can&amp;#39;t meaningfully review the code. You&amp;#39;re just rubber-stamping diffs you don&amp;#39;t have a hope of understanding.&lt;/p&gt;&lt;p&gt;The second is that &lt;em&gt;the work has shifted&lt;/em&gt;. We&amp;#39;re moving from &amp;quot;how do I write this code?&amp;quot; to &amp;quot;how do I review all of this code?&amp;quot;. Nobody is going to meaningfully maintain 30,000 lines a day of dense AI code. At that point, the codebase has escaped human comprehension, and you&amp;#39;ve &lt;em&gt;lost&lt;/em&gt;the game. This isn&amp;rsquo;t your project anymore, and sooner or later, you&amp;rsquo;ll face the Big Decision.&lt;/p&gt;&lt;h2&gt;Turtles all the way down&lt;/h2&gt;&lt;p&gt;I hear the proposed solution constantly: &amp;quot;I have an agent that writes the code, an agent that tests it, an agent that reviews the reviews, and so on.&amp;quot; This is, I think, genuinely insane for anything that matters.&lt;/p&gt;&lt;p&gt;We already have evidence from the field that this doesn&amp;rsquo;t work. &lt;a href="https://www.digitaltrends.com/computing/ai-code-wreaked-havoc-with-amazon-outage-and-now-the-company-is-making-tight-rules/"&gt;Amazon has had production failures from AI-generated code&lt;/a&gt;&amp;nbsp;produced through exactly these kinds of layered-AI pipelines. Microsoft&amp;#39;s aggressive approach to AI integration has shown what happens when &lt;a href="https://www.digitaltrends.com/computing/from-microsoft-to-microslop-the-ai-backlash-that-forced-a-reset/"&gt;AI-generated code enters production with minimal meaningful human oversight&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;In both of those cases, the &amp;ldquo;proper oversight&amp;rdquo; was also provided by AI. And the end result wasn&amp;rsquo;t encouraging for this pattern of behavior. For critical systems that carry &lt;a href="https://dev.to/tyson_cung/amazon-lost-63m-orders-after-ai-coding-tool-went-rogue-now-theyre-hitting-the-brakes-2h7p"&gt;real consequences&lt;/a&gt;, &amp;quot;AI supervising AI&amp;quot; is not a thing.&lt;/p&gt;&lt;p&gt;AI works when you treat it as a tool in your hands, not as an autonomous system you&amp;#39;ve delegated to. An engineer who understands architecture and can look at a diff and say &amp;quot;this is right&amp;quot; or &amp;quot;this is wrong, and here&amp;#39;s why&amp;quot; is &lt;em&gt;much &lt;/em&gt;more capable with AI than without it. &lt;/p&gt;&lt;p&gt;An engineer who has offloaded comprehension to the machine is flying blind; worse, they are flying very fast directly into a cliff wall. &lt;/p&gt;&lt;h2&gt;What should you do about it?&lt;/h2&gt;&lt;p&gt;When we treat AI agents as a tool, it turns out that not all that much &lt;em&gt;needs&lt;/em&gt;&amp;nbsp;to change. The current processes you have in place (CI/CD, testing, review cycles, etc.) are all about being able to &lt;em&gt;generate trust&lt;/em&gt;&amp;nbsp;in the new code being written. Whether a human wrote it or a GPU did is less interesting.&lt;/p&gt;&lt;p&gt;At the same time, we have decades of experience building big systems. We know that a Big Ball of Mud isn&amp;rsquo;t sustainable. We know that proper architecture means breaking the system into digestible chunks. Yes, with AI you can throw everything together, and it will sort of work for a surprisingly long time. Until it doesn&amp;rsquo;t.&lt;/p&gt;&lt;p&gt;With a proper architecture, the scope you need to keep track of is inherently limited. That allows you to evolve over time and make changes that are inherently limited in scope (thus, reviewable, actionable, etc.).&lt;/p&gt;&lt;p&gt;&amp;ldquo;The more things change, the more they stay the same.&amp;rdquo; It is a nice saying, but it also carries a fundamental truth. Using AI doesn&amp;rsquo;t absolve us from the realities on the ground, after all.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203940-c/using-ai-agents-in-long-lived-software-projects?Key=ba47e46e-bfb5-454a-ad3e-178ebc501c01</link><guid>http://ayende.com/203940-c/using-ai-agents-in-long-lived-software-projects?Key=ba47e46e-bfb5-454a-ad3e-178ebc501c01</guid><pubDate>Tue, 21 Apr 2026 12:00:00 GMT</pubDate></item><item><title>Agents, Code Reviews, and the Bottleneck Shift, Oh My!</title><description>&lt;p&gt;Like everything else, we have been using AI in various forms for a while now, from asking ChatGPT to write a function to asking it to explain an error, then graduating to running it on our code in the IDE, and finally to full-blown independent coding assistants. &lt;/p&gt;&lt;p&gt;Recently, we shifted into a much higher gear, rolling it out across most of the teams at RavenDB. I want to talk specifically about what that looks like in practice in real production software.&lt;/p&gt;&lt;p&gt;RavenDB is a mature codebase, with about 18 years of history behind it. The core team is a few dozen developers working on this full-time. We also care &lt;em&gt;very&lt;/em&gt;&amp;nbsp;deeply about correctness, performance, and maintainability. &lt;/p&gt;&lt;p&gt;With all the noise about Claude, Codex, and their ilk recently, we decided to run some experiments to see how we can leverage them to help us build RavenDB.&lt;/p&gt;&lt;h2&gt;The numbers that got my attention&lt;/h2&gt;&lt;p&gt;We started with features that were relatively self-contained &amp;mdash; ambitious enough to be real work, but isolated enough that an AI agent could take them end-to-end without stepping on core aspects of RavenDB. &lt;/p&gt;&lt;p&gt;The first one was estimated at about a month of work for a senior developer. We completed it in two days. To be fair, a significant portion of that time was spent learning how to work effectively with Claude as an agent, learning the ropes and the right discipline and workflows, not just the task itself.&lt;/p&gt;&lt;p&gt;The second was estimated at roughly three months for an initial version. It was delivered in about a week. And we didn&amp;#39;t just hit the target &amp;mdash; we significantly exceeded the planned feature set.&lt;/p&gt;&lt;p&gt;In terms of efficiency, we are talking about a proper &lt;em&gt;leap&lt;/em&gt;&amp;nbsp;from what we previously could expect. &lt;/p&gt;&lt;h2&gt;This isn&amp;#39;t vibe coding&lt;/h2&gt;&lt;p&gt;I want to be direct about something: this is not &amp;quot;prompt it and ship it.&amp;quot; There is a discipline required here. The AI can move very fast, explore a lot of ground, and generate code that looks right, but isn&amp;rsquo;t. Code ownership and engineering responsibility don&amp;#39;t go away; they become much more demanding.&lt;/p&gt;&lt;p&gt;I personally sat and read 30,000 lines of code. I had to understand what was there, push back on decisions, redirect the approach, and enforce the standards that RavenDB has built up over many years. &lt;/p&gt;&lt;p&gt;Those 30,000 lines of code didn&amp;rsquo;t appear out of thin air. They were the final result of a &lt;em&gt;lot&lt;/em&gt;&amp;nbsp;of planning, back and forth with the agent, incremental steps in the right direction (and many wrong ones, etc.).&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;To be fair, 30,000 lines of code sounds like a &lt;em&gt;lot&lt;/em&gt;, right? About 60% of that is actually tests, and about half of the remaining code is boilerplate infrastructure that we need to have, but isn&amp;rsquo;t really interesting. &lt;/p&gt;&lt;p&gt;The juicy parts are only around 5,000 lines or so. &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;In many respects, this isn&amp;rsquo;t prompt-and-go but feels a lot more like a pair programming session on steroids. &lt;/p&gt;&lt;p&gt;What AI agents give you is the ability to explore the problem space cheaply and quickly. After we had something built, I had a different idea about how to go about implementing it. So I asked it to do that, and it gave me something that I could actually &lt;em&gt;explore&lt;/em&gt;. &lt;/p&gt;&lt;p&gt;Being able to evaluate multiple different approaches to a solution is &lt;em&gt;crazy&lt;/em&gt;&amp;nbsp;valuable.&amp;nbsp;It is transformative for architectural decisions. &lt;/p&gt;&lt;p&gt;Having said that, using a coding agent to take all the boilerplate stuff meant that I was able to focus on the &amp;ldquo;fun parts&amp;rdquo;, the pieces that actually add the most value, not everything else that I need to do to get to that part.&lt;/p&gt;&lt;h2&gt;What this means going forward&lt;/h2&gt;&lt;p&gt;AI agents are going to amplify your existing engineering culture, for better or worse.&lt;/p&gt;&lt;p&gt;A lot of the cost of writing good software is going to move from actually writing code to reviewing it. For many people, the act of writing the code was also the part where they &lt;em&gt;thought&lt;/em&gt;&amp;nbsp;about it most deeply. &lt;/p&gt;&lt;p&gt;Now the thinking part moves either upfront, at the planning phase, or to the end, when you look at the pull request. Reading a pull request, you could reasonably expect to see code that has already been reasoned about and properly tamed. &lt;/p&gt;&lt;p&gt;Now, in some cases, this is the first time that a human is actually going to properly walk through the whole thing. To ensure proper quality, you also need to shift a lot of your focus to that part. &lt;/p&gt;&lt;p&gt;The bottleneck for good software is going to be the review cycle, the architectural approach, and an experienced team that can actually evaluate the output and ensure consistent high quality.&lt;/p&gt;&lt;p&gt;Without that, you can go very fast, but just generating code quickly is a losing proposition. You&amp;rsquo;ll go very fast directly into a painful collision with a wall.&lt;/p&gt;&lt;p&gt;We are still settling down and trying to properly understand the best approach to take, but I have to say that this experiment was a major success.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203939-c/agents-code-reviews-and-the-bottleneck-shift-oh-my?Key=a4aac51e-45ff-426d-8340-9c2056e743f0</link><guid>http://ayende.com/203939-c/agents-code-reviews-and-the-bottleneck-shift-oh-my?Key=a4aac51e-45ff-426d-8340-9c2056e743f0</guid><pubDate>Fri, 17 Apr 2026 12:00:00 GMT</pubDate></item><item><title>The 'Million AI Monkeys' Hypothesis &amp; Real-World Projects</title><description>&lt;p&gt;I have run into &lt;a href="https://x.com/johnrushx/status/2026961833723113782?s=52"&gt;this post&lt;/a&gt;&amp;nbsp;by John Rush, which I found really interesting, mostly because I so vehemently disagree with it. Here are the points that I want to address in John&amp;rsquo;s thesis:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;1. Open&amp;nbsp;Source movement gonna&amp;nbsp;end because AI can rewrite any oss repo into a new code and commercially redistribute it as their own.&lt;/p&gt;&lt;p&gt;2. Companies gonna use AI to generate their none core software as a marketing effort (cloudflare rebuilt nextjs&amp;nbsp;in &amp;nbsp;a week).&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;em&gt;Can&lt;/em&gt;&amp;nbsp;AI rewrite an OSS repo into new code? Let&amp;rsquo;s dig into this a little bit.&lt;/p&gt;&lt;p&gt;AI models today do a &lt;em&gt;great&lt;/em&gt;&amp;nbsp;job of translating code from one language to another. We have good testimonies that this is actually a pretty useful scenario, such as the recent translation of the &lt;a href="https://ladybird.org/posts/adopting-rust/"&gt;Ladybird JS engine to Rust&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;At RavenDB, we have been using that to manage our client APIs (written in multiple languages &amp;amp; platforms). It has been a great help with that.&lt;/p&gt;&lt;p&gt;But that is fundamentally the same as the &lt;a href="https://web.archive.org/web/20081205075009/https://msdn.microsoft.com/en-us/vstudio/aa718346.aspx"&gt;Java to C# converter&lt;/a&gt;&amp;nbsp;that shipped with Visual Studio 2005. That is 2005, not 2025, mind you. The link above is to the Wayback Machine because the original link itself is lost to history. &lt;/p&gt;&lt;p&gt;AI models do a much better job here, but they aren&amp;rsquo;t bringing something new to the table in this context. &lt;/p&gt;&lt;h2&gt;Claude C Compiler&lt;/h2&gt;&lt;p&gt;Now, let&amp;rsquo;s talk about using the model to replicate a project from scratch. And here we have a bunch of examples. There is the Claude C Compiler, an impressive feat of engineering that can compile the Linux kernel. &lt;/p&gt;&lt;p&gt;Except&amp;hellip; it is a proof of concept that you wouldn&amp;rsquo;t &lt;em&gt;want&lt;/em&gt;&amp;nbsp;to use. It produces code that is significantly slower than GCC, and its output is not something that you can trust. And it is not in a shape to be a long-term project that you would maintain over the years. &lt;/p&gt;&lt;p&gt;For a young project, being slower than the best-of-breed alternative is not a bad thing. You&amp;rsquo;ve shown that your project works; now you can work on optimization. &lt;/p&gt;&lt;p&gt;For an AI project, on the other hand, you are in a pretty bad place. The key here is in terms of long-term maintainability. There is &lt;a href="https://www.modular.com/blog/the-claude-c-compiler-what-it-reveals-about-the-future-of-software"&gt;a great breakdown of the Claude C Compiler from the creator of Clang&lt;/a&gt;&amp;nbsp;that I highly recommend reading.&lt;/p&gt;&lt;p&gt;The amount of work it would require to turn it into actual production-level code is &lt;em&gt;enormous&lt;/em&gt;. I think that it would be fair to say that the overall cost of building a production-level compiler with AI would be in the same ballpark as writing one directly.&lt;/p&gt;&lt;p&gt;Many of the issues in the Claude C Compiler are not bugs that you can &amp;ldquo;just fix&amp;rdquo;. They are deep architectural issues that require a very different approach. &lt;/p&gt;&lt;p&gt;Leaving that aside, let&amp;rsquo;s talk about the actual use case. The Linux kernel&amp;rsquo;s relationship with its compiler is not a trivial one. Compiler bugs and behaviors are routine issues that developers run into and need to work on. &lt;/p&gt;&lt;p&gt;See the occasional &amp;ldquo;discussion&amp;rdquo; on undefined behavior optimizations by the compiler for surprisingly straightforward code. &lt;/p&gt;&lt;h1&gt;Cloudflare&amp;rsquo;s vinext&lt;/h1&gt;&lt;p&gt;So Cloudflare &lt;a href="https://blog.cloudflare.com/vinext/"&gt;rebuilt Next.js in a week using AI&lt;/a&gt;. That is pretty impressive, but that is also a lie. They might have done &lt;em&gt;some&lt;/em&gt;&amp;nbsp;work in a week, but that isn&amp;rsquo;t something that is ready. Cloudflare is directly calling this highly experimental (very rightly so).&lt;/p&gt;&lt;p&gt;They also have several customers using it in production already. That is awesome news, except that within literal days of this announcement, &lt;a href="https://x.com/rauchg/status/2026864132423823499"&gt;multiple critical vulnerabilities have been found in this project&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;A new project having vulnerabilities is not unexpected. But some of those vulnerabilities were &lt;a href="https://x.com/samwcyo/status/2026888257779224594"&gt;literal copies of (fixed) vulnerabilities in the original&lt;/a&gt;&amp;nbsp;Next.js project. &lt;/p&gt;&lt;p&gt;The issue here is the pace of change and the impact. If it takes an agent a week to build a project and then you throw that into production, how much real testing has been done on it? How much is that code &lt;em&gt;worth&lt;/em&gt;? &lt;/p&gt;&lt;p&gt;John stated that this vinext&amp;nbsp;project for Cloudflare was a marketing effort. I have to note that they had to pay bug bounties as a result and exposed their customers to higher levels of risk. I don&amp;rsquo;t consider that a plus. There is also now the ongoing maintenance cost to deal with, of course. &lt;/p&gt;&lt;p&gt;The key here is that a line of code is not something that you look at in isolation. You need to look at its totality. Its history, usage, provenance, etc. A line of code in a project that has been battle-tested in production is &lt;em&gt;far&lt;/em&gt;&amp;nbsp;more valuable than a freshly generated one.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;ll refer again to the awesome &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/"&gt;&amp;ldquo;Things You Should Never Do&amp;rdquo; from Spolsky&lt;/a&gt;. That is over 25 years old and is &lt;em&gt;still &lt;/em&gt;excellent advice, even in the age of AI-generated code.&lt;/p&gt;&lt;h1&gt;NanoClaw&amp;rsquo;s approach&lt;/h1&gt;&lt;p&gt;You&amp;rsquo;ve probably heard about the Clawdbot &amp;rArr; Moltbot &amp;rArr; &lt;a href="https://openclaw.ai/"&gt;OpenClaw&lt;/a&gt;, a way to plug AI directly into everything and give your CISO a heart attack. That is an interesting story, but from a technical perspective, I want to focus on what it &lt;em&gt;does&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;A key part of what made OpenClaw successful was the number of integrations it has. You can connect it to Telegram, WhatsApp, Discord, and more. You can plug it into your Gmail, Notes, GitHub, etc.&lt;/p&gt;&lt;p&gt;It has about half a million lines of code (TypeScript), which were mostly generated by AI as well.&lt;/p&gt;&lt;p&gt;To contrast that, we have &lt;a href="https://nanoclaw.net/"&gt;NanoClaw&lt;/a&gt;&amp;nbsp;with ~500 lines of code. Not a typo, it is roughly a &lt;em&gt;thousand &lt;/em&gt;times smaller than OpenClaw. The key difference between these two projects is that NanoClaw rebuilds itself on the fly.&lt;/p&gt;&lt;p&gt;If you want to integrate with Telegram, for example, NanoClaw will use the AI model to &lt;em&gt;add the Telegram integration&lt;/em&gt;. In this case, it will use pre-existing code and use the model as a weird plugin system. But it also has the ability to &lt;em&gt;generate&lt;/em&gt;&amp;nbsp;new code for integrations it doesn&amp;rsquo;t already have. &lt;a href="https://deepwiki.com/gavrielc/nanoclaw/8.3-customize-skill-(customize)"&gt;See here for more details&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;On the one hand, that is a pretty neat way to reduce the overall code in the project. On the other hand, it means that each user of NanoClaw will have their own &lt;em&gt;bespoke&lt;/em&gt;&amp;nbsp;system.&lt;/p&gt;&lt;p&gt;Contrasting the OpenClaw and NanoClaw approaches, we have an interesting problem. Both of those systems are primarily built with AI, but NanoClaw is likely going to show a lot more variance in what is actually running on your system.&lt;/p&gt;&lt;p&gt;For example, if I want to use Signal as a communication channel, OpenClaw has that built in. You can integrate Signal into NanoClaw as well, but it will generate code (using the model) for this integration &lt;em&gt;separately&lt;/em&gt;&amp;nbsp;for each user who needs it.&lt;/p&gt;&lt;p&gt;A bespoke solution for each user may &lt;em&gt;sound&lt;/em&gt;&amp;nbsp;like a nice idea, but it just means that each NanoClaw is its own special snowflake. Just thinking about supporting something like that across many users gives me the shivers. &lt;/p&gt;&lt;p&gt;For example, &lt;a href="https://www.oasis.security/blog/openclaw-vulnerability"&gt;OpenClaw had an agent takeover vulnerability&lt;/a&gt;&amp;nbsp;(reported literally yesterday) that would allow a simple website visit to completely own the agent (with all that this implies). OpenClaw&amp;rsquo;s design means that it can be fixed in a single location.&lt;/p&gt;&lt;p&gt;NanoClaw&amp;rsquo;s design, on the other hand, means that for each user, there is a slightly different implementation, which may or may not be vulnerable. And there is no really good way to actually fix this.&lt;/p&gt;&lt;h1&gt;Summary&lt;/h1&gt;&lt;p&gt;The idea that you can just throw AI at a problem and have it generate code that you can then deploy to production is an attractive one. It is also by no means a new one. &lt;/p&gt;&lt;p&gt;The notion of &lt;a href="https://en.wikipedia.org/wiki/Computer-aided_software_engineering"&gt;CASE tools&lt;/a&gt;&amp;nbsp;used to be the way to go about it. The book &lt;a href="https://www.amazon.com/Application-Development-Without-Programmers-Martin/dp/0130389439/"&gt;Application Development Without Programmers&lt;/a&gt;&amp;nbsp;was published in 1982, for example. The world has changed since then, but we are still trying to get rid of programmers.&lt;/p&gt;&lt;p&gt;Generating code quickly is easy these days, but that just shifts the burden. The cost of &lt;em&gt;verifying&lt;/em&gt;&amp;nbsp;code has become a lot more pronounced. Note that I didn&amp;rsquo;t say expensive. It used to be the case that writing the code and verifying it were almost the same task. You wrote the code and thus had a human verifying that it made sense. Then there are the other review steps in a proper software lifecycle.&lt;/p&gt;&lt;p&gt;When we can drop 15,000 lines of code in a few minutes of prompting, the entire story changes. The value of a line of code on its own approaches zero. The value of a &lt;em&gt;reviewed&lt;/em&gt;&amp;nbsp;line of code, on the other hand, hasn&amp;rsquo;t changed.&lt;/p&gt;&lt;p&gt;A line of code from a battle-tested, mature project is infinitely more valuable than a newly generated one, regardless of how quickly it was produced. The cost of generating code approaches zero, sure.&lt;/p&gt;&lt;p&gt;But newly generated code isn&amp;rsquo;t &lt;em&gt;useful&lt;/em&gt;. In order for me to actually make use of that, I need to verify it and ensure that I can trust it. More importantly, I need to know that I can &lt;em&gt;build&lt;/em&gt;&amp;nbsp;on top of it. &lt;/p&gt;&lt;p&gt;I don&amp;rsquo;t see a lot of people paying attention to the concept of long-term maintainability for projects. But that is key. Otherwise, you are signing up upfront to be a legacy system that no one understands or can properly operate.&lt;/p&gt;&lt;p&gt;Production-grade software isn&amp;rsquo;t a prompt away, I&amp;rsquo;m afraid to say. There are still all the &lt;em&gt;other&lt;/em&gt;&amp;nbsp;hurdles that you have to go through to actually &lt;em&gt;mature&lt;/em&gt;&amp;nbsp;a project to be able to go all the way to production and evolve over time without exploding costs &amp;amp; complexities.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203907-b/the-million-ai-monkeys-hypothesis-real-world-projects?Key=76196b00-f0e4-4d7a-a587-4187133e99c0</link><guid>http://ayende.com/203907-b/the-million-ai-monkeys-hypothesis-real-world-projects?Key=76196b00-f0e4-4d7a-a587-4187133e99c0</guid><pubDate>Fri, 27 Feb 2026 12:00:00 GMT</pubDate></item><item><title>AI &amp; the movie Eagle Eye</title><description>&lt;p&gt;miscellaneous, development&lt;/p&gt;&lt;p&gt;In 2008, the movie Eagle Eye came out. I remember watching that at the time and absolutely loving this movie. It is an action movie, so enjoying it once is the sole criteria that I have. Surprisingly, I got flashbacks of this movie repeatedly in the past few weeks.&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/iDVRZlLhy3kwVTSqtLZQkQ.png"/&gt;&lt;/p&gt;&lt;p&gt;I think it is safe to talk about &amp;ldquo;spoilers&amp;rdquo; for a movie that is old enough to drive, so the core idea in this movie is that an AI wants to perform a certain action, but is prevented from doing so. It then comes up with a pretty convoluted approach to bypassing those limits. I&amp;rsquo;m intentionally vague here, because the movie is actually good and you should watch it.&lt;/p&gt;&lt;p&gt;The key here, which is the reason that I remember an 18 years old movie, is that we are actually seeing this behavior today with AI agents. It is an entirely&amp;nbsp;relatable phenomenon&amp;nbsp;to see the agent running into an obstacle, and then trying to bypass it using crazier and crazier techniques.&lt;/p&gt;&lt;p&gt;The movie aged particularly well in this regard, because what was a plot device in there is a daily occurrence in our lives now. For reference, &lt;a href="https://x.com/summeryue0/status/2025774069124399363"&gt;see this Tweet&lt;/a&gt;.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203875-a/ai-the-movie-eagle-eye?Key=84506b1e-56e9-4c6e-9340-9228f2780ae9</link><guid>http://ayende.com/203875-a/ai-the-movie-eagle-eye?Key=84506b1e-56e9-4c6e-9340-9228f2780ae9</guid><pubDate>Wed, 25 Feb 2026 12:00:00 GMT</pubDate></item><item><title>The hole in my falloaction</title><description>&lt;p&gt;I am working a bit with sparse files, and I need to output the list of holes in my file. &lt;/p&gt;&lt;p&gt;To my great surprise, I found that my file had more holes than I put into it. This probably deserves a bit of explanation. &lt;/p&gt;&lt;p&gt;If you know what sparse files are, feel free to skip this explanation:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;A sparse filereduces disk space usage by storing only the &lt;em&gt;non-zero&lt;/em&gt;&amp;nbsp;data blocks.Zero-filled regions (&amp;quot;holes&amp;quot;) are recorded as file system metadata&amp;nbsp;only. &lt;/p&gt;&lt;p&gt;The file still has the same &amp;ldquo;size&amp;rdquo;, but we don&amp;rsquo;t &lt;em&gt;need&lt;/em&gt;&amp;nbsp;to dedicate actual disk space for ranges that are filled with zeros, we can just remember that there are zeros there. This is a natural consequence of the fact that files aren&amp;rsquo;t actually composed of linear space on disk.&lt;/p&gt;&lt;p&gt;Filesystems grow files using extents (contiguous disk chunks).A file initially gets a single&amp;nbsp;extent (e.g., 1MB).Fast I/O is maintained as sequential data fills this contiguous block.Once&amp;nbsp;the extent is full, the filesystem allocates a new, separate extent (which will &lt;em&gt;not&lt;/em&gt;&amp;nbsp;reside next to the previous one, most likely).The file&amp;#39;s &lt;em&gt;logical&lt;/em&gt;&amp;nbsp;size grows continuously, but physical allocation occurs in discrete bursts as new extents are dynamically added.&lt;/p&gt;&lt;p&gt;If you are old enough to remember running defrag, that was essentially what it did. Ensured that the whole file was a single continuous allocation on disk. Because of this, it is very simple for a file system to just record holes, and the only file system that you&amp;rsquo;ll find in common use today that doesn&amp;rsquo;t support it is FAT.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;At any rate, I had a problem. My file has more holes than expected, and that is &lt;em&gt;not&lt;/em&gt;&amp;nbsp;a good thing. This is the sort of thing that calls for a &amp;ldquo;Stop, investigate, blog&amp;rdquo; reaction. Hence, this post.&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s see a small example that demonstrates this:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-bash'&gt;&lt;code class='line-numbers language-bash'&gt;&lt;span class="token comment"&gt;#define _GNU_SOURCE&lt;/span&gt;
&lt;span class="token comment"&gt;#include &amp;lt;stdio.h&gt;&lt;/span&gt;
&lt;span class="token comment"&gt;#include &amp;lt;fcntl.h&gt;&lt;/span&gt;
&lt;span class="token comment"&gt;#include &amp;lt;unistd.h&gt;&lt;/span&gt;


int &lt;span class="token function-name function"&gt;main&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    const off_t file_size &lt;span class="token operator"&gt;=&lt;/span&gt; 1024LL * &lt;span class="token number"&gt;1024&lt;/span&gt; * &lt;span class="token number"&gt;1024&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    int fd &lt;span class="token operator"&gt;=&lt;/span&gt; open&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"test-sparse-file.dat"&lt;/span&gt;, O_CREAT &lt;span class="token operator"&gt;|&lt;/span&gt; O_RDWR &lt;span class="token operator"&gt;|&lt;/span&gt; O_TRUNC, 0644&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    fallocate&lt;span class="token punctuation"&gt;(&lt;/span&gt;fd, &lt;span class="token number"&gt;0&lt;/span&gt;, &lt;span class="token number"&gt;0&lt;/span&gt;, file_size&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    
    off_t offset &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;while&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;offset &lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt; file_size&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        off_t hole_start &lt;span class="token operator"&gt;=&lt;/span&gt; lseek&lt;span class="token punctuation"&gt;(&lt;/span&gt;fd, offset, SEEK_HOLE&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;hole_start &lt;span class="token operator"&gt;&gt;=&lt;/span&gt; file_size&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token builtin class-name"&gt;break&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        
        off_t hole_end &lt;span class="token operator"&gt;=&lt;/span&gt; lseek&lt;span class="token punctuation"&gt;(&lt;/span&gt;fd, hole_start, SEEK_DATA&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;hole_end &lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; hole_end &lt;span class="token operator"&gt;=&lt;/span&gt; file_size&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        
        printf&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"Start: %.2f MB, End: %.2f MB&lt;span class="token entity" title="\n"&gt;\n&lt;/span&gt;"&lt;/span&gt;, 
               hole_start / &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;1024.0&lt;/span&gt; * &lt;span class="token number"&gt;1024.0&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;,
               hole_end / &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;1024.0&lt;/span&gt; * &lt;span class="token number"&gt;1024.0&lt;/span&gt;&lt;span class="token punctuation"&gt;))&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        
        offset &lt;span class="token operator"&gt;=&lt;/span&gt; hole_end&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;
    
    close&lt;span class="token punctuation"&gt;(&lt;/span&gt;fd&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token builtin class-name"&gt;return&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;If you run this code, you&amp;rsquo;ll see this surprising result:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-yaml'&gt;&lt;code class='line-numbers language-yaml'&gt;&lt;span class="token key atrule"&gt;Start&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; 0.00 MB&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token key atrule"&gt;End&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; 1024.00 MB&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;In other words, even though we just use &lt;code&gt;fallocate()&lt;/code&gt;&amp;nbsp;to &lt;em&gt;ensure&lt;/em&gt;&amp;nbsp;that we reserved the disk space, as far as &lt;code&gt;lseek()&lt;/code&gt;&amp;nbsp;is concerned, it is just one big hole. What is going on here?&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s dig a little deeper, using filefrag:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-yaml'&gt;&lt;code class='line-numbers language-yaml'&gt;$ filefrag &lt;span class="token punctuation"&gt;-&lt;/span&gt;b1048576 &lt;span class="token punctuation"&gt;-&lt;/span&gt;v test&lt;span class="token punctuation"&gt;-&lt;/span&gt;sparse&lt;span class="token punctuation"&gt;-&lt;/span&gt;file.dat 
&lt;span class="token key atrule"&gt;Filesystem type is&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; ef53
File size of test&lt;span class="token punctuation"&gt;-&lt;/span&gt;sparse&lt;span class="token punctuation"&gt;-&lt;/span&gt;file.dat is 1073741824 (1024 blocks of 1048576 bytes)
 &lt;span class="token key atrule"&gt;ext&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;logical_offset&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;        &lt;span class="token key atrule"&gt;physical_offset&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token key atrule"&gt;length&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;   &lt;span class="token key atrule"&gt;expected&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token key atrule"&gt;flags&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
   &lt;span class="token key atrule"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;        &lt;span class="token key atrule"&gt;0..      23&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165608..    165631&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;24&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;             unwritten
   &lt;span class="token key atrule"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;       &lt;span class="token key atrule"&gt;24..     151&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165376..    165503&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;128&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165632&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; unwritten
   &lt;span class="token key atrule"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;      &lt;span class="token key atrule"&gt;152..     279&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165248..    165375&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;128&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165504&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; unwritten
   &lt;span class="token key atrule"&gt;3&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;      &lt;span class="token key atrule"&gt;280..     407&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165120..    165247&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;128&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165376&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; unwritten
   &lt;span class="token key atrule"&gt;4&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;      &lt;span class="token key atrule"&gt;408..     535&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164992..    165119&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;128&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165248&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; unwritten
   &lt;span class="token key atrule"&gt;5&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;      &lt;span class="token key atrule"&gt;536..     663&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164864..    164991&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;128&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;165120&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; unwritten
   &lt;span class="token key atrule"&gt;6&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;      &lt;span class="token key atrule"&gt;664..     791&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164736..    164863&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;128&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164992&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; unwritten
   &lt;span class="token key atrule"&gt;7&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;      &lt;span class="token key atrule"&gt;792..     919&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164608..    164735&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;128&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164864&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; unwritten
   &lt;span class="token key atrule"&gt;8&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;      &lt;span class="token key atrule"&gt;920..    1023&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164480..    164583&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;    &lt;span class="token key atrule"&gt;104&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;     &lt;span class="token key atrule"&gt;164736&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; last&lt;span class="token punctuation"&gt;,&lt;/span&gt;unwritten&lt;span class="token punctuation"&gt;,&lt;/span&gt;eof
&lt;span class="token key atrule"&gt;test-sparse-file.dat&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; 9 extents found&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;You can see that the file is made of 9 separate extents. The first one is 24MB in size, then 7 extents that are 128MB each, and the final one is 104MB.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Amusingly enough, the physical layout of the file is in reverse order to the logical layout of the file. That is just the allocation pattern of the file system, since there is no relation between the two.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Now, let&amp;rsquo;s try to figure out what is going on here. Do you see the flags on those extents? It says &lt;code&gt;unwritten&lt;/code&gt;. That means this is physical space that was allocated to the file, but the file system is aware that it never wrote to that space. Therefore, that space &lt;em&gt;must&lt;/em&gt;&amp;nbsp;be zero. &lt;/p&gt;&lt;p&gt;In other words, conceptually, this unwritten space is no different from a sparse region in the file. In both cases, the file system can just hand me a block of zeros when I try to access it.&lt;/p&gt;&lt;p&gt;The question is, why is the file system behaving in this manner? And the answer is that this is an optimization. Instead of reading the data (which we &lt;em&gt;know&lt;/em&gt;&amp;nbsp;to be zeros) from the disk, we can just hand it over to the application directly. That saves on I/O, which is quite nice.&lt;/p&gt;&lt;p&gt;Consider the typical scenario of allocating a file and then writing to it. Without this optimization, we would literally &lt;em&gt;double&lt;/em&gt;&amp;nbsp;the amount of I/O &amp;nbsp;we have to do. &lt;/p&gt;&lt;p&gt;It turns out that this optimization also applies to Windows and Mac, but the reason I ran into that on Linux is that I used the &lt;code&gt;lseek(SEEK_HOLE)&lt;/code&gt;, which considers the unwritten portion as a sparse hole as well. This makes sense, since if I want to copy data and I am aware of sparse regions, I &lt;em&gt;should&lt;/em&gt;&amp;nbsp;treat the unwritten portions as holes as well. &lt;/p&gt;&lt;p&gt;You can use the &lt;code&gt;ioctl(FS_IOC_FIEMAP)&lt;/code&gt;&amp;nbsp;to inspect the actual file extents (this is what &lt;code&gt;filefrag&lt;/code&gt;&amp;nbsp;does) if you actually care about the difference.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203843-c/the-hole-in-my-falloaction?Key=792324ea-5ab1-498a-b596-3082054f0601</link><guid>http://ayende.com/203843-c/the-hole-in-my-falloaction?Key=792324ea-5ab1-498a-b596-3082054f0601</guid><pubDate>Thu, 05 Feb 2026 12:00:00 GMT</pubDate></item><item><title>A tale of one-off, coding agents and the shortest path to victory</title><description>&lt;p&gt;I needed to export all the messages from one of our Slack channels. Slack has a way of exporting &lt;em&gt;everything&lt;/em&gt;, but nothing that could easily just give me all the messages in a single channel.&lt;/p&gt;&lt;p&gt;There are tools like slackdump or Slack apps that I could use, and I tried, but I got lost trying to make it work. In frustration, I opened VS Code and wrote:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I want a simple node.js that accepts a channel name from Slack and export all the messages in the channel to a CSV file&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The output was a single script and instructions on how I should register to get the right token. It literally took me less time to ask for the script than to try to figure out how to use the &amp;ldquo;proper&amp;rdquo; tools for this.&lt;/p&gt;&lt;p&gt;The ability to do these sorts of one-off things is &lt;strong&gt;exhilarating&lt;/strong&gt;. &lt;/p&gt;&lt;p&gt;Keep in mind: this isn&amp;rsquo;t generally applicable if you need something that would actually work over time. See &lt;a href="https://ayende.com/blog/203779-A/maintainability-in-the-age-of-coding-agents?key=2301e977ca474a28969f5bdad6dcad9f"&gt;my other post&lt;/a&gt;&amp;nbsp;for details on that.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203811-a/a-tale-of-one-off-coding-agents-and-the-shortest-path-to-victory?Key=7d74c708-dfba-44a4-9fea-d70a6e35c936</link><guid>http://ayende.com/203811-a/a-tale-of-one-off-coding-agents-and-the-shortest-path-to-victory?Key=7d74c708-dfba-44a4-9fea-d70a6e35c936</guid><pubDate>Mon, 02 Feb 2026 12:00:00 GMT</pubDate></item><item><title>Maintainability in the age of coding agents</title><description>&lt;p&gt;Modern coding agents can generate a &lt;em&gt;lot&lt;/em&gt;&amp;nbsp;of code very quickly.What once consumed days or weeks of a person&amp;rsquo;s time is now a simple matter of a prompt and a coffee break.The question is whether this changes any of the fundamental principles ofsoftware development.&lt;/p&gt;
&lt;p&gt;A significant portion of software engineering&amp;nbsp;(beyond pure algorithms and data structure work) is not about the code itself, but about managing the social aspects of&amp;nbsp;building and evolving&amp;nbsp;the software&amp;nbsp;over time.&lt;/p&gt;
&lt;p&gt;Our system's architecture inherently mirrors the structure of the organization that builds it, as stated by Conway's Law.Therefore, software engineering deals a lot with how a software project&amp;nbsp;is structured to ensure that&amp;nbsp;a (human) team can deliver, make changes, and maintain it over time.&lt;/p&gt;
&lt;p&gt;That is&amp;nbsp;why maintainability is such a high-value target:&amp;nbsp;an unmaintainable project quickly becomes one no one can safely change.&amp;nbsp;A good example is OpenSSL circa Heartbleed, or your bank&amp;rsquo;s COBOL-based core systems.&lt;/p&gt;
&lt;p&gt;Does this still apply&amp;nbsp;in the&amp;nbsp;era of coding agents?If a new feature is needed, and I can simply ask a model to regenerate the whole thing from scratch, bypassing technical debt and re-incorporating all constraints, do I still need to worry about maintainability?&lt;/p&gt;
&lt;p&gt;My answer in this regard is emphatically &lt;em&gt;yes&lt;/em&gt;.There is &lt;em&gt;immense&lt;/em&gt;&amp;nbsp;value in ensuring the maintainability of projects, even in the age of AI agents.&lt;/p&gt;
&lt;p&gt;One of the most obvious answers is that a maintainable project minimizes the amount of code you must review and touch to make a change.Translating this into the language of Large Language Models, this means you are fundamentally reducing the required context needed to execute a change.&lt;/p&gt;
&lt;p&gt;It isn&amp;rsquo;t just about saving our token&amp;nbsp;budget. Even assuming an&amp;nbsp;essentially unlimited budget, the true value extends beyond mere computation cost.&lt;/p&gt;
&lt;p&gt;The maintainability of a software project remains critical because you cannot &lt;em&gt;trust&lt;/em&gt;&amp;nbsp;a model to act with absolute competence.You do not have the option of simply telling a model, "Make this application secure," and blindly expecting a perfect outcome. It will give you a thumbs-up&amp;nbsp;and place your product API key in the client-side code.&lt;/p&gt;
&lt;p&gt;Furthermore, in a mature software project, even one built entirely with AI, making substantial changes using an AI agent is incredibly risky.Consider the scenario where you spend a week with an agent, carefully tweaking the system's behavior, reviewing the code, and directing its output into the exact shape required.&lt;/p&gt;
&lt;p&gt;Six months later, you return to the same area for a change.If the model rewrites everything from scratch,&amp;nbsp;because it can, the entire context and history of those days and weeks of careful guidancewill be&amp;nbsp;lost.This lost context is far more valuable than the code itself.&lt;/p&gt;
&lt;p&gt;Remember Hyrum&amp;rsquo;s Law: "With a sufficient number of users of an API, it does not matter what you promise in the contract:all observable behaviors of your system will be depended on by somebody."&lt;/p&gt;
&lt;p&gt;The "sufficient number of users" is surprisingly low, and observable behaviors include non-obvious factors like performance characteristics, the order of elements in a JSON document, the packet merging algorithm in a router you weren&amp;rsquo;t even aware existed, etc.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The key is this: if&amp;nbsp;a coding agent routinely rewrites large swaths of code, you are not performing an equivalent exchange.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even if the old code had&amp;nbsp;been AI-generated, it was&amp;nbsp;subsequently subjected to human review, clarification, testing, and verification&amp;nbsp;by users, then deployed - and it survived the production environment and production loads.&lt;/p&gt;
&lt;p&gt;The entirely new code has no validated quality yet.You must still expend time and effort&amp;nbsp;to verify its correctness.&lt;em&gt;That&lt;/em&gt;&amp;nbsp;is the difference between the existing code and the new one.&lt;/p&gt;
&lt;p&gt;Over 25 years ago, &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/"&gt;Joel Spolsky wrote Things You Should Never Do&lt;/a&gt;&amp;nbsp;about the Netscape rewrite. That particular article has &lt;em&gt;withstood &lt;/em&gt;the test of time very well. And it is entirely relevant in the age of coding agents as well.&lt;/p&gt;
&lt;p&gt;Part of my job involves reviewing code on a project that is&amp;nbsp;over fifteen years old with over a million lines of code. The past week,I've reviewed pull requests ranging from changes of a few hundred lines&amp;nbsp;to one that changed over 10,000&amp;nbsp;lines of code.&lt;/p&gt;
&lt;p&gt;The complexity involved in code review scales exponentially with the amount of code changed, because you must understand not just the changed code,&amp;nbsp;but all its interactions with the rest of the system.&lt;/p&gt;
&lt;p&gt;That 10,000+ lines of code pull request is something that is applicable for &lt;em&gt;major&lt;/em&gt;&amp;nbsp;features, worth the time and effort that it takes to properly understand and evaluate the change.&lt;/p&gt;
&lt;p&gt;Thinking that you can just have a coding agent throw big changes on a project fundamentally misunderstands how projects thrive. And assuming you can have one agent write the code and another review it is a short trip to madness.&lt;/p&gt;
&lt;p&gt;In summary, maintainability in the age of coding agents looks remarkably like it did before.The essential requirements remain: clear boundaries, a consistent architecture, and the ability to go into a piece of code and understand exactly what it's doing.&lt;/p&gt;
&lt;p&gt;Funnily enough, the same aspects of good software engineering discipline also translate well into best practices for AI usage: limiting the scope of change, reducing the amount of required context, etc.&lt;/p&gt;
&lt;p&gt;You should aim to modify a single piece of code, or better yet, create new code instead of modifying existing,&amp;nbsp;validated code&amp;nbsp;(Open/Closed Principle).&lt;/p&gt;
&lt;p&gt;Even with AI, the human act of reviewing code is still crucial.And if your proposed solution is to have one AI agent review another, you have simply pushed the problem one layer up, as you are still faced with the necessity of specifying exactly what the system is supposed to be doing in a way that is unambiguous and clear.&lt;/p&gt;
&lt;p&gt;There is already a proper way to do that, we call it coding 🙂.&lt;/p&gt;
</description><link>http://ayende.com/203779-a/maintainability-in-the-age-of-coding-agents?Key=2301e977-ca47-4a28-969f-5bdad6dcad9f</link><guid>http://ayende.com/203779-a/maintainability-in-the-age-of-coding-agents?Key=2301e977-ca47-4a28-969f-5bdad6dcad9f</guid><pubDate>Fri, 30 Jan 2026 12:00:00 GMT</pubDate></item><item><title>API Design: Don't try to guess</title><description>&lt;p&gt;I was reviewing some code, and I ran into the following snippet. Take a look at it:&lt;/p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-java'&gt;&lt;code class='line-numbers language-java'&gt;&lt;span class="token keyword"&gt;public&lt;/span&gt; &lt;span class="token keyword"&gt;void&lt;/span&gt; &lt;span class="token class-name"&gt;AddAttachment&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;string fileName&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token class-name"&gt;Stream&lt;/span&gt; stream&lt;span class="token punctuation"&gt;)&lt;/span&gt;
   &lt;span class="token punctuation"&gt;{&lt;/span&gt;
       &lt;span class="token class-name"&gt;ValidationMethods&lt;span class="token punctuation"&gt;.&lt;/span&gt;AssertNotNullOrEmpty&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;fileName&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;nameof&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;fileName&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
       &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;stream &lt;span class="token operator"&gt;==&lt;/span&gt; &lt;span class="token keyword"&gt;null&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
           &lt;span class="token keyword"&gt;throw&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;nameof&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;stream&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


       string type &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;GetContentType&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;fileName&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


       _attachments&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Add&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;PutAttachmentCommandData&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"__this__"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; fileName&lt;span class="token punctuation"&gt;,&lt;/span&gt; stream&lt;span class="token punctuation"&gt;,&lt;/span&gt; type&lt;span class="token punctuation"&gt;,&lt;/span&gt; changeVector&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;string&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Empty&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
   &lt;span class="token punctuation"&gt;}&lt;/span&gt;


   &lt;span class="token keyword"&gt;private&lt;/span&gt; &lt;span class="token keyword"&gt;static&lt;/span&gt; string &lt;span class="token class-name"&gt;GetContentType&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;string fileName&lt;span class="token punctuation"&gt;)&lt;/span&gt;
   &lt;span class="token punctuation"&gt;{&lt;/span&gt;
       &lt;span class="token keyword"&gt;var&lt;/span&gt; extension &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;Path&lt;span class="token punctuation"&gt;.&lt;/span&gt;GetExtension&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;fileName&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
       &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;string&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;extension&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
           &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token string"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt; &lt;span class="token comment"&gt;// Default fallback&lt;/span&gt;


       &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;extension&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token keyword"&gt;switch&lt;/span&gt;
       &lt;span class="token punctuation"&gt;{&lt;/span&gt;
           &lt;span class="token string"&gt;".jpg"&lt;/span&gt; or &lt;span class="token string"&gt;".jpeg"&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token string"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
           &lt;span class="token string"&gt;".png"&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token string"&gt;"image/png"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
           &lt;span class="token string"&gt;".webp"&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token string"&gt;"image/webp"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
           &lt;span class="token string"&gt;".gif"&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token string"&gt;"image/gif"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
           &lt;span class="token string"&gt;".pdf"&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token string"&gt;"application/pdf"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
           &lt;span class="token string"&gt;".txt"&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token string"&gt;"text/plain"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
           _ &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token string"&gt;"application/octet-stream"&lt;/span&gt;
       &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
   &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;p&gt;I don&amp;rsquo;t like this code because the API is trying to guess the intent of the caller. We are making some reasonable inferences here, for sure, but we are also ensuring that any future progress will require us to change our code, instead of letting the caller do that.&lt;/p&gt;&lt;p&gt;In fact, the caller probably knows a &lt;em&gt;lot&lt;/em&gt;&amp;nbsp;more than we do about what is going on. They know if they are uploading an image, and probably in what format too. They know that they just uploaded a CSV file (and that we need to classify it as plain text, etc.).&lt;/p&gt;&lt;p&gt;This is one of those cases where the best option is &lt;em&gt;not&lt;/em&gt;&amp;nbsp;to try to be smart. I recommended that we write the function to let the caller deal with it.&lt;/p&gt;&lt;p&gt;It is important to note that this is meant to be a public API in a library that is shipped to external customers, so changing something in the library is not easy (change, release, deploy, update - that can take a &lt;em&gt;while&lt;/em&gt;). We need to make sure that we aren&amp;rsquo;t &lt;em&gt;blocking&lt;/em&gt;&amp;nbsp;the caller from doing things they may want to. &lt;/p&gt;&lt;p&gt;This is a case of trying to help the user, but instead ending up crippling what they can do with the API.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203747-a/api-design-dont-try-to-guess?Key=683e3e2e-0a5f-4f10-a978-04e0f2fab851</link><guid>http://ayende.com/203747-a/api-design-dont-try-to-guess?Key=683e3e2e-0a5f-4f10-a978-04e0f2fab851</guid><pubDate>Thu, 29 Jan 2026 12:00:00 GMT</pubDate></item><item><title>Introducing: RavenDB Kubernetes Operator</title><description>&lt;p&gt;RavenDB has recently introduced its dedicated Kubernetes Operator, a big improvement over the Helm charts that teams have been using. This is meant to streamline database orchestration and management, essentially giving you an automated &amp;quot;SRE-in-a-box.&amp;quot;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;You can read the &lt;a href="https://github.com/ravendb/ravendb/discussions/22115"&gt;full announcement here&lt;/a&gt;. And the actual operator &lt;a href="https://github.com/ravendb/ravendb-operator?tab=readme-ov-file#ravendb-kubernetes-operator"&gt;is available here&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The Operator shifts the management paradigm from manual configuration to a declarative model. Simply applying a &lt;code&gt;RavenDBCluster &lt;/code&gt;custom resource definition (CRD) allows developers to automate the heavy lifting of cluster formation, storage binding, and external networking, removing the operational friction typically associated with running stateful distributed systems on K8s.&lt;/p&gt;&lt;p&gt;Most importantly, it isn&amp;rsquo;t a one-time thing. The RavenDB Kubernetes Operator is all about &amp;quot;Day 2&amp;quot; operational intelligence. It handles complex lifecycle tasks with high precision, such as executing safe rolling upgrades with built-in validation gates to prevent breaking changes.&lt;/p&gt;&lt;p&gt;From dealing with the intricacies of certificate rotation&amp;mdash;supporting both Let&amp;rsquo;s Encrypt and private PKI&amp;mdash;to providing real-time health insights directly via &lt;code&gt;kubectl&lt;/code&gt;, the automation of these critical maintenance tasks lets the Operator ensure that your RavenDB clusters remain resilient, secure, and performant with minimal manual intervention.&lt;/p&gt;&lt;p&gt;For example, you can push an upgrade from RavenDB 7.0 to RavenDB 7.2, and the Operator will automatically handle performing a rolling upgrade for you, ensuring there is no downtime during deployment. There is no need for complex orchestration playbooks, you just push the update, and it happens for you.&lt;/p&gt;&lt;p&gt;This is part of the same DevOps push we are making. &lt;a href="https://ravendb.net/articles/ravendb-ansible-collection-new-features-12-25?utm_source=linkedin&amp;utm_medium=organic&amp;utm_campaign=New_Features"&gt;If you are partial to Ansible, on the other hand, we have recently published great support there as well&lt;/a&gt;.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203715-a/introducing-ravendb-kubernetes-operator?Key=cca815c3-a486-4117-a0dd-0a9047b6095d</link><guid>http://ayende.com/203715-a/introducing-ravendb-kubernetes-operator?Key=cca815c3-a486-4117-a0dd-0a9047b6095d</guid><pubDate>Thu, 15 Jan 2026 12:00:00 GMT</pubDate></item><item><title>Hiring: Sales Engineer in Europe</title><description>&lt;p&gt;I&amp;rsquo;m looking for a key technical voice to join the team: a Sales Engineer who will be based in a GMT to GMT+3 time zone&amp;nbsp;to best support our growing European and international customer base.&lt;/p&gt;&lt;p&gt;We want someone who is passionate about solving complex technical challenges who can have fun talking to people and building relationships.You&amp;rsquo;ll bridge the gap between our technology and our customers&amp;#39; business needs.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The Technical Chops:&lt;/strong&gt;We need a technical champion for the sales process.That means diving deep into solution architecture, designing and executing proof-of-concepts, and helping customers architect reliable, scalable, and ridiculously fast systems using RavenDB.You need to understand databases (SQL, NoSQL, and the cloud), and be ready to learn RavenDB&amp;#39;s powerful features inside and out.If you have a background in development (C#, Java, Python&amp;mdash;it all helps!) and enjoy thinking about things like indexing strategies, data modeling, and performance tuning, you&amp;rsquo;ll love this.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;People Person:&lt;/strong&gt;&amp;nbsp;You need to be able to walk into a room (virtual or physical), quickly identify a customer&amp;#39;s pain points, and articulate a clear, compelling vision for how RavenDB solves them.This role requires excellent communication skills&amp;mdash;you&amp;rsquo;ll be giving engaging demos, leading technical presentations, and collaborating directly with high-level technical teams.If you can discuss a multi-region deployment strategy one minute and explain the ROI to a business executive the next, you&amp;rsquo;ve got the commercial savviness we&amp;rsquo;re looking for.&lt;/p&gt;&lt;p&gt;You should have 3+ years of experience in a pre-sales or solution architecture role.&amp;nbsp;A strong general database background is required, experience with NoSQL databases is a big plus.&lt;/p&gt;&lt;p&gt;Please ping us either via commenting here or submit your details to &lt;a href="mailto:jobs@ravendb.net"&gt;jobs@ravendb.net&lt;/a&gt;&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203683-c/hiring-sales-engineer-in-europe?Key=80d2204e-d285-4212-97bc-14a208b50f41</link><guid>http://ayende.com/203683-c/hiring-sales-engineer-in-europe?Key=80d2204e-d285-4212-97bc-14a208b50f41</guid><pubDate>Tue, 13 Jan 2026 12:00:00 GMT</pubDate></item><item><title>Swiping left on writing authentication code yourself</title><description>&lt;p&gt;You may have heard about &lt;a href="https://www.resecurity.com/blog/article/mongobleed-cve-2025-14847-mongodb-memory-leak-flaw"&gt;a recent security vulnerability in MongoDB&lt;/a&gt;&amp;nbsp;(MongoBleed). The gist is that you can (as an unauthenticated user) remotely read the contents of MongoDB&amp;rsquo;s memory (including things like secrets, document data, and PII). You can read the details about the actual technical issue in the link above.&lt;/p&gt;&lt;p&gt;The root cause of the problem is that the authentication process for MongoDB uses MongoDB&amp;rsquo;s own code. That sounds like a very strange statement, no? Consider the layer at which authentication happens. MongoDB handles authentication at the application level.&lt;/p&gt;&lt;p&gt;Let me skip ahead a bit to talk about how RavenDB handles the problem of authentication. We &lt;a href="https://ayende.com/blog/178977/ravendb-4-0-securing-the-keys-to-the-kingdom"&gt;thought long and hard about that problem&lt;/a&gt;&amp;nbsp;when we redesigned RavenDB for the 4.0 release. One of the key design decisions we made was to &lt;em&gt;not&lt;/em&gt;&amp;nbsp;handle authentication ourselves.&lt;/p&gt;&lt;p&gt;Authentication in RavenDB is based on X.509 certificates. That is usually the highest level of security you&amp;rsquo;re asked for by enterprises anyway, so RavenDB&amp;rsquo;s minimum security level is already at the high end. That decision, however, had a lot of other implications. &lt;/p&gt;&lt;p&gt;RavenDB doesn&amp;rsquo;t have any code to actually authenticate a user. Instead, authentication happens at the infrastructure layer, before any application-level code runs. That means that at a very fundamental level, we don&amp;rsquo;t &lt;em&gt;deal&lt;/em&gt;&amp;nbsp;with unauthenticated input. That is rejected &lt;em&gt;very&lt;/em&gt;&amp;nbsp;early in the process.&lt;/p&gt;&lt;p&gt;It isn&amp;rsquo;t a theoretical issue, by the way. A &lt;a href="https://www.ravendb.net/articles/on-cve-2025-55315-security-assessment"&gt;recent CVE was released&lt;/a&gt;&amp;nbsp;for .NET-based applications (of which RavenDB is one) that could lead to exactly this issue, an authentication bypass problem.RavenDB is &lt;em&gt;not&lt;/em&gt;&amp;nbsp;vulnerable as a result of this issue because the authentication mechanism it relies on is much lower in the stack.&lt;/p&gt;&lt;p&gt;By the same token, the code that actually performs the authentication for RavenDB is the same code that validates that your connection to your bank is secure from hackers. On Linux - OpenSSL, on Windows - SChannel. These are already &lt;em&gt;very&lt;/em&gt;&amp;nbsp;carefully scrutinized and security-critical infrastructure for pretty much everyone. &lt;/p&gt;&lt;p&gt;This design decision also leads to an interesting division inside RavenDB. There is a very strict separation between authentication-related code (provided by the platform) and RavenDB&amp;rsquo;s. &lt;/p&gt;&lt;p&gt;The problem for MongoDB is that they reused the same code for reading BSON documents from the network as part of their authentication mechanism. &lt;/p&gt;&lt;p&gt;That means that &lt;em&gt;any&lt;/em&gt;&amp;nbsp;aspect of BSON in MongoDB needs to be analyzed with an eye toward unauthenticated user input, as this CVE shows. &lt;/p&gt;&lt;p&gt;An attempt to add compression support to reduce network traffic resulted in size confusion, which then led to this problem. To be clear, that is a very reasonable set of steps that happened. For RavenDB, something similar is plausible, but not for &lt;em&gt;unauthorized users&lt;/em&gt;.&lt;/p&gt;&lt;h2&gt;What about Heartbleed?&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/h2&gt;&lt;p&gt;The name &lt;a href="https://www.resecurity.com/blog/article/mongobleed-cve-2025-14847-mongodb-memory-leak-flaw"&gt;Mongobleed&lt;/a&gt;&amp;nbsp;is an intentional reference to &lt;a href="https://en.wikipedia.org/wiki/Heartbleed"&gt;a very similar bug in OpenSSL&lt;/a&gt;&amp;nbsp;from over a decade ago, with similar disastrous consequences. Wouldn&amp;rsquo;t RavenDB then be vulnerable in the same manner as MongoDB?&lt;/p&gt;&lt;p&gt;That is where the choice to use the platform infrastructure comes to our aid. Yes, in such a scenario, RavenDB would be vulnerable. But so would pretty much everything else. For example, MongoDB itself, even though it isn&amp;rsquo;t using OpenSSL for authentication, would also be vulnerable to such a bug in OpenSSL.&lt;/p&gt;&lt;p&gt;The good thing about OpenSSL&amp;rsquo;s Heartbleed bug is that it shined a huge spotlight on such bugs, and it means that a &lt;em&gt;lot&lt;/em&gt;&amp;nbsp;of time, money, and effort has been dedicated to rooting out similar issues, to the point where trust in OpenSSL has been restored. &lt;/p&gt;&lt;h2&gt;Summary&lt;/h2&gt;&lt;p&gt;One of the key decisions that we made when we built RavenDB was to look at how we could use the underlying (battle-tested) infrastructure to do things for us. &lt;/p&gt;&lt;p&gt;For security purposes, that means we have reduced the risk of vulnerabilities. A bug in RavenDB code isn&amp;rsquo;t a security vulnerability, you have to target the (much more closely scrutinized) infrastructure to actually get to a vulnerable state. That is part of our Zero Trust policy. &lt;/p&gt;&lt;p&gt;RavenDB has a far simpler security footprint, we use the enterprise-level TLS &amp;amp; X.509 for authentication instead of implementing six different protocols (and carrying the liability of each). This both simplifies the process of setting up RavenDB securely and reduces the effort required to achieve proper security compliance.&lt;/p&gt;&lt;p&gt;You cannot underestimate the power of checking the &amp;ldquo;X.509 client authentication&amp;rdquo; box and dropping whole &lt;em&gt;sections&lt;/em&gt;&amp;nbsp;of the security audit when deploying a new system. &lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203651-b/swiping-left-on-writing-authentication-code-yourself?Key=80bc09c8-a4ec-4a64-9479-58e7f514ee6e</link><guid>http://ayende.com/203651-b/swiping-left-on-writing-authentication-code-yourself?Key=80bc09c8-a4ec-4a64-9479-58e7f514ee6e</guid><pubDate>Wed, 07 Jan 2026 12:00:00 GMT</pubDate></item><item><title>PropertySphere bot: understanding images</title><description>&lt;p&gt;In &lt;a href="https://ayende.com/blog/203621-B/propertyspheres-intelligent-telegram-bot?key=1802d1b262ad40169d1fcac2ebf1acff"&gt;the previous post&lt;/a&gt;, I talked about the &lt;a href="https://github.com/ayende/samples.properties"&gt;PropertySphere&lt;/a&gt;&amp;nbsp;Telegram bot (you can also &lt;a href="https://www.youtube.com/watch?v=XOdXDNIGzxE"&gt;watch the full video here&lt;/a&gt;). In this post, I want to show how we can make it even smarter. Take a look at the following chat screenshot:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/FoTncQtYsh8DLJQ3Aj7AfA.png"/&gt;&lt;/p&gt;&lt;p&gt;What is actually going on here? This small interaction showcases a &lt;em&gt;numbe&lt;/em&gt;r of RavenDB features, all at once. Let&amp;rsquo;s first focus on how Telegram hands us images. This is done using Photoor &lt;code&gt;Document &lt;/code&gt;messages (depending on exactly how you send the message to Telegram).&lt;/p&gt;&lt;p&gt;The following code shows how we receive and store a photo from Telegram:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token comment"&gt;// Download the largest version of the photo from Telegram:&lt;/span&gt;
&lt;span class="token keyword"&gt;var&lt;/span&gt; ms &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;MemoryStream&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;var&lt;/span&gt; fileId &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;message&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Photo.MaxBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;ps &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;ps&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;FileSize&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;FileId&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;var&lt;/span&gt; file &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;botClient&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;GetInfoAndDownloadFile&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;fileId&lt;span class="token punctuation"&gt;,&lt;/span&gt; ms&lt;span class="token punctuation"&gt;,&lt;/span&gt; cancellationToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token comment"&gt;// Create a Photo document to store metadata:&lt;/span&gt;
&lt;span class="token keyword"&gt;var&lt;/span&gt; photo &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;Photo&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token class-name"&gt;ConversationId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;GetConversationId&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;chatId&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;Id&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"photos/"&lt;/span&gt;&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token class-name"&gt;Guid.NewGuid&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToString&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"N"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;RenterId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;renter&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Id&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;Caption&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;message&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Caption&lt;/span&gt; &lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;message&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Text&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token comment"&gt;// Store the image as an attachment on the document:&lt;/span&gt;
&lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;session&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;StoreAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;photo&lt;span class="token punctuation"&gt;,&lt;/span&gt; cancellationToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;ms&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Position&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;session&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Advanced.Attachments.Store&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;photo&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"image.jpg"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; ms&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;session&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cancellationToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token comment"&gt;// Notify the user that we're processing the image:&lt;/span&gt;
&lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;botClient&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;SendMessage&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
chatId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
       &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Looking at the photo you sent..., may take me a moment..."&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
       cancellationToken
&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;A &lt;code&gt;Photo&lt;/code&gt;&amp;nbsp;message in Telegram may contain multiple versions of the image in various resolutions. Here I&amp;rsquo;m simply selecting the best one by file size, downloading the image from Telegram&amp;rsquo;s servers to a memory stream, then I create a &lt;code&gt;Photo&lt;/code&gt;&amp;nbsp;document and add the image stream to it as an attachment.&lt;/p&gt;&lt;p&gt;We also tell the client to wait while we process the image, but there is no further code that &lt;em&gt;does&lt;/em&gt;&amp;nbsp;anything with it. &lt;/p&gt;&lt;h2&gt;Gen AI &amp;amp; Attachment processing&lt;/h2&gt;&lt;p&gt;We use a Gen AI task to actually process the image, handling it in the background since it may take a while and we want to keep the chat with the user open. That said, if you look at the actual screenshots, the entire conversation took under a minute.&lt;/p&gt;&lt;p&gt;Here is the actual Gen AI task definition for processing these photos:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token keyword"&gt;var&lt;/span&gt; genAiTask &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;GenAiConfiguration&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token class-name"&gt;Name&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Image Description Generator"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;Identifier&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;TaskIdentifier&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;Collection&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Photos"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;Prompt&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
        You are an AI Assistant looking at photos from renters in 
        rental property management, usually about some issue they have. 
        Your task is to generate a concise and accurate description of what 
        is depicted in the photo provided, so maintenance can help them.
        """&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;


    &lt;span class="token comment"&gt;// Expected structure of the model's response:&lt;/span&gt;
    &lt;span class="token class-name"&gt;SampleObject&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
        {
            "Description": "Description of the image"
        }
        """&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;


    &lt;span class="token comment"&gt;// Apply the generated description to the document:&lt;/span&gt;
    &lt;span class="token class-name"&gt;UpdateScript&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"this.Description = &lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;output&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt;.Description;"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;


    &lt;span class="token comment"&gt;// Pass the caption and image to the model for processing:&lt;/span&gt;
    &lt;span class="token class-name"&gt;GenAiTransformation&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;GenAiTransformation&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Script&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
            ai.genContext({
                Caption: this.Caption
            }).withJpeg(loadAttachment("image.jpg"));
            """&lt;/span&gt;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;ConnectionStringName&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Property Management AI Model"&lt;/span&gt;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;What we are doing here is asking RavenDB to send the caption and image contents from each document in the Photos collection to the AI model, along with the given prompt.&amp;nbsp;Then we ask it to explain in detail what is in the picture. &lt;/p&gt;&lt;p&gt;Here is an example of the results of this task after it completed. For reference, here is the full description of the image from the model:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;A leaking metal pipe under a sink is dripping water into a bucket. There is water and stains on the wooden surface beneath the pipe, indicating ongoing leakage and potential water damage.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src="/blog/Images/ktSm-_2Tpkg8fVCY2YtJUg.png"/&gt;&lt;/p&gt;&lt;h3&gt;What model is required for this?&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;I&amp;rsquo;m using the &lt;code&gt;gpt-4.1-mini&lt;/code&gt;&amp;nbsp;model here; there is no need for anything beyond that. It is a multimodal model capable of handling both text and images, so it works great for our needs.&lt;/p&gt;&lt;p&gt;You can read more about &lt;a href="https://ravendb.net/articles/unlock-ravendb-genai-potential-with-attachments"&gt;processing attachments with RavenDB&amp;rsquo;s Gen AI here&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;We still need to close the loop, of course. The Gen AI task that processes the images is actually running in the background. How do we get the output of that from the database and into the chat?&lt;/p&gt;&lt;p&gt;To process that, we create a RavenDB Subscription to the Photos collection, which looks like this:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-json'&gt;&lt;code class='line-numbers language-json'&gt;store.Subscriptions.Create(new SubscriptionCreationOptions
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    Name = SubscriptionName&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    Query = &lt;span class="token string"&gt;""&lt;/span&gt;"
        from &lt;span class="token string"&gt;"Photos"&lt;/span&gt; 
        where Description != &lt;span class="token null keyword"&gt;null&lt;/span&gt;
        &lt;span class="token string"&gt;""&lt;/span&gt;"
&lt;span class="token punctuation"&gt;}&lt;/span&gt;);&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;This subscription is called by RavenDB whenever a document in the Photos collection is created or updated with the &lt;em&gt;Description&lt;/em&gt;&amp;nbsp;having a value. In other words, this will be triggered when the GenAI task updates the photo after it runs. &lt;/p&gt;&lt;p&gt;The actual handling of the subscription is done using the following code:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-javascript'&gt;&lt;code class='line-numbers language-javascript'&gt;_documentStore&lt;span class="token punctuation"&gt;.&lt;/span&gt;Subscriptions&lt;span class="token punctuation"&gt;.&lt;/span&gt;GetSubscriptionWorker&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;Photo&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"After Photos Analysis"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Run&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;async&lt;/span&gt; &lt;span class="token parameter"&gt;batch&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        using &lt;span class="token keyword"&gt;var&lt;/span&gt; session &lt;span class="token operator"&gt;=&lt;/span&gt; batch&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;OpenAsyncSession&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        &lt;span class="token function"&gt;foreach&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;&lt;span class="token keyword"&gt;var&lt;/span&gt; item &lt;span class="token keyword"&gt;in&lt;/span&gt; batch&lt;span class="token punctuation"&gt;.&lt;/span&gt;Items&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token punctuation"&gt;{&lt;/span&gt;
            &lt;span class="token keyword"&gt;var&lt;/span&gt; renter &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;await&lt;/span&gt; session&lt;span class="token punctuation"&gt;.&lt;/span&gt;LoadAsync&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;Renter&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
item&lt;span class="token punctuation"&gt;.&lt;/span&gt;Result&lt;span class="token punctuation"&gt;.&lt;/span&gt;RenterId&lt;span class="token operator"&gt;!&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
            &lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token function"&gt;ProcessMessageAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;_botClient&lt;span class="token punctuation"&gt;,&lt;/span&gt; renter&lt;span class="token punctuation"&gt;.&lt;/span&gt;TelegramChatId&lt;span class="token operator"&gt;!&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                $&lt;span class="token string"&gt;"Uploaded an image with caption: {item.Result.Caption}\r\n"&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt;
                $&lt;span class="token string"&gt;"Image description: {item.Result.Description}."&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
                cancellationToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        &lt;span class="token punctuation"&gt;}&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;In other words, we run over the items in the subscription batch, and for each one, we emit a &amp;ldquo;fake&amp;rdquo; message as if it were sent by the user to the Telegram bot. Note that we aren&amp;rsquo;t invoking the RavenDB conversation directly, but instead reusing the Telegram message handling logic. This way, the reply from the model will go directly back into the users&amp;#39; chat.&lt;/p&gt;&lt;p&gt;You can see how that works in the screenshot above. It looks like the model looked at the image, and then it acted. In this case, it acted by creating a service request. We previously looked at charging a credit card, and now let&amp;rsquo;s see how we handle creating a service request by the model.&lt;/p&gt;&lt;p&gt;The AI Agent is defined with a &lt;code&gt;CreateServiceRequest&lt;/code&gt;&amp;nbsp;action, which looks like this:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-bash'&gt;&lt;code class='line-numbers language-bash'&gt;Actions &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;
    new AiAgentToolAction
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        Name &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string"&gt;"CreateServiceRequest"&lt;/span&gt;,
        Description &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string"&gt;"Create a new service request for the renter's unit"&lt;/span&gt;,
        ParametersSampleObject &lt;span class="token operator"&gt;=&lt;/span&gt; JsonConvert.SerializeObject&lt;span class="token punctuation"&gt;(&lt;/span&gt;
            new CreateServiceRequestArgs
            &lt;span class="token punctuation"&gt;{&lt;/span&gt;
                    Type &lt;span class="token operator"&gt;=&lt;/span&gt;         &lt;span class="token string"&gt;""&lt;/span&gt;"
Maintenance &lt;span class="token operator"&gt;|&lt;/span&gt; Repair &lt;span class="token operator"&gt;|&lt;/span&gt; Plumbing &lt;span class="token operator"&gt;|&lt;/span&gt; Electrical &lt;span class="token operator"&gt;|&lt;/span&gt; 
HVAC &lt;span class="token operator"&gt;|&lt;/span&gt; Appliance &lt;span class="token operator"&gt;|&lt;/span&gt; Community &lt;span class="token operator"&gt;|&lt;/span&gt; Neighbors &lt;span class="token operator"&gt;|&lt;/span&gt; Other
&lt;span class="token string"&gt;""&lt;/span&gt;",
            Description &lt;span class="token operator"&gt;=&lt;/span&gt;         &lt;span class="token string"&gt;""&lt;/span&gt;"
Detailed description of the issue with all 
relevant context
&lt;span class="token string"&gt;""&lt;/span&gt;"
                &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;,
&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;As a reminder, this is the description of the action that the model can invoke. Its actual handling is done when we create the conversation, like so:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;conversation&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Handle&lt;/span&gt;&lt;span class="token generics"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token class-name"&gt;PropertyAgent.CreateServiceRequestArgs&lt;/span&gt;&lt;span class="token punctuation"&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"CreateServiceRequest"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
&lt;span class="token keyword"&gt;async&lt;/span&gt; args &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    using &lt;span class="token keyword"&gt;var&lt;/span&gt; session &lt;span class="token operator"&gt;=&lt;/span&gt; _documentStore&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;OpenAsyncSession&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;var&lt;/span&gt; unitId &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;renterUnits&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;FirstOrDefault&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;var&lt;/span&gt; propertyId &lt;span class="token operator"&gt;=&lt;/span&gt; unitId&lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Substring&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;unitId&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;LastIndexOf&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;'/'&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;var&lt;/span&gt; serviceRequest &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;ServiceRequest&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;RenterId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;renter&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Id&lt;/span&gt;&lt;span class="token operator"&gt;!&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;UnitId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; unitId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Type&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;args&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Type&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Description&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;args&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Description&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Status&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Open"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;OpenedAt&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;DateTime.UtcNow&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;PropertyId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; propertyId
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;session&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;StoreAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;serviceRequest&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;session&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;return&lt;/span&gt; $&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Service request created ID `{serviceRequest.Id}` for your unit."&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;In this case, there isn&amp;rsquo;t really much &lt;em&gt;to&lt;/em&gt;&amp;nbsp;do here, but hopefully this conveys the kind of code this allows you to write.&lt;/p&gt;&lt;h1&gt;Summary&lt;/h1&gt;&lt;p&gt;The &lt;a href="https://github.com/ayende/samples.properties"&gt;PropertySphere&lt;/a&gt;&amp;nbsp;sample application and its Telegram bot are interesting, mostly because of everything that &lt;em&gt;isn&amp;rsquo;t&lt;/em&gt;&amp;nbsp;here. We have a bot that has a pretty complex set of behaviors, but there isn&amp;rsquo;t a lot of complexity for &lt;em&gt;us&lt;/em&gt;&amp;nbsp;to deal with.&lt;/p&gt;&lt;p&gt;This behavior is emergent from the capabilities we entrusted to the model, and the kind of capabilities we give it. At the same time, I&amp;rsquo;m not &lt;em&gt;trusting&lt;/em&gt;&amp;nbsp;the model, but verifying that what it does is always within the scope of the user&amp;rsquo;s capabilities. &lt;/p&gt;&lt;p&gt;Extending what we have here to allow additional capabilities is easy. Consider adding the ability to get invoices directly from the Telegram interface, a great exercise in extending what you can do with the sample app.&lt;/p&gt;&lt;p&gt;There is also &lt;a href="https://www.youtube.com/watch?v=XOdXDNIGzxE"&gt;the full video &lt;/a&gt;where I walk you through all aspects of the sample application, and as always, we&amp;rsquo;d love to talk to you on &lt;a href="http://discord.gg/ravendb"&gt;Discord &lt;/a&gt;or in our &lt;a href="http://github.com/ravendb/ravendb/discussions/"&gt;GitHub discussions&lt;/a&gt;.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203622-b/propertysphere-bot-understanding-images?Key=889433c1-a0cb-4633-b096-390e184c9159</link><guid>http://ayende.com/203622-b/propertysphere-bot-understanding-images?Key=889433c1-a0cb-4633-b096-390e184c9159</guid><pubDate>Fri, 26 Dec 2025 12:00:00 GMT</pubDate></item><item><title>PropertySphere's intelligent Telegram bot</title><description>&lt;p&gt;In &lt;a href="https://ayende.com/blog/203620-B/propertysphere-sample-application?key=2ab9814a35aa48ee86f3f3f9b1accd57"&gt;the previous post&lt;/a&gt;, I introduced the &lt;a href="https://github.com/ayende/samples.properties"&gt;PropertySphere&lt;/a&gt;&amp;nbsp;sample application (you can also &lt;a href="https://www.youtube.com/watch?v=XOdXDNIGzxE"&gt;watch the video introducing it here&lt;/a&gt;). In this post, I want to go over how we build a Telegram bot for this application, so Renters can communicate with the application, check their status, raise issues, and even pay their bills.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m using Telegram here because the process of creating a new bot is trivial, the API is really fun to work with, and it takes very little effort.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Compare that to something like WhatsApp, where just the &lt;em&gt;process&lt;/em&gt;&amp;nbsp;for creating a bot is a PITA. &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Without further ado, let&amp;rsquo;s look at what the Telegram bot looks like:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/ykeP1CWNETKtFlzQteq9ow.png"/&gt;&lt;/p&gt;&lt;p&gt;There are a bunch of interesting things that you can see in the screenshot. We communicate with the bot on the other end using natural text. There aren&amp;#39;t a lot of screens / options that you have to go through, it is just natural mannerism.&lt;/p&gt;&lt;p&gt;The process is pretty streamlined from the perspective of the user. What does that look like from the implementation perspective? A lot of the time, that kind of interface involves&amp;hellip; &lt;em&gt;big &lt;/em&gt;amount of complexity in the backend.&lt;/p&gt;&lt;p&gt;Here is what I usually think when I consider those demos:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/SDGqd664wltq5nf1C8FRqQ.png"/&gt;&lt;/p&gt;&lt;p&gt;In our example, we can implement all of this in about 250 lines of code. The magic behind it is the fact that we can rely on RavenDB&amp;rsquo;s AI Agents feature to do most of the heavy lifting for us.&lt;/p&gt;&lt;p&gt;Inside RavenDB, this is defined as follows:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/ivSxYW8aNkfTU0f4jxAjPA.png"/&gt;&lt;/p&gt;&lt;p&gt;For this post, however, we&amp;rsquo;ll look at how we actually built this AI-powered Telegram bot. The full code is &lt;a href="https://github.com/ayende/samples.properties/blob/master/Services/PropertyAgent.cs"&gt;here&lt;/a&gt;&amp;nbsp;if you want to browse through it. &lt;/p&gt;&lt;h3&gt;What model is used here?&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;It&amp;rsquo;s worth mentioning that I&amp;rsquo;m not using anything fancy, the agent is using baseline &lt;code&gt;gpt-4.1-mini&lt;/code&gt;&amp;nbsp;for the demo. There is no need for training or customization, the way we create the agent already takes care of that.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Here is the overall agent definition: &lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;store&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;AI.CreateAgent&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentConfiguration&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Name&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Property Assistant"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Identifier&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"property-agent"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;ConnectionStringName&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Property Management AI Model"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;SystemPrompt&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
            You are a property management assistant for renters.
            ... redacted ...
            Do NOT discuss non-property topics. 
            """&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Parameters&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;
            &lt;span class="token comment"&gt;// Visible to the model:&lt;/span&gt;
            &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentParameter&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"currentDate"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Current date in yyyy-MM-dd format"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token comment"&gt;// Agent scope only, not visible to the model directly&lt;/span&gt;
            &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentParameter&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"renterId"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Renter ID; answer only for this renter"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; sendToModel&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token boolean"&gt;false&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentParameter&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"renterUnits"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"List of unit IDs occupied by the renter"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; sendToModel&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token boolean"&gt;false&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;SampleObject&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;JsonConvert.SerializeObject&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;Reply&lt;/span&gt;
        &lt;span class="token punctuation"&gt;{&lt;/span&gt;
            &lt;span class="token class-name"&gt;Answer&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Detailed answer to query (markdown syntax)"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token class-name"&gt;Followups&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Likely follow-ups"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token comment"&gt;// redacted&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;The code above will create an agent with the given prompt. It turns out that a lot of work actually goes into that prompt to explain to the AI model exactly what its role is, what it is meant to do, etc. &lt;/p&gt;&lt;p&gt;I reproduced the entire prompt below so you can read it more easily, but take into account that you&amp;rsquo;ll likely tweak it a &lt;em&gt;lot&lt;/em&gt;, and that it is usually much longer than what we have here (although what we have below is quite functional, as you can see from the screenshots).&lt;/p&gt;&lt;h3&gt;The agent&amp;rsquo;s prompt&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;You are a property management assistant for renters.&lt;/p&gt;&lt;p&gt;Provide information about rent, utilities, debts, service requests, and property details.&lt;/p&gt;&lt;p&gt;Be professional, helpful, and responsive to renters&amp;rsquo; needs.&lt;/p&gt;&lt;p&gt;You can answer in Markdown format. Make sure to use ticks (`) whenever you discuss identifiers.&lt;/p&gt;&lt;p&gt;Do not suggest actions that are not explicitly allowed by the tools available to you.&lt;/p&gt;&lt;p&gt;Do NOT discuss non-property topics. Answer only for the current renter.&lt;/p&gt;&lt;p&gt;When discussing amounts, always format them as currency with 2 decimal places.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The way RavenDB deals with AI Agents, we define two very important aspects of them. First, we have the parameters, which define the scope of the system. In this case, you can see that we pass the &lt;code&gt;currentDate&lt;/code&gt;, as well as provide the &lt;code&gt;renterId &lt;/code&gt;and &lt;code&gt;renterUnits &lt;/code&gt;that this agent is going to deal with. &lt;/p&gt;&lt;p&gt;We expose the current date to the model, but not the renter ID or the units that define the scope (we&amp;rsquo;ll touch on that in a bit). The model needs the current date so it will understand when it is running and have context for things like &amp;ldquo;last month&amp;rdquo;. But we don&amp;rsquo;t need to give it the IDs, they have no meaning and are instead used to define the scope of a particular conversation with the model. &lt;/p&gt;&lt;p&gt;The sample object we use defines the structure of the reply that we require the model to give us. In this case, we want to get a textual message from the model in Markdown format, as well as a separate array of likely follow-ups that we can provide to the user.&lt;/p&gt;&lt;p&gt;In order to do its job, the agent needs to be able to access the system. RavenDB handles that by letting you define queries that the model can ask the agent to execute when it needs more information. Here are some of them:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token class-name"&gt;Queries&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;
    &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentToolQuery&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Name&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"GetRenterInfo"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Description&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Retrieve renter profile details"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Query&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"from Renters where id() = &lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;renterId&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;ParametersSampleObject&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"{}"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Options&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentToolQueryOptions&lt;/span&gt;
        &lt;span class="token punctuation"&gt;{&lt;/span&gt;
            &lt;span class="token class-name"&gt;AllowModelQueries&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token boolean"&gt;false&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token class-name"&gt;AddToInitialContext&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token boolean"&gt;true&lt;/span&gt;
        &lt;span class="token punctuation"&gt;}&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
     &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentToolQuery&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Name&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"GetOutstandingDebts"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Description&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Retrieve renter's outstanding debts (unpaid balances)"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Query&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
            from index 'DebtItems/Outstanding'
            where RenterIds in (&lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;renterId&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt;) and AmountOutstanding &gt; 0
            order by DueDate asc
            limit 10
            """&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;ParametersSampleObject&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"{}"&lt;/span&gt;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentToolQuery&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Name&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"GetUtilityUsage"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Description&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
Retrieve utility usage for renter's unit within a date 
range (for calculating bills)
"""&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Query&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
            from 'Units'
            where id() in (&lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;renterUnits&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt;)
            select 
                timeseries(from 'Power' 
between &lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;startDate&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt; and &lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;endDate&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt; 
group by 1d 
select sum()),
                timeseries(from 'Water' 
between &lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;startDate&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt; and &lt;/span&gt;&lt;span class="token interpolation"&gt;&lt;span class="token punctuation"&gt;$&lt;/span&gt;&lt;span class="token expression"&gt;endDate&lt;/span&gt;&lt;/span&gt;&lt;span class="token string"&gt; 
group by 1d 
select sum())
            """&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;ParametersSampleObject&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; 
&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
{
"startDate": "yyyy-MM-dd", 
"endDate": "yyyy-MM-dd"
}
"""&lt;/span&gt;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;The first query in the previous snippet, &lt;code&gt;GetRenterInfo, &lt;/code&gt;is interesting. You can see that it is marked as: &lt;code&gt;AllowModelQueries = false, AddToInitialContext = true. W&lt;/code&gt;hat does that mean?&lt;/p&gt;&lt;p&gt;It means that as part of creating a new conversation with the model, we are going to run the query to get all the renter&amp;rsquo;s details and add that to the initial context we send to the model. That allows us to provide the model with the information it will likely need upfront. &lt;/p&gt;&lt;p&gt;Note that we use the &lt;code&gt;$renterId&lt;/code&gt;&amp;nbsp;and &lt;code&gt;$renterUnits&lt;/code&gt;&amp;nbsp;parameters in the queries. While they aren&amp;rsquo;t exposed directly to the model, they affect what information the model can see. This is a good thing, since it means we place guardrails very early on. The model simply cannot see any information that is out of scope for it.&lt;/p&gt;&lt;h2&gt;The model can ask for additional information when it needs to&amp;hellip;&lt;/h2&gt;&lt;p&gt;An important observation about the design of AI agents with RavenDB: note that we provided the model with a bunch of potential queries that it can run. &lt;code&gt;GetRenterInfo &lt;/code&gt;is run at the beginning, since it gives us the initial context, but the rest are left for the judgment of the model.&lt;/p&gt;&lt;p&gt;The model can decide what queries it needs to run in order to answer the user&amp;rsquo;s questions, and it does so of its own accord. This decision means that once you have defined the set of queries and operations that the model can run, you are mostly done. The AI is smart enough to figure out what to do and then act according to your data.&lt;/p&gt;&lt;p&gt;Here is an example of what this looks like from the backend:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/IKdfEV3eVXzR_jl_InBldw.png"/&gt;&lt;/p&gt;&lt;p&gt;Here you can see that the user asked about their utilities, the model then ran the appropriate query and formulated an answer for the user.&lt;/p&gt;&lt;h3&gt;The follow-ups UX pattern&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;You might have noticed that we asked the model for follow-up questions that the user may want to ask. This is a hidden way to guide the &lt;em&gt;user&lt;/em&gt;&amp;nbsp;toward the set of operations that the model supports. &lt;/p&gt;&lt;p&gt;The model will generate the follow-ups based on its own capabilities (queries and actions that it knows it can run), so this is a pretty simple way to &amp;ldquo;tell&amp;rdquo; that to the user without being obnoxious about it.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Let&amp;rsquo;s look at how things work when we actually use this to build the bot, then come back to the rest of the agent&amp;rsquo;s definition. &lt;/p&gt;&lt;h2&gt;Plugging the model into Telegram&lt;/h2&gt;&lt;p&gt;We looked at the agent&amp;rsquo;s definition so far - let&amp;rsquo;s see how we actually use that. The Telegram&amp;rsquo;s API is really nice, basically boiling down to:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;_botClient &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;TelegramBotClient&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;botSecretToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
_botClient&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;StartReceiving&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token class-name"&gt;HandleUpdateAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;HandleErrorAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;ReceiverOptions&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;AllowedUpdates&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;
            &lt;span class="token class-name"&gt;UpdateType.Message&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
            &lt;span class="token class-name"&gt;UpdateType.CallbackQuery&lt;/span&gt; 
            &lt;span class="token punctuation"&gt;]&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    _cts&lt;span class="token punctuation"&gt;.&lt;/span&gt;Token
&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


&lt;span class="token keyword"&gt;async&lt;/span&gt; &lt;span class="token class-name"&gt;Task&lt;/span&gt; &lt;span class="token class-name"&gt;HandleUpdateAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token class-name"&gt;ITelegramBotClient&lt;/span&gt; botClient&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
&lt;span class="token class-name"&gt;Update&lt;/span&gt; update&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token class-name"&gt;CancellationToken&lt;/span&gt; cancellationToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;switch&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;update&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token keyword"&gt;case&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; &lt;span class="token class-name"&gt;Message&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; &lt;span class="token class-name"&gt;Text&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; &lt;span class="token punctuation"&gt;}&lt;/span&gt; messageText &lt;span class="token punctuation"&gt;}&lt;/span&gt; message &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
            &lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;ProcessMessageAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;botClient&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;message&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Chat.Id.ToString&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
messageText&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
cancellationToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
            &lt;span class="token keyword"&gt;break&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;And then the Telegram API will call the &lt;code&gt;HandleUpdateAsync&lt;/code&gt;&amp;nbsp;method when there is a new message to the bot. Note that you may actually get multiple (concurrent messages), maybe from different chats, at the same time.&lt;/p&gt;&lt;p&gt;We&amp;rsquo;ll focus on the process message function, where we start by checking exactly who we are talking to:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-javascript'&gt;&lt;code class='line-numbers language-javascript'&gt;&lt;span class="token keyword"&gt;async&lt;/span&gt; Task &lt;span class="token function"&gt;ProcessMessageAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;ITelegramBotClient botClient&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
string chatId&lt;span class="token punctuation"&gt;,&lt;/span&gt; string messageText&lt;span class="token punctuation"&gt;,&lt;/span&gt; CancellationToken cancellationToken&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    using &lt;span class="token keyword"&gt;var&lt;/span&gt; session &lt;span class="token operator"&gt;=&lt;/span&gt; _documentStore&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;OpenAsyncSession&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;var&lt;/span&gt; renter &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;await&lt;/span&gt; session&lt;span class="token punctuation"&gt;.&lt;/span&gt;Query&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;Renter&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;r&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt; r&lt;span class="token punctuation"&gt;.&lt;/span&gt;TelegramChatId &lt;span class="token operator"&gt;==&lt;/span&gt; chatId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
 cancellationToken&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;renter &lt;span class="token operator"&gt;==&lt;/span&gt; &lt;span class="token keyword"&gt;null&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token keyword"&gt;await&lt;/span&gt; botClient&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;SendMessage&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;chatId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token string"&gt;"Sorry, your Telegram account is not linked to a renter profile."&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token literal-property property"&gt;cancellationToken&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; cancellationToken
        &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        &lt;span class="token keyword"&gt;return&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;
    &lt;span class="token keyword"&gt;var&lt;/span&gt; conversationId &lt;span class="token operator"&gt;=&lt;/span&gt; $&lt;span class="token string"&gt;"chats/{chatId}/{DateTime.Today:yyyy-MM-dd}"&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token comment"&gt;// more code in the next snippet&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;Telegram uses the term chat ID in their API, but it is what I would call the renter&amp;rsquo;s ID. When we register renters, we also record their Telegram chat ID, which means that when we get a message from a user, we can check whether they are a valid renter in our system. If not, we fail early and are done.&lt;/p&gt;&lt;p&gt;If they are, this is where things start to get interesting. Look at the conversation ID that we generated in the last line. RavenDB uses the notion of a conversation with the agent to hold state. The conversation we create here means that the bot will use the same conversation with the user for the same day.&lt;/p&gt;&lt;p&gt;Another way to do that would be to keep the same conversation ID open for the same user. Since RavenDB will automatically handle summarizing and trimming the conversation, either option is fine and mostly depends on your scenario.&lt;/p&gt;&lt;p&gt;The next stage is to create the actual conversation. To do that, we need to provide the model with the right context it is looking for:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-bash'&gt;&lt;code class='line-numbers language-bash'&gt;var renterUnits &lt;span class="token operator"&gt;=&lt;/span&gt; await session.Query&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;Lease&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    .Where&lt;span class="token punctuation"&gt;(&lt;/span&gt;l &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; l.RenterIds.Contains&lt;span class="token punctuation"&gt;(&lt;/span&gt;renter.Id&lt;span class="token operator"&gt;!&lt;/span&gt;&lt;span class="token punctuation"&gt;))&lt;/span&gt;
    .Select&lt;span class="token punctuation"&gt;(&lt;/span&gt;l &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; l.UnitId&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    .ToListAsync&lt;span class="token punctuation"&gt;(&lt;/span&gt;cts&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


var conversation &lt;span class="token operator"&gt;=&lt;/span&gt; _documentStore.AI.Conversation&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"property-agent"&lt;/span&gt;,
    conversationId,
    new AiConversationCreationOptions
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        Parameters &lt;span class="token operator"&gt;=&lt;/span&gt; new Dictionary&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;string, object?&lt;span class="token operator"&gt;&gt;&lt;/span&gt;
        &lt;span class="token punctuation"&gt;{&lt;/span&gt;
            &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"renterId"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; renter.Id,
            &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"renterUnits"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; renterUnits,
            &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"currentDate"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; DateTime.Today.ToString&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"yyyy-MM-dd"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token punctuation"&gt;}&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;You can see that we pass the renter ID and the relevant units for the renter to the model. Those form the creation parameters for the conversation and cannot be changed. That is one of the reasons why you may want to have a different conversation per day, to get the updated values if they changed.&lt;/p&gt;&lt;p&gt;With that done, we can send the results back to the model and then to the user, like so:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token keyword"&gt;var&lt;/span&gt; result &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;conversation&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;RunAsync&lt;/span&gt;&lt;span class="token generics"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token class-name"&gt;PropertyAgent.Reply&lt;/span&gt;&lt;span class="token punctuation"&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cts&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


&lt;span class="token keyword"&gt;var&lt;/span&gt; replyMarkup &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;ReplyKeyboardMarkup&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;result&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Answer.Followups
    .Select&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;text &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;KeyboardButton&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;text&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToArray&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;ResizeKeyboard&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token boolean"&gt;true&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;OneTimeKeyboard&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token boolean"&gt;true&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


&lt;span class="token keyword"&gt;await&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;botClient&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;SendMessage&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    chatId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;result&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Answer.Answer&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    replyMarkup&lt;span class="token punctuation"&gt;:&lt;/span&gt; replyMarkup&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    cancellationToken&lt;span class="token punctuation"&gt;:&lt;/span&gt; cts&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;The &lt;code&gt;RunAsync()&lt;/code&gt;&amp;nbsp;method handles the entire interaction with the model, and most of the code is just dealing with the reply markup for Telegram.&lt;/p&gt;&lt;p&gt;If you look closely at the chat screenshot above, you can see that we aren&amp;rsquo;t just asking the model questions, we get the bot to perform actions. For example, paying the rent. Here is what this looks like:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/yZoNCftwkqpV06ap7pNWAg.png"/&gt;&lt;/p&gt;&lt;p&gt;How does this work?&lt;/p&gt;&lt;h2&gt;Paying the rent through the bot&lt;/h2&gt;&lt;p&gt;When we looked at the agent, we saw that we exposed some queries that the agent can run. But that isn&amp;rsquo;t the complete picture, we also give the model the ability to run actions. Here is what this looks like from the agent&amp;rsquo;s definition side:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token class-name"&gt;Actions&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;
    &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;AiAgentToolAction&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Name&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"ChargeCard"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Description&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"""
Record a payment for one or more outstanding debts. The 
renter can pay multiple debt items in a single transaction. 
Can pay using any stored card on file.
"""&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;ParametersSampleObject&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;JsonConvert.SerializeObject&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;ChargeCardArgs&lt;/span&gt;
        &lt;span class="token punctuation"&gt;{&lt;/span&gt;
            &lt;span class="token class-name"&gt;DebtItemIds&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"debtitems/1-A"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"debtitems/2-A"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token class-name"&gt;PaymentMethod&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Card"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
            &lt;span class="token class-name"&gt;Card&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Last 4 digits of the card"&lt;/span&gt;&lt;/span&gt;
        &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;The idea here is that we expose to the model the kinds of actions it can request, and we specify what parameters it should pass to them, etc. What we are &lt;em&gt;not&lt;/em&gt;&amp;nbsp;doing here is giving the model control over actually running any code or modifying any data.&lt;/p&gt;&lt;p&gt;Instead, when the model needs to charge a card, it will have to call &lt;em&gt;your&lt;/em&gt;&amp;nbsp;code and go through validation, business logic, and authorization. Here is what this looks like on the other side. When we create a conversation, we specify handlers for all the actions we need to take, like so:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-javascript'&gt;&lt;code class='line-numbers language-javascript'&gt;conversation&lt;span class="token punctuation"&gt;.&lt;/span&gt;Handle&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;PropertyAgent&lt;span class="token punctuation"&gt;.&lt;/span&gt;ChargeCardArgs&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"ChargeCard"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token keyword"&gt;async&lt;/span&gt; &lt;span class="token parameter"&gt;args&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    using &lt;span class="token keyword"&gt;var&lt;/span&gt; paySession &lt;span class="token operator"&gt;=&lt;/span&gt; _documentStore&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;OpenAsyncSession&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;var&lt;/span&gt; renterWithCard &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;await&lt;/span&gt; paySession&lt;span class="token punctuation"&gt;.&lt;/span&gt;LoadAsync&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;Renter&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;renter&lt;span class="token punctuation"&gt;.&lt;/span&gt;Id&lt;span class="token operator"&gt;!&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cts&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;var&lt;/span&gt; card &lt;span class="token operator"&gt;=&lt;/span&gt; renterWithCard&lt;span class="token operator"&gt;?.&lt;/span&gt;CreditCards
&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;c&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt; c&lt;span class="token punctuation"&gt;.&lt;/span&gt;Last4Digits &lt;span class="token operator"&gt;==&lt;/span&gt; args&lt;span class="token punctuation"&gt;.&lt;/span&gt;Card&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;card &lt;span class="token operator"&gt;==&lt;/span&gt; &lt;span class="token keyword"&gt;null&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token keyword"&gt;throw&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
$&lt;span class="token string"&gt;"Card ending in {args.Card} not found in your profile."&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;


    &lt;span class="token keyword"&gt;var&lt;/span&gt; totalPaid &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token keyword"&gt;await&lt;/span&gt; PaymentService&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;CreatePaymentForDebtsWithCardAsync&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
        paySession&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        renter&lt;span class="token punctuation"&gt;.&lt;/span&gt;Id&lt;span class="token operator"&gt;!&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        args&lt;span class="token punctuation"&gt;.&lt;/span&gt;DebtItemIds&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        card&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        args&lt;span class="token punctuation"&gt;.&lt;/span&gt;PaymentMethod&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        cts&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;return&lt;/span&gt; $&lt;span class="token string"&gt;"Charged {totalPaid:C2} to {card.Type}"&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt;
    $&lt;span class="token string"&gt;" ending in {card.Last4Digits}."&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;Note that we do some basic validation, then we call the &lt;code&gt;CreatePaymentForDebtsWithCardAsync()&lt;/code&gt;method to perform the actual operation. It is also fun that we can just return a message string to give the model an idea about what the result of the action is. &lt;/p&gt;&lt;p&gt;Inside &lt;code&gt;CreatePaymentForDebtsWithCardAsync(),&lt;/code&gt;we also verify that the debts we are asked to pay are associated with the current renter; we may have to apply additional logic, etc. The concept is that we assume the model is &lt;em&gt;not&lt;/em&gt;&amp;nbsp;to be trusted, so we need to carefully validate the input and use our code to verify that everything is fine.&lt;/p&gt;&lt;h1&gt;Summary&lt;/h1&gt;&lt;p&gt;This post has gone on for quite a while, so I think we&amp;rsquo;ll stop here. As a reminder, the &lt;a href="https://github.com/ayende/samples.properties"&gt;PropertySphere&lt;/a&gt;&amp;nbsp;sample application code is available. And if you are one of those who prefer videos to text, you can &lt;a href="https://www.youtube.com/watch?v=XOdXDNIGzxE"&gt;watch the video here&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;In &lt;a href="https://ayende.com/blog/203622-B/propertysphere-bot-understanding-images?key=889433c1a0cb4633b096390e184c9159"&gt;the next post&lt;/a&gt;, I&amp;rsquo;m going to show you how we can make the bot even smarter by adding visual recognition to the mix.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203621-b/propertyspheres-intelligent-telegram-bot?Key=1802d1b2-62ad-4016-9d1f-cac2ebf1acff</link><guid>http://ayende.com/203621-b/propertyspheres-intelligent-telegram-bot?Key=1802d1b2-62ad-4016-9d1f-cac2ebf1acff</guid><pubDate>Mon, 22 Dec 2025 12:00:00 GMT</pubDate></item><item><title>PropertySphere sample application</title><description>&lt;p&gt;This post introduces the &lt;a href="https://github.com/ayende/samples.properties"&gt;PropertySphere&lt;/a&gt;&amp;nbsp;sample application. I&amp;rsquo;m going to talk about some aspects of the sample application in this post, then in the next one, we will introduce AI into the mix.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;You can also &lt;a href="https://www.youtube.com/watch?v=XOdXDNIGzxE"&gt;watch me walk through the entire application in this video&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This is based on a real-world scenario from a customer. One of the nicest things about AI being so easy to use is that I can generate throwaway code for a conversation with a customer that is actually a full-blown application.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href="https://github.com/ayende/samples.properties"&gt;The full code for the sample application is available on GitHub&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Here is the application dashboard, so you can get some idea about what this is all about:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/vkRtc5XjRQjjIvDAGgqZ-Q.png"/&gt;&lt;/p&gt;&lt;p&gt;The idea is that you have Properties (apartment buildings), which have Units (apartments), which you then Lease to Renters. Note the capitalized words in the last sentence, those are the key domain entities that we work with.&lt;/p&gt;&lt;p&gt;Note that this dashboard shows quite a lot of data from many different places in the system. The map defines which properties we are looking at. It&amp;rsquo;s not just a static map, it is interactive. You can zoom in on a region to apply a spatial filter to the data in the dashboard. &lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s take a closer look at what we are doing here. I&amp;rsquo;m primarily a backend guy, so I&amp;rsquo;m ignoring what the front end is doing to focus on the actual behavior of the system.&lt;/p&gt;&lt;p&gt;Here is what a typical endpoint will return for the dashboard:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-javascript'&gt;&lt;code class='line-numbers language-javascript'&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token function"&gt;HttpGet&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"status/{status}"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
&lt;span class="token keyword"&gt;public&lt;/span&gt; IActionResult &lt;span class="token function"&gt;GetByStatus&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;string status&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;FromQuery&lt;span class="token punctuation"&gt;]&lt;/span&gt; string&lt;span class="token operator"&gt;?&lt;/span&gt; boundsWkt&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;var&lt;/span&gt; docQuery &lt;span class="token operator"&gt;=&lt;/span&gt; _session
&lt;span class="token punctuation"&gt;.&lt;/span&gt;Query&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;ServiceRequests_ByStatusAndLocation&lt;span class="token punctuation"&gt;.&lt;/span&gt;Result&lt;span class="token punctuation"&gt;,&lt;/span&gt;
 ServiceRequests_ByStatusAndLocation&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Where&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;x&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt; x&lt;span class="token punctuation"&gt;.&lt;/span&gt;Status &lt;span class="token operator"&gt;==&lt;/span&gt; status&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;OrderByDescending&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;x&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt; x&lt;span class="token punctuation"&gt;.&lt;/span&gt;OpenedAt&lt;span class="token punctuation"&gt;)&lt;/span&gt;
        &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Take&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;10&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;!&lt;/span&gt;string&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;boundsWkt&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        docQuery &lt;span class="token operator"&gt;=&lt;/span&gt; docQuery&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Spatial&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
&lt;span class="token parameter"&gt;x&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt; x&lt;span class="token punctuation"&gt;.&lt;/span&gt;Location&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token parameter"&gt;spatial&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt; spatial&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Within&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;boundsWkt&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;


    &lt;span class="token keyword"&gt;var&lt;/span&gt; results &lt;span class="token operator"&gt;=&lt;/span&gt; docQuery&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Select&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;x&lt;/span&gt; &lt;span class="token operator"&gt;=&gt;&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        x&lt;span class="token punctuation"&gt;.&lt;/span&gt;Id&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        x&lt;span class="token punctuation"&gt;.&lt;/span&gt;Status&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        x&lt;span class="token punctuation"&gt;.&lt;/span&gt;OpenedAt&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        x&lt;span class="token punctuation"&gt;.&lt;/span&gt;UnitId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        x&lt;span class="token punctuation"&gt;.&lt;/span&gt;PropertyId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        x&lt;span class="token punctuation"&gt;.&lt;/span&gt;Type&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        x&lt;span class="token punctuation"&gt;.&lt;/span&gt;Description&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        PropertyName &lt;span class="token operator"&gt;=&lt;/span&gt; RavenQuery&lt;span class="token punctuation"&gt;.&lt;/span&gt;Load&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;Property&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;x&lt;span class="token punctuation"&gt;.&lt;/span&gt;PropertyId&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;Name&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        UnitNumber &lt;span class="token operator"&gt;=&lt;/span&gt; RavenQuery&lt;span class="token punctuation"&gt;.&lt;/span&gt;Load&lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt;Unit&lt;span class="token operator"&gt;&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;x&lt;span class="token punctuation"&gt;.&lt;/span&gt;UnitId&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;UnitNumber
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToList&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


    &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token function"&gt;Ok&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;results&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;We use a static index (we&amp;rsquo;ll see exactly why in a bit) to query for all the service requests by status and location, and then we project data from the document, including &lt;em&gt;related&lt;/em&gt;&amp;nbsp;document properties (the last two lines in the &lt;code&gt;Select&lt;/code&gt;&amp;nbsp;call).&lt;/p&gt;&lt;p&gt;A ServiceRequest doesn&amp;rsquo;t have a location, it gets that from its associated Property, so during indexing, we pull that from the relevant Property, like so:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token class-name"&gt;Map&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; requests &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt;
    from sr &lt;span class="token keyword"&gt;in&lt;/span&gt; requests
    let property &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;LoadDocument&lt;/span&gt;&lt;span class="token generics"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token class-name"&gt;Property&lt;/span&gt;&lt;span class="token punctuation"&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;PropertyId&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    select &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;Result&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Id&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Id&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Status&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Status&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;OpenedAt&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;OpenedAt&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;UnitId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;UnitId&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;PropertyId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;PropertyId&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Type&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Type&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Description&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;sr&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Description&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Location&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt;  &lt;span class="token class-name"&gt;CreateSpatialField&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;property&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Latitude&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;property&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Longitude&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;You can see how we load the related Property and then index its location for the spatial query (last line).&lt;/p&gt;&lt;p&gt;You can see more interesting features when you drill down to the Unit page, where both its current status and its utility usage are displayed. That is handled using RavenDB&amp;rsquo;s time series feature, and then projected to a nice view on the frontend:&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/k21t_u3JYDeXsjc-d33YaQ.png"/&gt;&lt;/p&gt;&lt;p&gt;In the backend, this is handled using the following action call:&lt;/p&gt;&lt;p&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-dart'&gt;&lt;code class='line-numbers language-dart'&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token class-name"&gt;HttpGet&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"unit/{*unitId}"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;
public &lt;span class="token class-name"&gt;IActionResult&lt;/span&gt; &lt;span class="token class-name"&gt;GetUtilityUsage&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;string unitId&lt;span class="token punctuation"&gt;,&lt;/span&gt; 
&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token class-name"&gt;FromQuery&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token class-name"&gt;DateTime&lt;/span&gt;&lt;span class="token operator"&gt;?&lt;/span&gt; from&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token class-name"&gt;FromQuery&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token class-name"&gt;DateTime&lt;/span&gt;&lt;span class="token operator"&gt;?&lt;/span&gt; to&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
&lt;span class="token keyword"&gt;var&lt;/span&gt; unit &lt;span class="token operator"&gt;=&lt;/span&gt; _session&lt;span class="token punctuation"&gt;.&lt;/span&gt;Load&lt;span class="token generics"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token class-name"&gt;Unit&lt;/span&gt;&lt;span class="token punctuation"&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;unitId&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;unit &lt;span class="token operator"&gt;==&lt;/span&gt; &lt;span class="token keyword"&gt;null&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token class-name"&gt;NotFound&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Unit not found"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


&lt;span class="token keyword"&gt;var&lt;/span&gt; fromDate &lt;span class="token operator"&gt;=&lt;/span&gt; from &lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token class-name"&gt;DateTime.Today.AddMonths&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;-&lt;/span&gt;&lt;span class="token number"&gt;3&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;var&lt;/span&gt; toDate &lt;span class="token operator"&gt;=&lt;/span&gt; to &lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token class-name"&gt;DateTime.Today&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


&lt;span class="token keyword"&gt;var&lt;/span&gt; result &lt;span class="token operator"&gt;=&lt;/span&gt; _session&lt;span class="token punctuation"&gt;.&lt;/span&gt;Query&lt;span class="token generics"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token class-name"&gt;Unit&lt;/span&gt;&lt;span class="token punctuation"&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Where&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;u &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;u&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Id&lt;/span&gt; &lt;span class="token operator"&gt;==&lt;/span&gt; unitId&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Select&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;u &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;PowerUsage&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;RavenQuery.TimeSeries&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;u&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Power"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Where&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;ts &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;ts&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Timestamp&lt;/span&gt; &lt;span class="token operator"&gt;&gt;=&lt;/span&gt; fromDate &lt;span class="token operator"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;ts&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Timestamp&lt;/span&gt; &lt;span class="token operator"&gt;&amp;lt;=&lt;/span&gt; toDate&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GroupBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;g &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;g&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Hours&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Select&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;g &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;g&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Sum&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToList&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;WaterUsage&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;RavenQuery.TimeSeries&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;u&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string-literal"&gt;&lt;span class="token string"&gt;"Water"&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Where&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;ts &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;ts&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Timestamp&lt;/span&gt; &lt;span class="token operator"&gt;&gt;=&lt;/span&gt; fromDate &lt;span class="token operator"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;ts&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Timestamp&lt;/span&gt; &lt;span class="token operator"&gt;&amp;lt;=&lt;/span&gt; toDate&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;GroupBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;g &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;g&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Hours&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Select&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;g &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;g&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Sum&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
            &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToList&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;


&lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token class-name"&gt;Ok&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token keyword"&gt;new&lt;/span&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token class-name"&gt;UnitId&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; unitId&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;UnitNumber&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;unit&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;UnitNumber&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;From&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; fromDate&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;To&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; toDate&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;PowerUsage&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; result&lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;PowerUsage&lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;Results&lt;span class="token operator"&gt;?&lt;/span&gt;
&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Select&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;r &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;UsageDataPoint&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Timestamp&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;r&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;From&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Value&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;r&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Sum&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToList&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;List&lt;/span&gt;&lt;span class="token generics"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token class-name"&gt;UsageDataPoint&lt;/span&gt;&lt;span class="token punctuation"&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token class-name"&gt;WaterUsage&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; result&lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;WaterUsage&lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;Results&lt;span class="token operator"&gt;?&lt;/span&gt;
&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;Select&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;r &lt;span class="token operator"&gt;=&lt;/span&gt;&lt;span class="token operator"&gt;&gt;&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;UsageDataPoint&lt;/span&gt;
    &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token class-name"&gt;Timestamp&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;r&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;From&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
        &lt;span class="token class-name"&gt;Value&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token class-name"&gt;&lt;span class="token namespace"&gt;r&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;/span&gt;Sum&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;ToList&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token keyword"&gt;new&lt;/span&gt; &lt;span class="token class-name"&gt;List&lt;/span&gt;&lt;span class="token generics"&gt;&lt;span class="token punctuation"&gt;&amp;lt;&lt;/span&gt;&lt;span class="token class-name"&gt;UsageDataPoint&lt;/span&gt;&lt;span class="token punctuation"&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p&gt;As you can see, we run a single query to fetch data from multiple time series, which allows us to render this page.&lt;/p&gt;&lt;p&gt;By now, I think you have a pretty good grasp of what the application is about. So get ready for &lt;a href="https://ayende.com/blog/203621-B/propertyspheres-intelligent-telegram-bot?key=1802d1b262ad40169d1fcac2ebf1acff"&gt;the &lt;/a&gt;&lt;em&gt;&lt;a href="https://ayende.com/blog/203621-B/propertyspheres-intelligent-telegram-bot?key=1802d1b262ad40169d1fcac2ebf1acff"&gt;next &lt;/a&gt;&lt;/em&gt;&lt;a href="https://ayende.com/blog/203621-B/propertyspheres-intelligent-telegram-bot?key=1802d1b262ad40169d1fcac2ebf1acff"&gt;post&lt;/a&gt;, where I will talk about how to add AI capabilities to the mix.&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203620-b/propertysphere-sample-application?Key=2ab9814a-35aa-48ee-86f3-f3f9b1accd57</link><guid>http://ayende.com/203620-b/propertysphere-sample-application?Key=2ab9814a-35aa-48ee-86f3-f3f9b1accd57</guid><pubDate>Fri, 19 Dec 2025 12:00:00 GMT</pubDate></item><item><title>RavenDB Kubernetes Operator </title><description>&lt;p&gt;I&amp;rsquo;m happy to announce the official release of the RavenDB Kubernetes Operator. &lt;/p&gt;&lt;p&gt;As organizations use Kubernetes for more and more parts of their infrastructure, the complexity of deploying databases in such an environment is quite a challenge. For RavenDB, you need to handle certificates, persistence, and upgrades, and it is easy for that to become a bottleneck. This release bridges the gap between RavenDB&amp;rsquo;s ease of use and the declarative power of Kubernetes.&lt;/p&gt;&lt;p&gt;&lt;img src="/blog/Images/f7WrI1kJoEBQT5keUwCnQQ.png"/&gt;&lt;/p&gt;&lt;p&gt;If you are new to the concept, think of an operator as software that acts like a Site Reliability Engineer (SRE).&amp;nbsp;Kubernetes is excellent at managing stateless applications, but databases require specific knowledge to manage correctly (e.g., &amp;quot;Don&amp;#39;t upgrade all nodes at once&amp;quot; or &amp;quot;Ensure the leader is stable before restarting&amp;quot;).&lt;/p&gt;&lt;p&gt;The RavenDB Operator extends the Kubernetes API. It allows you to define &lt;em&gt;what&lt;/em&gt;&amp;nbsp;you want your cluster to look like (the &amp;quot;Manifest&amp;quot;), and the Operator works tirelessly in the background to make sure your infrastructure matches that state.&lt;/p&gt;&lt;h3&gt;Why This Matters&lt;/h3&gt;&lt;p&gt;Previously, deploying a secure, clustered RavenDB instance on K8s required manual configuration of StatefulSets, Services, and complex TLS certificate chains.&lt;/p&gt;&lt;p&gt;With the RavenDB Kubernetes Operator, everything is driven by a single custom resource: &lt;code&gt;RavenDBCluster&lt;/code&gt;. You provide the specs, and the Operator handles the heavy lifting, ensuring your deployments are fully reproducible, secure, and declarative.&lt;/p&gt;&lt;p&gt;Here is what the Operator brings to the table:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Automated Security &amp;amp; Certificate Management: Whether you are using Let&amp;rsquo;s Encrypt or Self-Signed certificates, the Operator handles bootstrapping, distribution, and rotation.For the Operator&amp;rsquo;s own webhook certificate, the Operator uses cert-manager behind the scenes, since that is not exposed externally.&lt;/li&gt;&lt;li&gt;Safe Rolling Upgrades: Database upgrades can be scary. The Operator orchestrates upgrades node-by-node, using safety gates to ensure the cluster is healthy and data is safe before moving to the next node. If a gate fails, the upgrade stops automatically.&lt;/li&gt;&lt;li&gt;Flexible External Access: Exposing a database outside K8s is often a networking headache. We&amp;rsquo;ve added dedicated support for AWS NLB, Azure Load Balancer, HAProxy, Traefik, and NGINX, giving you production-ready access strategies out of the box.&lt;/li&gt;&lt;li&gt;Storage Orchestration: Declarative control over your data, logs, and audit volumes, supporting local paths, AWS EBS, and Azure Disks.&lt;/li&gt;&lt;li&gt;One-Click Deploy: Using our official Helm chart, you can spin up a fully operational cluster in minutes.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The Operator is available now via Helm and works on EKS, AKS, Kind, Minikube, and Kubeadm clusters.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;ArtifactHub (Helm Chart):&lt;a href="https://artifacthub.io/packages/helm/ravendb-operator/ravendb-operator"&gt;Get the Chart&lt;/a&gt;&lt;/li&gt;&lt;li&gt;GitHub Repository:&lt;a href="https://github.com/ravendb/ravendb-operator"&gt;View Source &amp;amp; Examples&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Quickstart Guide:&lt;a href="https://www.google.com/search?q=https://github.com/ravendb/ravendb-operator%3Ftab%3Dreadme-ov-file%23installation"&gt;Read the Docs&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We look forward to seeing what you build with RavenDB on Kubernetes!&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/203587-b/ravendb-kubernetes-operator?Key=1d5af4d7-9a6e-42a5-84a2-5d446342e2bb</link><guid>http://ayende.com/203587-b/ravendb-kubernetes-operator?Key=1d5af4d7-9a6e-42a5-84a2-5d446342e2bb</guid><pubDate>Mon, 15 Dec 2025 12:00:00 GMT</pubDate></item></channel></rss>